稳定币项目构建 (二)

基础测试编写,加深对该项目的理解 对应代码库:https://github.com/langjiyunmie/Defi-stablecoin/

编写测试

使用Foundry框架,在scripts目录下编写部署脚本

我们分为两个,一个用于部署合约,一个用与填充部署时所需要的合约信息

我们需要部署 DSCEngine 和 DecentralizedStableCoin 这两个合约

DSCEngine的构造函数是这样的

 // 构造函数,初始化抵押品和价格源
    constructor(address[] memory tokenAddresses, address[] memory priceFeedAddresses, address dscAddress) {
        if (tokenAddresses.length != priceFeedAddresses.length) {
            revert DSCEngine__TokenAddressesAndPriceFeedAddressesLengthsMustBeTheSame();
        }

        for (uint256 i = 0; i < tokenAddresses.length; i++) {
            if (tokenAddresses[i] == address(0)) {
                revert DSCEngine__TokenAddressesAndPriceFeedAddressesLengthsMustBeTheSame();
            }
            priceFeeds[tokenAddresses[i]] = priceFeedAddresses[i];
            _collateralTokens.push(tokenAddresses[i]);
        }
        i_dsc = DecentralizedStableCoin(dscAddress);
    }

我们需要明确地知道 token地址 和 priceFeed地址。在sepolia测试网的环境下,我们可以访问chainlink官网去获取到这些信息。如果是在本地进行测试,我们则需要mock合约去设置这些信息。

DecentralizedStableCoin 的构造函数是这样的

  constructor() ERC20("DecentralizedStableCoin", "DSC") Ownable(msg.sender) {}

这里的信息都是已经填充好的,需要的就是 eth 和 btc 的信息

部署合约脚本

我们引用openzeppelin里面的mock合约

HelperConfig

ERC20mock

我们并不需要全部的erc20的功能,根据我们前面写的合约,我们只需要授权,转移,铸造,销毁这四个功能。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract ERC20Mock is ERC20 {
    constructor(string memory name, string memory symbol, address initialAccount, uint256 initialBalance)
        payable
        ERC20(name, symbol)
    {
        _mint(initialAccount, initialBalance);
    }

    function mint(address account, uint256 amount) public {
        _mint(account, amount);
    }

    function burn(address account, uint256 amount) public {
        _burn(account, amount);
    }

    function transferInternal(address from, address to, uint256 value) public {
        _transfer(from, to, value);
    }

    function approveInternal(address owner, address spender, uint256 value) public {
        _approve(owner, spender, value);
    }
}
MockV3Aggregator
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

/**
 * @title MockV3Aggregator
 * @notice Based on the FluxAggregator contract
 * @notice Use this contract when you need to test
 * other contract's ability to read data from an
 * aggregator contract, but how the aggregator got
 * its answer is unimportant
 */
contract MockV3Aggregator {
    uint256 public constant version = 0;

    uint8 public decimals;
    int256 public latestAnswer;
    uint256 public latestTimestamp;
    uint256 public latestRound;

    mapping(uint256 => int256) public getAnswer;
    mapping(uint256 => uint256) public getTimestamp;
    mapping(uint256 => uint256) private getStartedAt;

    constructor(uint8 _decimals, int256 _initialAnswer) {
        decimals = _decimals;
        updateAnswer(_initialAnswer);
    }

    function updateAnswer(int256 _answer) public {
        latestAnswer = _answer;
        latestTimestamp = block.timestamp;
        latestRound++;
        getAnswer[latestRound] = _answer;
        getTimestamp[latestRound] = block.timestamp;
        getStartedAt[latestRound] = block.timestamp;
    }

    function updateRoundData(uint80 _roundId, int256 _answer, uint256 _timestamp, uint256 _startedAt) public {
        latestRound = _roundId;
        latestAnswer = _answer;
        latestTimestamp = _timestamp;
        getAnswer[latestRound] = _answer;
        getTimestamp[latestRound] = _timestamp;
        getStartedAt[latestRound] = _startedAt;
    }

    function getRoundData(uint80 _roundId)
        external
        view
        returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
    {
        return (_roundId, getAnswer[_roundId], getStartedAt[_roundId], getTimestamp[_roundId], _roundId);
    }

    function latestRoundData()
        external
        view
        returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
    {
        return (
            uint80(latestRound),
            getAnswer[latestRound],
            getStartedAt[latestRound],
            getTimestamp[latestRound],
            uint80(latestRound)
        );
    }

    function description() external pure returns (string memory) {
        return "v0.6/tests/MockV3Aggregator.sol";
    }
}

这里起作用的就是构造函数部分,其他函数不重要

有了这两个信息,我们就可以开始编写 HelperConfig 合约

编写 HelperConfig 合约

确定传入 MockV3Aggregator 的参数

    uint8 public constant DECIMALS = 8;
    uint256 public constant wethPrice = 2000e8;
    uint256 public constant wbtcPrice = 20000e8;

确定传入的信息结构

struct NetworkConfig {
        address wethUsdPriceFeed;
        address wbtcUsdPriceFeed;
        address weth;
        address wbtc;
        uint256 deployerKey;
    }

这里 deployerKey 如果是在sepolia上,就是你的私钥,本地的话就是anvil用户的私钥地址

 uint256 public DEFAULT_ANVIL_PRIVATE_KEY = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80;

对于构造函数来讲,我们需要通过链id来确定我们需要的信息,sepolia的链id 是 11155111,本地链是31337。

constructor() {
        if (block.chainid == 11155111) {
            activeNetworkConfig = getSepoliaEthConfig();
        } else {
            activeNetworkConfig = getAnvilEthConfig();
        }
    }

对于获取sepolia链的信息,就是去访问chainlink的官网

78C38EF384D7F07100AC5B072ECF2C17.png

对于本地链的信息

我们需要将我们的合约部署在链上,这一个状态跟我们平常设置变量是不一样的。

我们需要使用到Foundry的作弊码,在本地链上广播我们的合约,状态才可以被记录在链上,为我们后续提供访问

vm.startBroadcast();
//...code 
vm.stopBroadcast();

这样就是在广播我们的合约,通过new 创建合约对象,部署上链

vm.startBroadcast();
        MockV3Aggregator wethUsdPriceFeed = new MockV3Aggregator(DECIMALS, int256(wethPrice));
        MockV3Aggregator wbtcUsdPriceFeed = new MockV3Aggregator(DECIMALS, int256(wbtcPrice));
        ERC20Mock wethMock = new ERC20Mock("Wrapped Ether", "WETH", msg.sender, 1000e8);
        ERC20Mock wbtcMock = new ERC20Mock("Wrapped Bitcoin", "WBTC", msg.sender, 1000e8);
        vm.stopBroadcast();

完整的代码如下

 function getAnvilEthConfig() public returns (NetworkConfig memory anvilConfig) {
        if (activeNetworkConfig.wethUsdPriceFeed != address(0)) {
            return activeNetworkConfig;
        }

        vm.startBroadcast();
        MockV3Aggregator wethUsdPriceFeed = new MockV3Aggregator(DECIMALS, int256(wethPrice));
        MockV3Aggregator wbtcUsdPriceFeed = new MockV3Aggregator(DECIMALS, int256(wbtcPrice));
        ERC20Mock wethMock = new ERC20Mock("Wrapped Ether", "WETH", msg.sender, 1000e8);
        ERC20Mock wbtcMock = new ERC20Mock("Wrapped Bitcoin", "WBTC", msg.sender, 1000e8);
        vm.stopBroadcast();

        anvilConfig = NetworkConfig({
            wethUsdPriceFeed: address(wethUsdPriceFeed),
            wbtcUsdPriceFeed: address(wbtcUsdPriceFeed),
            weth: address(wethMock),
            wbtc: address(wbtcMock),
            deployerKey: DEFAULT_ANVIL_PRIVATE_KEY
        });
    }

DeployDSC

所有的信息都已经知道,我们需要 import 将要部署的合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Script, console} from "forge-std/Script.sol";
import {DecentralizedStableCoin} from "../src/DecentralizedStableCoin.sol";
import {DSCEngine} from "../src/DSCEngine.sol";
import {HelperConfig} from "./HelperConfig.s.sol";

之后通过使用合约名来实例化对象,传入构造函数需要的参数,并返回对象

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Script, console} from "forge-std/Script.sol";
import {DecentralizedStableCoin} from "../src/DecentralizedStableCoin.sol";
import {DSCEngine} from "../src/DSCEngine.sol";
import {HelperConfig} from "./HelperConfig.s.sol";

contract DeployDSC is Script {
    address[] public tokenAddresses;
    address[] public priceFeedAddresses;

    function run() external returns (DecentralizedStableCoin, DSCEngine, HelperConfig) {
        HelperConfig helperConfig = new HelperConfig();
        (address wethUsdPriceFeed, address wbtcUsdPriceFeed, address weth, address wbtc, uint256 deployerKey) =
            helperConfig.activeNetworkConfig();
        tokenAddresses = [weth, wbtc];
        priceFeedAddresses = [wethUsdPriceFeed, wbtcUsdPriceFeed];

        vm.startBroadcast(deployerKey);
        DecentralizedStableCoin dsc = new DecentralizedStableCoin();
        DSCEngine dsce = new DSCEngine(tokenAddresses, priceFeedAddresses, address(dsc));
        // 将 DSC 的所有权转移给 engine
        dsc.transferOwnership(address(dsce));
        vm.stopBroadcast();
        return (dsc, dsce, helperConfig);
    }
}

将 DSC 的所有权转移给 DSCEngine 非常关键,这关系到mint的权限问题

DSCEngine测试

跟我们部署合约时所需要的变量一样

    address public wethUsdPriceFeed;
    address public wbtcUsdPriceFeed;
    address public weth;
    address public wbtc;
    uint256 public deployerKey;

我们将引用 DeployDSC 合约,实例化DeployDSC对象,启用run函数,获取返回值

  deployer = new DeployDSC();
  (dsc,dsce,helperConfig) = deployer.run();
  (wethUsdPriceFeed,wbtcUsdPriceFeed, weth,wbtc,deployerKey) =
  helperConfig.activeNetworkConfig();

之后,调用weth里面的mint方法,为我们的用户铸造代币

// ether 是一个单位转换器,1 ether = 1e18
uint256 public constant STARTING_BALANCE = 10 ether;

ERC20Mock(weth).mint(user, STARTING_USER_BALANCE);
ERC20Mock(wbtc).mint(user, STARTING_USER_BALANCE);

构造函数测试

测试也差不多时按照我们写合约时的思路进行编写,我们首先对构造函数进行测试,确定 if 语句是否真的生效了。

  • 创建变量数组

    address[] public tokenAddresses;
    address[] public feedAddresses;
  • 使用push方法添加变量

    tokenAddresses.push(weth);
    feedAddresses.push(ethUsdPriceFeed);
    feedAddresses.push(btcUsdPriceFeed);

    按照正常情况下,交易是会回滚的,但我们也正期望那样子,来确定if语句是否生效

    这里用到了 vm.expectRevert 方法,如字面意思,只有当下面的操作回滚了,该测试函数才算通过。这里通过获取错误事件的函数选择器来达到效果。

    我们返回去看看我们的错误事件是怎么命名的

    if (tokenAddresses.length != priceFeedAddresses.length) {
              revert DSCEngine__TokenAddressesAndPriceFeedAddressesLengthsMustBeTheSame();
          }

    那我们方法中就如下这样定义,本质也就是围绕if语句去写

    vm.expectRevert(DSCEngine.DSCEngine__TokenAddressesAndPriceFeedAddressesLengthsMustBeTheSame.selector);
    
    new DSCEngine(tokenAddresses, feedAddresses, address(dsc));

    两个if 语句的测试

    //构造函数测试
      function test_constructor_reverts_if_token_addresses_and_price_feed_addresses_lengths_are_not_the_same() public {
          tokenAddresses.push(weth);
          tokenAddresses.push(wbtc);
          priceFeedAddresses.push(wethUsdPriceFeed);
          vm.expectRevert(DSCEngine.DSCEngine__TokenAddressesAndPriceFeedAddressesLengthsMustBeTheSame.selector);
          new DSCEngine(tokenAddresses,priceFeedAddresses,address(dsc));
      }
    
      function test_constructor_reverts_if_token_address_is_zero_address() public {
          tokenAddresses.push(address(0));
          tokenAddresses.push(wbtc);
          priceFeedAddresses.push(wethUsdPriceFeed);
          priceFeedAddresses.push(wbtcUsdPriceFeed);
          vm.expectRevert(DSCEngine.DSCEngine__TokenAddressIsZeroAddress.selector);
          new DSCEngine(tokenAddresses,priceFeedAddresses,address(dsc));
      }

价格测试

先看看我们之前写的两个关于 AggregatorV3Interface 的函数

给出定值数量的usd,换算成原生代币有多少个

  function getTokenAmountFromUsd(address tokenCollateralAddress, uint256 usdAmountIn) public view returns (uint256) {
        AggregatorV3Interface priceFeed = AggregatorV3Interface(priceFeeds[tokenCollateralAddress]);
        (, int256 price,,,) = priceFeed.latestRoundData();
        return (usdAmountIn * _PRECISION) / (uint256(price) * _ADDITIONAL_FEED_PRECISION);
    }

给出定值数量的token,换算成ustd有多少个

 function getUsdValue(address token, uint256 amount) public view returns (uint256) {
        return _getUsdValue(token, amount);
    }

    /**
     * @notice 计算代币的USD价值
     * @dev 使用Chainlink预言机获取价格,并进行精度转换
     * 例如:1 ETH = 1000 USD,Chainlink返回1000 * 1e8
     * @param token 代币地址
     * @param amount 代币数量
     * @return USD价值(18位精度)
     */
    function _getUsdValue(address token, uint256 amount) private view returns (uint256) {
        AggregatorV3Interface priceFeed = AggregatorV3Interface(priceFeeds[token]);
        (, int256 price,,,) = priceFeed.latestRoundData();
        return ((uint256(price) * _ADDITIONAL_FEED_PRECISION * amount) / _PRECISION);
    }

对应的测试

    // 价格测试
    // 测试 getTokenAmountFromUsd 函数
    function test_getTokenAmountFromUsd_returns_correct_amount() public {
        // 0.05 这样的小数需要转换为整数形式,通常是通过乘以相应的精度(比如 1e18)来实现。
        uint256 usdAmount = 100 ether;
        uint256 expectedWethAmount = 0.05 ether;
        uint256 wethAmount = dsce.getTokenAmountFromUsd(weth,usdAmount);
        assertEq(wethAmount,expectedWethAmount);
    }
    // 测试 getUsdValue 函数
    function test_getUsdValue_returns_correct_price() public {
        uint256 ethAmount = 10 ether;
        //uint256 expectedWethValue = 2000e8;
        // 2000$/ETH * 10 ether = 20000 e18
        uint256 expectedWethValue = 20000e18;
        uint256 wethUsdPrice = dsce.getUsdValue(weth,ethAmount);
        assertEq(wethUsdPrice,expectedWethValue);
    }

质押函数检测

转账功能

之前写的函数

function despositCollateral(address tokenCollateralAddress
, uint256 amountCollateral) public  {
        _collateralDeposited[msg.sender][tokenCollateralAddress] += amountCollateral;
        emit CollateralDeposited(msg.sender, tokenCollateralAddress, amountCollateral);
        bool success = IERC20(tokenCollateralAddress).transferFrom(msg.sender, address(this), amountCollateral);
        if(!success){
            revert DSCEngine__TransferFailed();
        }

触发revert的条件是success的值为假。这个值跟transferFrom函数有关。也就是我们需要创建一个transferFrom不完善的 ERC20Mock 来模拟这个环境。我们把之前写的DSC代币合约中的transferFrom函数重写置空,return false,把功能破坏就行了。

  function transferFrom(address _from, address _to, uint256 _amount) public override returns (bool) {
        return false;
    }

我们之前在setUp函数中部署的dsce合约是跟这个不同,因为各个功能都是完好的。所以我们等于是要重新起一个环境,只不过是dsc代币合约不同。

至此,我们需要 代币地址,DSCEngine,铸造代币,授权转账

对应的测试

  • 创建代币对象,参数赋值

    MockFailedTransferFrom mockDsc = new MockFailedTransferFrom();
    tokenAddresses = [address(mockDsc)];
    feedAddresses = [ethUsdPriceFeed];
  • 创建engine对象,参数赋值

    vm.prank(owner);
          DSCEngine mockDsce = new DSCEngine(tokenAddresses, feedAddresses, address(mockDsc));
  • 铸币

    mockDsc.mint(user, amountCollateral);
  • 授权转账

    vm.startPrank(owner);
    ERC20Mock(address(mockDsc)).approve(address(dsce),100 ether);
    vm.expectRevert(DSCEngine.DSCEngine__TransferFailed.selector);
    dsce.depositCollateral(address(mockDsc),100 ether);
    vm.stopPrank();
modifier测试
测试抵押品为0的情况
  • 测试 depositCollateral 函数在抵押品为0时会 revert

    function testRevertIfCollateralZero() public {
          vm.startPrank(user);
          ERC20Mock(weth).approve(address(dsce),100 ether);
          vm.expectRevert(DSCEngine.DSCEngine__MoreThanZero.selector);
          dsce.depositCollateral(weth,0);
          vm.stopPrank();
      }
    
测试代币合法性
  • 函数在代币不允许时会 revert

    跟前面的情况一样,创建一个不在允许列表里的代币进行测试

    function testRevertsWithUnapprovedCollateral() public {
          ERC20Mock mockDsc = new ERC20Mock("Mock Dsc","MDC",user,100 ether);
          vm.startPrank(user);
          mockDsc.approve(address(dsce),10 ether);
          vm.expectRevert(DSCEngine.DSCEngine__TokenNotAllowed.selector);
          dsce.depositCollateral(address(mockDsc),10 ether);
          vm.stopPrank();
      }
modifier定义复用代码
 modifier depositedCollateral() {
        vm.startPrank(user);
        ERC20Mock(weth).approve(address(dsce), amountCollateral);
        dsce.depositCollateral(weth, amountCollateral);
        vm.stopPrank();
        _;
    }
测试账户信息

测试预期铸造的dsc数量跟getAccountInformation函数返回的值是否一致

合约函数

 function getAccountInformation(address user)
        public
        view
        returns (uint256 totalDscMinted, uint256 totalCollateralValue)
    {
        (totalDscMinted, totalCollateralValue) = _getAccountInformation(user);
    }

 function getTokenAmountFromUsd(address tokenCollateralAddress, uint256 usdAmountIn) public view returns (uint256) {
        AggregatorV3Interface priceFeed = AggregatorV3Interface(priceFeeds[tokenCollateralAddress]);
        (, int256 price,,,) = priceFeed.latestRoundData();
        return (usdAmountIn * _PRECISION) / (uint256(price) * _ADDITIONAL_FEED_PRECISION);
    }

对应的测试

  // 测试 账户的信息
    function test_getInformationOfUser() public depositedCollateral {
       (uint256 totalDscMinted,uint256 collateralValueInUsd) = dsce.getAccountInformation(user);
        uint256 expectedDepositAmount = dsce.getTokenAmountFromUsd(weth,collateralValueInUsd);
        assertEq(totalDscMinted,0);
        assertEq(expectedDepositAmount,100 ether);
    }

质押铸造函数测试

测试健康因子

对于正常情况下

DSC.max = totalColleral * price * 利率

为了测试是否会因为健康因子而revert,我们直接不乘以利率,来去铸造对应数量的dsc代币

 function testRevertsIfMintedDscBreaksHealthFactor() public {
        // 获取weth的价格
        (,int256 price,,,) = MockV3Aggregator(wethUsdPriceFeed).latestRoundData();
        // 计算没有经过利率转化mint的dsc数量
        uint256 amountToMint = (amountCollateral * (uint256(price)) * dsce.getAdditionalFeedPrecision()) / 1e18;
        vm.startPrank(user);
        ERC20Mock(weth).approve(address(dsce),amountCollateral);
        vm.expectRevert(DSCEngine.DSCEngine__HealthFactorIsBroken.selector);
        dsce.depositCollateralAndMintDsc(weth,amountCollateral,amountToMint);
        vm.stopPrank();
     }
测试等值铸造dsc
    function testCanMintDsc() public depositedCollateral {
        vm.prank(user);
        dsce.mintDsc(10 ether);
        // 获取用户铸造的dsc数量
        (uint256 totalDscMinted,uint256 collateralValueInUsd) = dsce.getAccountInformation(user);
        uint256 userBalance = totalDscMinted;
        assertEq(userBalance,10 ether);
    }

赎回函数检测

burnDsc零参检测

测试当burn的dsc数量为0时会 revert

    function testRevertsIfBurnAmountIsZero() public {
        vm.startPrank(user);
        ERC20Mock(weth).approve(address(dsce),amountCollateral);
        dsce.depositCollateralAndMintDsc(weth,amountCollateral,amountToMint);
        vm.expectRevert(DSCEngine.DSCEngine__MoreThanZero.selector);
        dsce.burnDsc(0);
        vm.stopPrank();
    }
转账功能

跟前面检测质押函数一样,只是使用 transfer 方法进行转账,那么我们重写transfer函数即可,返回false参数

mock合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ERC20Burnable, ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol";

// 在 OpenZeppelin 合约包的未来版本中,必须使用合约所有者的地址声明 Ownable
// 作为参数。
// 例如:
// constructor() ERC20("去中心化稳定币", "DSC") ownable(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266) {}
contract MockTransferFailed is ERC20Burnable, Ownable {
    error DecentralizedStableCoin__AmountMustBeGreaterThanZero();
    error DecentralizedStableCoin__BurnAmountExceedsBalance();
    error DecentralizedStableCoin__CannotMintToZeroAddress();

    event Burned(address indexed from, uint256 amount);
    event Minted(address indexed to, uint256 amount);

    constructor() ERC20("DecentralizedStableCoin", "DSC") Ownable(msg.sender) {}

    function burn(uint256 _amount) public override onlyOwner {
        uint256 balance = balanceOf(msg.sender);

        if (_amount < 0) {
            revert DecentralizedStableCoin__AmountMustBeGreaterThanZero();
        }

        if (balance < _amount) {
            revert DecentralizedStableCoin__BurnAmountExceedsBalance();
        }

        super.burn(_amount);
        emit Burned(msg.sender, _amount);
    }

    function mint(address _to, uint256 _amount) public onlyOwner returns (bool) {
        if (_amount <= 0) {
            revert DecentralizedStableCoin__AmountMustBeGreaterThanZero();
        }

        if (_to == address(0)) {
            revert DecentralizedStableCoin__CannotMintToZeroAddress();
        }

        _mint(_to, _amount);

        emit Minted(_to, _amount);
        return true;
    }

    function transfer(address to, uint256 amount) public override returns (bool) {
        return false;
    }
}

对应的测试

// 测试赎回函数
    // 测试赎回函数在转账失败时会 revert
    function test_redeemCollateral_reverts_if_transfer_failed() public  {
        address owner = msg.sender;
        vm.prank(owner);
        MockTransferFailed mockDsc = new MockTransferFailed();
        vm.prank(owner);
        mockDsc.mint(owner,100 ether);
        tokenAddresses.push(address(mockDsc));
        priceFeedAddresses.push(wethUsdPriceFeed);
        vm.prank(owner);
        DSCEngine Mockdsce = new DSCEngine(tokenAddresses,priceFeedAddresses,address(mockDsc));
        vm.prank(owner);
        mockDsc.transferOwnership(address(Mockdsce));
        vm.startPrank(owner);
        ERC20Mock(address(mockDsc)).approve(address(Mockdsce),10 ether);
        Mockdsce.depositCollateral(address(mockDsc),10 ether);
        vm.expectRevert(DSCEngine.DSCEngine__TransferFailed.selector);
        Mockdsce.redeemCollateral(address(mockDsc),10 ether);
        vm.stopPrank();
    }   
赎回零参检测

测试赎回函数是否可以赎回 0 抵押品

    function test_redeemCollateral_reverts_if_amount_is_zero() public {
        vm.startPrank(user);
        ERC20Mock(weth).approve(address(dsce),amountCollateral);
        dsce.depositCollateral(weth,amountCollateral);
        vm.expectRevert(DSCEngine.DSCEngine__MoreThanZero.selector);
        dsce.redeemCollateral(weth,0);
        vm.stopPrank();
    }
赎回功能
// 测试是否可以正常赎回
   function testCanRedeemCollateral() public depositedCollateral {
        uint256 startingUserWethBalance = dsce.getCollateralBalanceOfUser(user,weth);
        assertEq(startingUserWethBalance,amountCollateral);
        vm.prank(user);
        dsce.redeemCollateral(weth,amountCollateral);
        uint256 endingUserWethBalance = dsce.getCollateralBalanceOfUser(user,weth);
        assertEq(endingUserWethBalance,0);
   }
预期事件返回参数
 // 测试 emitCollateralRedeemedWithCorrectArgs 函数
   function testEmitCollateralRedeemedWithCorrectArgs() public depositedCollateral {
        vm.expectEmit(true,true,false,true,address(dsce));
        emit CollateralRedeemed(user,weth,amountCollateral);
        vm.startPrank(user);
        dsce.redeemCollateral(weth, amountCollateral);
        vm.stopPrank();
    }
  • 这里使用 vm.expectEmit(1,2,3,4) 方法

  • 1 到 3 如果值为 true,则表示检查 indexed 参数,反之如果该事件参数前没有indexed,则是 false 值

  • 4 如果值为 true,则表示检查所有非indexed参数,反之则不检查

    方法示例

    // 假设我们有这样一个事件
    event Transfer(
      address indexed from,    // 第一个 indexed 参数
      address indexed to,      // 第二个 indexed 参数
      uint256 amount          // 非 indexed 参数
    );
    
    function testEventEmission() public {
      address user1 = address(1);
      address user2 = address(2);
      uint256 amount = 100;
    
      // 1. 检查所有参数
      vm.expectEmit(true, true, false, true, address(token));
      emit Transfer(user1, user2, amount);
      token.transfer(user1, user2, amount);
      // 解释:
      // - true:检查 from (user1)
      // - true:检查 to (user2)
      // - false:没有第三个 indexed 参数
      // - true:检查 amount (100)
      // - address(token):事件从这个合约发出
    
      // 2. 只检查发送者和金额
      vm.expectEmit(true, false, false, true, address(token));
      emit Transfer(user1, address(0), amount);
      token.transfer(user1, user2, amount);
      // 解释:接收者可以是任何地址,不影响测试通过
    
      // 3. 只检查发送者
      vm.expectEmit(true, false, false, false, address(token));
      emit Transfer(user1, address(0), 0);
      token.transfer(user1, user2, amount);
      // 解释:只检查发送者是 user1,其他参数可以是任何值
    }

赎回并销毁dsc函数

modifier定义复用代码
    modifier depositedCollateralAndMintedDsc() {
        vm.startPrank(user);
        ERC20Mock(weth).approve(address(dsce), amountCollateral);
        dsce.depositCollateralAndMintDsc(weth, amountCollateral, amountToMint);
        vm.stopPrank();
        _;
    }
burnDsc零参检测
// 测试当burn的dsc数量为0时会 revert
    function testMustRedeemMoreThanZero() public depositedCollateralAndMintedDsc {
        vm.startPrank(user);
        dsc.approve(address(dsce),amountToMint);
        vm.expectRevert(DSCEngine.DSCEngine__MoreThanZero.selector);
        dsce.redeemCollateralForDsc(weth,amountCollateral,0);
        vm.stopPrank();
    }
赎回销毁

这里要注意,第一次授权是因为你要存入抵押品,第二次授权是因为你要让合约销毁你的dsc代币

function testCanRedeemCollateralForDsc() public {
        vm.startPrank(user);
        // 1. 先批准 WETH 的使用权
        ERC20Mock(weth).approve(address(dsce), amountCollateral);
        // 2. 存入抵押品并铸造 DSC
        dsce.depositCollateralAndMintDsc(weth, amountCollateral, amountToMint);

        // 3. 记录初始抵押品余额
        uint256 startingUserWethBalance = dsce.getCollateralBalanceOfUser(user, weth);
        assertEq(startingUserWethBalance, amountCollateral);

        // 4. 批准 DSC 的使用权(用于销毁)
        dsc.approve(address(dsce), amountToMint);

        // 5. 赎回抵押品并销毁 DSC
        dsce.redeemCollateralForDsc(weth, amountCollateral, amountToMint);

        // 6. 验证抵押品已经全部赎回
        uint256 endingUserWethBalance = dsce.getCollateralBalanceOfUser(user, weth);
        assertEq(endingUserWethBalance, 0);
        vm.stopPrank();
    }
验证计算健康因子函数
function testHealthFactorIsBroken() public depositedCollateralAndMintedDsc {
        // 1. 获取抵押品的 USD 价值
        uint256 collateralValueInUsd = dsce.getUsdValue(weth, amountCollateral);
        // 2. 获取用户信息
        (uint256 totalDscMinted,) = dsce.getAccountInformation(user);
        // 3. 计算健康因子
        uint256 expectedHealthFactor = dsce.calculateHealthFactor(totalDscMinted,collateralValueInUsd);
        // 4. 验证健康因子是否破损
        assertEq(dsce.getHealthFactor(user),expectedHealthFactor);
    }

//价格变化后
function testHealthFactorIsNotBroken() public depositedCollateralAndMintedDsc {
        // 1. 将 ETH 价格更新为 $1000
        MockV3Aggregator(wethUsdPriceFeed).updateAnswer(1000e8); // 注意这里应该是 1000e8 而不是 1000e18

        // 2. 新的健康因子计算
        // 抵押品价值 = 10 ETH * $1000 = $10,000
        // 考虑清算阈值后 = $10,000 * 50% = $5,000
        // 健康因子 = $5,000 / $100 = 50

        uint256 healthFactor = dsce.getHealthFactor(user);
        assertEq(healthFactor, 50 ether); 

    }

测试清算函数

清算函数

这里可以直接设置eth下跌的价格,也可以通过调用函数更新价格,达到动态下跌的场景。我们这里结合在两种去模拟,eth先进一步下跌到 18u,当我们调用清算函数时,清算函数会调用mock代币的burn函数,使得价格再一次更新。

mock合约

// SPDX-License-Identifier: MIT

// This is considered an Exogenous, Decentralized, Anchored (pegged), Crypto Collateralized low volitility coin

// Layout of Contract:
// version
// imports
// errors
// interfaces, libraries, contracts
// Type declarations
// State variables
// Events
// Modifiers
// Functions

// Layout of Functions:
// constructor
// receive function (if exists)
// fallback function (if exists)
// external
// public
// internal
// private
// view & pure functions

pragma solidity ^0.8.20;

import { ERC20Burnable, ERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { MockV3Aggregator } from "./MockV3Aggregator.sol";

/*
 * @title DecentralizedStableCoin
 * @author Patrick Collins
 * Collateral: Exogenous
 * Minting (Stability Mechanism): Decentralized (Algorithmic)
 * Value (Relative Stability): Anchored (Pegged to USD)
 * Collateral Type: Crypto
 *
* This is the contract meant to be owned by DSCEngine. It is a ERC20 token that can be minted and burned by the
DSCEngine smart contract.
 */
contract MockMoreDebtDSC is ERC20Burnable, Ownable {
    error DecentralizedStableCoin__AmountMustBeMoreThanZero();
    error DecentralizedStableCoin__BurnAmountExceedsBalance();
    error DecentralizedStableCoin__NotZeroAddress();

    address mockAggregator;

    /*
    In future versions of OpenZeppelin contracts package, Ownable must be declared with an address of the contract owner
    as a parameter.
    For example:
    constructor() ERC20("DecentralizedStableCoin", "DSC") Ownable(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266) {}
    Related code changes can be viewed in this commit:
    https://github.com/OpenZeppelin/openzeppelin-contracts/commit/13d5e0466a9855e9305119ed383e54fc913fdc60
    */
    constructor(address _mockAggregator) ERC20("DecentralizedStableCoin", "DSC") Ownable(msg.sender) {
        mockAggregator = _mockAggregator;
    }

    function burn(uint256 _amount) public override onlyOwner {
        // We crash the price
        MockV3Aggregator(mockAggregator).updateAnswer(0);
        uint256 balance = balanceOf(msg.sender);
        if (_amount <= 0) {
            revert DecentralizedStableCoin__AmountMustBeMoreThanZero();
        }
        if (balance < _amount) {
            revert DecentralizedStableCoin__BurnAmountExceedsBalance();
        }
        super.burn(_amount);
    }

    function mint(address _to, uint256 _amount) external onlyOwner returns (bool) {
        if (_to == address(0)) {
            revert DecentralizedStableCoin__NotZeroAddress();
        }
        if (_amount <= 0) {
            revert DecentralizedStableCoin__AmountMustBeMoreThanZero();
        }
        _mint(_to, _amount);
        return true;
    }
}

测试函数

// 测试清算函数
    function testMustImproveHealthFactorOnLiquidation() public {
        // 1. 设置测试环境
        // 创建一个特殊的 DSC mock 合约,这个合约在 burn 时会将价格崩溃到 0
        MockMoreDebtDSC mockDsc = new MockMoreDebtDSC(wethUsdPriceFeed);
        // 设置支持的代币和价格源
        tokenAddresses.push(weth);
        priceFeedAddresses.push(wethUsdPriceFeed);

        // 部署新的 DSCEngine 并转移 DSC 的所有权
        address owner = msg.sender;
        vm.prank(owner);
        DSCEngine mockDsce = new DSCEngine(tokenAddresses, priceFeedAddresses, address(mockDsc));
        // 这里的owner是当前测试合约
        mockDsc.transferOwnership(address(mockDsce));

        // 2. 设置被清算用户
        vm.startPrank(user);
        // 用户批准 DSCEngine 使用其 WETH
        ERC20Mock(address(weth)).approve(address(mockDsce), amountCollateral);
        // 用户存入 10 ETH 并铸造 100 DSC
        mockDsce.depositCollateralAndMintDsc(weth, amountCollateral, amountToMint);
        vm.stopPrank();

        // 3. 设置清算人
        // 给清算人铸造 1 ETH
        uint256 collateralToLiquidate = 1 ether;
        ERC20Mock(weth).mint(liquidator, collateralToLiquidate);

        // 4. 清算人的操作
        vm.startPrank(liquidator);
        // 清算人批准 DSCEngine 使用其 WETH
        ERC20Mock(address(weth)).approve(address(mockDsce), collateralToLiquidate);
        // 设置要清算的债务数量
        uint256 debtToCover = 10 ether;
        // 清算人也存入抵押品并铸造 DSC
        mockDsce.depositCollateralAndMintDsc(weth, collateralToLiquidate, amountToMint);
        // 批准 DSCEngine 使用清算人的 DSC
        mockDsc.approve(address(mockDsce), debtToCover);

        // 5. 触发清算条件
        // 将 ETH 价格更新为 $18,导致用户健康因子破坏
        int256 ethUsdUpdatedPrice = 18e8;
        MockV3Aggregator(wethUsdPriceFeed).updateAnswer(ethUsdUpdatedPrice);

        // 6. 执行清算
        // 预期清算会失败,因为清算后的健康因子仍然是破坏的
        // 清算 10 DSC 的数量太小,原来债务时 100 dsc,无法显著改善健康因子
        vm.expectRevert(DSCEngine.DSCEngine__HealthFactorIsBroken.selector);
        mockDsce.liquidate(weth, user , debtToCover);
        vm.stopPrank();
    }
健康因子正常时清算?
 // 测试处于健康因子正常时,是否可以清算
    function testLiquidationDoesNotHappenIfHealthFactorIsAboveThreshold() public depositedCollateralAndMintedDsc{
        ERC20Mock(weth).approve(address(dsce),amountCollateral);
        vm.startPrank(liquidator);
        ERC20Mock(weth).mint(liquidator,amountCollateral);
        ERC20Mock(weth).approve(address(dsce),amountCollateral);
        dsce.depositCollateralAndMintDsc(weth,amountCollateral,amountToMint);
        uint256 debtToCover = 10 ether;
        ERC20Mock(weth).approve(address(dsce),debtToCover);
        vm.expectRevert(DSCEngine.DSCEngine__HealthFactorIsNotBroken.selector);
        dsce.liquidate(weth,user,debtToCover);
        vm.stopPrank();
    }
modifier定义复用代码
modifier liquidated(){
        vm.startPrank(user);
        ERC20Mock(weth).approve(address(dsce),amountCollateral);
        dsce.depositCollateralAndMintDsc(weth,amountCollateral,amountToMint);
        vm.stopPrank();
        int256 ethUsdUpdatedPrice = 18e8;

        MockV3Aggregator(wethUsdPriceFeed).updateAnswer(ethUsdUpdatedPrice);
        uint256 userHealthFactor = dsce.getHealthFactor(user);

        ERC20Mock(address(weth)).mint(liquidator,collateralToCover);
        vm.startPrank(liquidator);
        ERC20Mock(weth).approve(address(dsce),collateralToCover);
        dsce.depositCollateralAndMintDsc(weth,collateralToCover,amountToMint);
        dsc.approve(address(dsce),amountToMint);
        dsce.liquidate(weth,user,amountToMint);
        vm.stopPrank();
        _;
    }
测试清算返回值

整个过程

清算过程模拟:
初始状态:
用户:10 ETH 抵押品,100 DSC 债务
ETH 价格:$2000
健康因子:10
价格暴跌后:
ETH 价格:$18
抵押品价值:$180
可用价值:$90
健康因子:0.9
清算操作:
清算人支付:100 DSC
清算人获得:
基础抵押品:100 DSC 等值的 ETH
额外奖励:10% 的 ETH
总计获得:约 6.11 ETH(110 DSC / $18)
最终状态:
用户债务:0 DSC
用户剩余抵押品:约 3.89 ETH
清算人获得:约 6.11 ETH
这个测试验证了完整的清算流程,包括:
价格暴跌触发清算条件
清算人准备足够的 DSC
执行清算并获得抵押品
系统恢复健康状态(因为债务被完全清除)

测试

 function testLiquidationPayoutIsCorrect() public liquidated{
          uint256 liquidatorWethBalance = ERC20Mock(weth).balanceOf(liquidator);
        uint256 expectedWeth = dsce.getTokenAmountFromUsd(weth, amountToMint)
            + (dsce.getTokenAmountFromUsd(weth, amountToMint) * dsce.getLiquidationBonus() / dsce.getLiquidationPrecision());
        uint256 hardCodedExpected = 6_111_111_111_111_111_110;
        assertEq(liquidatorWethBalance, hardCodedExpected);
        assertEq(liquidatorWethBalance, expectedWeth);
    }
测试剩余抵押拼价值

整个过程

这个测试验证了:
清算过程中抵押品扣除的准确性
用户剩余抵押品的计算正确性
确保用户在清算后仍然保留了应得的抵押品
具体数值:
初始:用户有 10 ETH($180)
清算:约 6.11 ETH($110)
剩余:约 3.89 ETH($70)
这个测试很重要,因为它确保:
清算机制不会错误地拿走过多抵押品
用户的剩余资产被正确保留
整个清算计算过程的准确性

测试

 function testUserStillHasSomeEthAfterLiquidation() public liquidated {
        // Get how much WETH the user lost
        uint256 amountLiquidated = dsce.getTokenAmountFromUsd(weth, amountToMint)
            + (dsce.getTokenAmountFromUsd(weth, amountToMint) * dsce.getLiquidationBonus() / dsce.getLiquidationPrecision());

        uint256 usdAmountLiquidated = dsce.getUsdValue(weth, amountLiquidated);
        uint256 expectedUserCollateralValueInUsd = dsce.getUsdValue(weth, amountCollateral) - (usdAmountLiquidated);

        (, uint256 userCollateralValueInUsd) = dsce.getAccountInformation(user);
        uint256 hardCodedExpectedValue = 70_000_000_000_000_000_020;
        assertEq(userCollateralValueInUsd, expectedUserCollateralValueInUsd);
        assertEq(userCollateralValueInUsd, hardCodedExpectedValue);
    }
  • 原创
  • 学分: 19
  • 分类: DeFi
  • 标签:
点赞 1
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

1 条评论

请先 登录 后评论
浪迹陨灭
浪迹陨灭
0x0c37...a92b
专注于solidity智能合约的开发