{"author":{"address":"0x0c3743ac31156269ea0ea04bdb1864645017a92b","user":"https://learnblockchain.cn/people/19204"},"content":{"body":"## 编写测试\r\n\r\n使用Foundry框架，在scripts目录下编写部署脚本\r\n\r\n我们分为两个，一个用于部署合约，一个用与填充部署时所需要的合约信息\r\n\r\n我们需要部署  DSCEngine 和 DecentralizedStableCoin 这两个合约 \r\n\r\nDSCEngine的构造函数是这样的\r\n\r\n```solidity\r\n // 构造函数，初始化抵押品和价格源\r\n    constructor(address[] memory tokenAddresses, address[] memory priceFeedAddresses, address dscAddress) {\r\n        if (tokenAddresses.length != priceFeedAddresses.length) {\r\n            revert DSCEngine__TokenAddressesAndPriceFeedAddressesLengthsMustBeTheSame();\r\n        }\r\n\r\n        for (uint256 i = 0; i \u003c tokenAddresses.length; i++) {\r\n            if (tokenAddresses[i] == address(0)) {\r\n                revert DSCEngine__TokenAddressesAndPriceFeedAddressesLengthsMustBeTheSame();\r\n            }\r\n            priceFeeds[tokenAddresses[i]] = priceFeedAddresses[i];\r\n            _collateralTokens.push(tokenAddresses[i]);\r\n        }\r\n        i_dsc = DecentralizedStableCoin(dscAddress);\r\n    }\r\n\r\n```\r\n\r\n我们需要明确地知道 token地址 和 priceFeed地址。在sepolia测试网的环境下，我们可以访问chainlink官网去获取到这些信息。如果是在本地进行测试，我们则需要mock合约去设置这些信息。\r\n\r\nDecentralizedStableCoin 的构造函数是这样的\r\n\r\n```solidity\r\n  constructor() ERC20(\"DecentralizedStableCoin\", \"DSC\") Ownable(msg.sender) {}\r\n```\r\n\r\n这里的信息都是已经填充好的，需要的就是 eth 和 btc 的信息\r\n\r\n### 部署合约脚本\r\n\r\n我们引用openzeppelin里面的mock合约\r\n\r\n#### HelperConfig\r\n\r\n##### ERC20mock\r\n\r\n我们并不需要全部的erc20的功能，根据我们前面写的合约，我们只需要授权，转移，铸造，销毁这四个功能。\r\n\r\n```solidity\r\n// SPDX-License-Identifier: MIT\r\npragma solidity ^0.8.20;\r\n\r\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\r\n\r\ncontract ERC20Mock is ERC20 {\r\n    constructor(string memory name, string memory symbol, address initialAccount, uint256 initialBalance)\r\n        payable\r\n        ERC20(name, symbol)\r\n    {\r\n        _mint(initialAccount, initialBalance);\r\n    }\r\n\r\n    function mint(address account, uint256 amount) public {\r\n        _mint(account, amount);\r\n    }\r\n\r\n    function burn(address account, uint256 amount) public {\r\n        _burn(account, amount);\r\n    }\r\n\r\n    function transferInternal(address from, address to, uint256 value) public {\r\n        _transfer(from, to, value);\r\n    }\r\n\r\n    function approveInternal(address owner, address spender, uint256 value) public {\r\n        _approve(owner, spender, value);\r\n    }\r\n}\r\n\r\n```\r\n\r\n##### MockV3Aggregator\r\n\r\n```solidity\r\n// SPDX-License-Identifier: MIT\r\npragma solidity ^0.8.18;\r\n\r\n/**\r\n * @title MockV3Aggregator\r\n * @notice Based on the FluxAggregator contract\r\n * @notice Use this contract when you need to test\r\n * other contract's ability to read data from an\r\n * aggregator contract, but how the aggregator got\r\n * its answer is unimportant\r\n */\r\ncontract MockV3Aggregator {\r\n    uint256 public constant version = 0;\r\n\r\n    uint8 public decimals;\r\n    int256 public latestAnswer;\r\n    uint256 public latestTimestamp;\r\n    uint256 public latestRound;\r\n\r\n    mapping(uint256 =\u003e int256) public getAnswer;\r\n    mapping(uint256 =\u003e uint256) public getTimestamp;\r\n    mapping(uint256 =\u003e uint256) private getStartedAt;\r\n\r\n    constructor(uint8 _decimals, int256 _initialAnswer) {\r\n        decimals = _decimals;\r\n        updateAnswer(_initialAnswer);\r\n    }\r\n\r\n    function updateAnswer(int256 _answer) public {\r\n        latestAnswer = _answer;\r\n        latestTimestamp = block.timestamp;\r\n        latestRound++;\r\n        getAnswer[latestRound] = _answer;\r\n        getTimestamp[latestRound] = block.timestamp;\r\n        getStartedAt[latestRound] = block.timestamp;\r\n    }\r\n\r\n    function updateRoundData(uint80 _roundId, int256 _answer, uint256 _timestamp, uint256 _startedAt) public {\r\n        latestRound = _roundId;\r\n        latestAnswer = _answer;\r\n        latestTimestamp = _timestamp;\r\n        getAnswer[latestRound] = _answer;\r\n        getTimestamp[latestRound] = _timestamp;\r\n        getStartedAt[latestRound] = _startedAt;\r\n    }\r\n\r\n    function getRoundData(uint80 _roundId)\r\n        external\r\n        view\r\n        returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)\r\n    {\r\n        return (_roundId, getAnswer[_roundId], getStartedAt[_roundId], getTimestamp[_roundId], _roundId);\r\n    }\r\n\r\n    function latestRoundData()\r\n        external\r\n        view\r\n        returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)\r\n    {\r\n        return (\r\n            uint80(latestRound),\r\n            getAnswer[latestRound],\r\n            getStartedAt[latestRound],\r\n            getTimestamp[latestRound],\r\n            uint80(latestRound)\r\n        );\r\n    }\r\n\r\n    function description() external pure returns (string memory) {\r\n        return \"v0.6/tests/MockV3Aggregator.sol\";\r\n    }\r\n}\r\n\r\n```\r\n\r\n这里起作用的就是构造函数部分，其他函数不重要\r\n\r\n有了这两个信息，我们就可以开始编写 HelperConfig 合约\r\n\r\n##### 编写 HelperConfig 合约\r\n\r\n确定传入 MockV3Aggregator 的参数\r\n\r\n```solidity\r\n\tuint8 public constant DECIMALS = 8;\r\n    uint256 public constant wethPrice = 2000e8;\r\n    uint256 public constant wbtcPrice = 20000e8;\r\n```\r\n\r\n确定传入的信息结构\r\n\r\n```solidity\r\nstruct NetworkConfig {\r\n        address wethUsdPriceFeed;\r\n        address wbtcUsdPriceFeed;\r\n        address weth;\r\n        address wbtc;\r\n        uint256 deployerKey;\r\n    }\r\n```\r\n\r\n这里 deployerKey 如果是在sepolia上，就是你的私钥，本地的话就是anvil用户的私钥地址\r\n\r\n```solidity\r\n uint256 public DEFAULT_ANVIL_PRIVATE_KEY = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80;\r\n```\r\n\r\n对于构造函数来讲，我们需要通过链id来确定我们需要的信息，sepolia的链id 是 11155111，本地链是31337。\r\n\r\n```solidity\r\nconstructor() {\r\n        if (block.chainid == 11155111) {\r\n            activeNetworkConfig = getSepoliaEthConfig();\r\n        } else {\r\n            activeNetworkConfig = getAnvilEthConfig();\r\n        }\r\n    }\r\n```\r\n\r\n对于获取sepolia链的信息，就是去访问chainlink的官网\r\n\r\n\r\n![78C38EF384D7F07100AC5B072ECF2C17.png](https://img.learnblockchain.cn/attachments/2025/03/GiKONlCn67c714d36d633.png)\r\n\r\n对于本地链的信息\r\n\r\n我们需要将我们的合约部署在链上，这一个状态跟我们平常设置变量是不一样的。\r\n\r\n我们需要使用到Foundry的作弊码，在本地链上广播我们的合约，状态才可以被记录在链上，为我们后续提供访问\r\n\r\n```solidity\r\nvm.startBroadcast();\r\n//...code \r\nvm.stopBroadcast();\r\n```\r\n\r\n这样就是在广播我们的合约，通过new 创建合约对象，部署上链\r\n\r\n```solidity\r\nvm.startBroadcast();\r\n        MockV3Aggregator wethUsdPriceFeed = new MockV3Aggregator(DECIMALS, int256(wethPrice));\r\n        MockV3Aggregator wbtcUsdPriceFeed = new MockV3Aggregator(DECIMALS, int256(wbtcPrice));\r\n        ERC20Mock wethMock = new ERC20Mock(\"Wrapped Ether\", \"WETH\", msg.sender, 1000e8);\r\n        ERC20Mock wbtcMock = new ERC20Mock(\"Wrapped Bitcoin\", \"WBTC\", msg.sender, 1000e8);\r\n        vm.stopBroadcast();\r\n```\r\n\r\n完整的代码如下\r\n\r\n```solidity\r\n function getAnvilEthConfig() public returns (NetworkConfig memory anvilConfig) {\r\n        if (activeNetworkConfig.wethUsdPriceFeed != address(0)) {\r\n            return activeNetworkConfig;\r\n        }\r\n\r\n        vm.startBroadcast();\r\n        MockV3Aggregator wethUsdPriceFeed = new MockV3Aggregator(DECIMALS, int256(wethPrice));\r\n        MockV3Aggregator wbtcUsdPriceFeed = new MockV3Aggregator(DECIMALS, int256(wbtcPrice));\r\n        ERC20Mock wethMock = new ERC20Mock(\"Wrapped Ether\", \"WETH\", msg.sender, 1000e8);\r\n        ERC20Mock wbtcMock = new ERC20Mock(\"Wrapped Bitcoin\", \"WBTC\", msg.sender, 1000e8);\r\n        vm.stopBroadcast();\r\n\r\n        anvilConfig = NetworkConfig({\r\n            wethUsdPriceFeed: address(wethUsdPriceFeed),\r\n            wbtcUsdPriceFeed: address(wbtcUsdPriceFeed),\r\n            weth: address(wethMock),\r\n            wbtc: address(wbtcMock),\r\n            deployerKey: DEFAULT_ANVIL_PRIVATE_KEY\r\n        });\r\n    }\r\n```\r\n\r\n#### DeployDSC\r\n\r\n所有的信息都已经知道，我们需要 import 将要部署的合约\r\n\r\n```solidity\r\n// SPDX-License-Identifier: MIT\r\npragma solidity ^0.8.20;\r\n\r\nimport {Script, console} from \"forge-std/Script.sol\";\r\nimport {DecentralizedStableCoin} from \"../src/DecentralizedStableCoin.sol\";\r\nimport {DSCEngine} from \"../src/DSCEngine.sol\";\r\nimport {HelperConfig} from \"./HelperConfig.s.sol\";\r\n\r\n```\r\n\r\n之后通过使用合约名来实例化对象，传入构造函数需要的参数，并返回对象\r\n\r\n```solidity\r\n// SPDX-License-Identifier: MIT\r\npragma solidity ^0.8.20;\r\n\r\nimport {Script, console} from \"forge-std/Script.sol\";\r\nimport {DecentralizedStableCoin} from \"../src/DecentralizedStableCoin.sol\";\r\nimport {DSCEngine} from \"../src/DSCEngine.sol\";\r\nimport {HelperConfig} from \"./HelperConfig.s.sol\";\r\n\r\ncontract DeployDSC is Script {\r\n    address[] public tokenAddresses;\r\n    address[] public priceFeedAddresses;\r\n\r\n    function run() external returns (DecentralizedStableCoin, DSCEngine, HelperConfig) {\r\n        HelperConfig helperConfig = new HelperConfig();\r\n        (address wethUsdPriceFeed, address wbtcUsdPriceFeed, address weth, address wbtc, uint256 deployerKey) =\r\n            helperConfig.activeNetworkConfig();\r\n        tokenAddresses = [weth, wbtc];\r\n        priceFeedAddresses = [wethUsdPriceFeed, wbtcUsdPriceFeed];\r\n\r\n        vm.startBroadcast(deployerKey);\r\n        DecentralizedStableCoin dsc = new DecentralizedStableCoin();\r\n        DSCEngine dsce = new DSCEngine(tokenAddresses, priceFeedAddresses, address(dsc));\r\n        // 将 DSC 的所有权转移给 engine\r\n        dsc.transferOwnership(address(dsce));\r\n        vm.stopBroadcast();\r\n        return (dsc, dsce, helperConfig);\r\n    }\r\n}\r\n\r\n```\r\n\r\n将 DSC 的所有权转移给 DSCEngine 非常关键，这关系到mint的权限问题\r\n\r\n### DSCEngine测试\r\n\r\n跟我们部署合约时所需要的变量一样\r\n\r\n```solidity\r\n    address public wethUsdPriceFeed;\r\n    address public wbtcUsdPriceFeed;\r\n    address public weth;\r\n    address public wbtc;\r\n    uint256 public deployerKey;\r\n```\r\n\r\n我们将引用 DeployDSC 合约，实例化DeployDSC对象，启用run函数，获取返回值\r\n\r\n```solidity\r\n  deployer = new DeployDSC();\r\n  (dsc,dsce,helperConfig) = deployer.run();\r\n  (wethUsdPriceFeed,wbtcUsdPriceFeed, weth,wbtc,deployerKey) =\r\n  helperConfig.activeNetworkConfig();\r\n```\r\n\r\n之后，调用weth里面的mint方法，为我们的用户铸造代币\r\n\r\n```solidity\r\n// ether 是一个单位转换器，1 ether = 1e18\r\nuint256 public constant STARTING_BALANCE = 10 ether;\r\n\r\nERC20Mock(weth).mint(user, STARTING_USER_BALANCE);\r\nERC20Mock(wbtc).mint(user, STARTING_USER_BALANCE);\r\n```\r\n\r\n#### 构造函数测试\r\n\r\n测试也差不多时按照我们写合约时的思路进行编写，我们首先对构造函数进行测试，确定 if 语句是否真的生效了。\r\n\r\n- 创建变量数组\r\n\r\n  ```solidity\r\n   address[] public tokenAddresses;\r\n   address[] public feedAddresses;\r\n  ```\r\n\r\n- 使用push方法添加变量\r\n\r\n  ```solidity\r\n  tokenAddresses.push(weth);\r\n  feedAddresses.push(ethUsdPriceFeed);\r\n  feedAddresses.push(btcUsdPriceFeed);\r\n  ```\r\n\r\n  按照正常情况下，交易是会回滚的，但我们也正期望那样子，来确定if语句是否生效\r\n\r\n  这里用到了 vm.expectRevert 方法，如字面意思，只有当下面的操作回滚了，该测试函数才算通过。这里通过获取错误事件的函数选择器来达到效果。\r\n\r\n  我们返回去看看我们的错误事件是怎么命名的\r\n\r\n  ```solidity\r\n  if (tokenAddresses.length != priceFeedAddresses.length) {\r\n              revert DSCEngine__TokenAddressesAndPriceFeedAddressesLengthsMustBeTheSame();\r\n          }\r\n  ```\r\n\r\n  那我们方法中就如下这样定义，本质也就是围绕if语句去写\r\n\r\n  ```solidity\r\n  vm.expectRevert(DSCEngine.DSCEngine__TokenAddressesAndPriceFeedAddressesLengthsMustBeTheSame.selector);\r\n  \r\n  new DSCEngine(tokenAddresses, feedAddresses, address(dsc));\r\n  ```\r\n\r\n  两个if 语句的测试\r\n\r\n  ```solidity\r\n   //构造函数测试\r\n      function test_constructor_reverts_if_token_addresses_and_price_feed_addresses_lengths_are_not_the_same() public {\r\n          tokenAddresses.push(weth);\r\n          tokenAddresses.push(wbtc);\r\n          priceFeedAddresses.push(wethUsdPriceFeed);\r\n          vm.expectRevert(DSCEngine.DSCEngine__TokenAddressesAndPriceFeedAddressesLengthsMustBeTheSame.selector);\r\n          new DSCEngine(tokenAddresses,priceFeedAddresses,address(dsc));\r\n      }\r\n      \r\n      function test_constructor_reverts_if_token_address_is_zero_address() public {\r\n          tokenAddresses.push(address(0));\r\n          tokenAddresses.push(wbtc);\r\n          priceFeedAddresses.push(wethUsdPriceFeed);\r\n          priceFeedAddresses.push(wbtcUsdPriceFeed);\r\n          vm.expectRevert(DSCEngine.DSCEngine__TokenAddressIsZeroAddress.selector);\r\n          new DSCEngine(tokenAddresses,priceFeedAddresses,address(dsc));\r\n      }\r\n  ```\r\n\r\n#### 价格测试\r\n\r\n先看看我们之前写的两个关于 AggregatorV3Interface 的函数\r\n\r\n给出定值数量的usd，换算成原生代币有多少个\r\n\r\n```solidity\r\n  function getTokenAmountFromUsd(address tokenCollateralAddress, uint256 usdAmountIn) public view returns (uint256) {\r\n        AggregatorV3Interface priceFeed = AggregatorV3Interface(priceFeeds[tokenCollateralAddress]);\r\n        (, int256 price,,,) = priceFeed.latestRoundData();\r\n        return (usdAmountIn * _PRECISION) / (uint256(price) * _ADDITIONAL_FEED_PRECISION);\r\n    }\r\n```\r\n\r\n给出定值数量的token，换算成ustd有多少个\r\n\r\n```solidity\r\n function getUsdValue(address token, uint256 amount) public view returns (uint256) {\r\n        return _getUsdValue(token, amount);\r\n    }\r\n\r\n    /**\r\n     * @notice 计算代币的USD价值\r\n     * @dev 使用Chainlink预言机获取价格，并进行精度转换\r\n     * 例如：1 ETH = 1000 USD，Chainlink返回1000 * 1e8\r\n     * @param token 代币地址\r\n     * @param amount 代币数量\r\n     * @return USD价值（18位精度）\r\n     */\r\n    function _getUsdValue(address token, uint256 amount) private view returns (uint256) {\r\n        AggregatorV3Interface priceFeed = AggregatorV3Interface(priceFeeds[token]);\r\n        (, int256 price,,,) = priceFeed.latestRoundData();\r\n        return ((uint256(price) * _ADDITIONAL_FEED_PRECISION * amount) / _PRECISION);\r\n    }\r\n\r\n```\r\n\r\n对应的测试\r\n\r\n```solidity\r\n\t// 价格测试\r\n    // 测试 getTokenAmountFromUsd 函数\r\n    function test_getTokenAmountFromUsd_returns_correct_amount() public {\r\n        // 0.05 这样的小数需要转换为整数形式，通常是通过乘以相应的精度（比如 1e18）来实现。\r\n        uint256 usdAmount = 100 ether;\r\n        uint256 expectedWethAmount = 0.05 ether;\r\n        uint256 wethAmount = dsce.getTokenAmountFromUsd(weth,usdAmount);\r\n        assertEq(wethAmount,expectedWethAmount);\r\n    }\r\n    // 测试 getUsdValue 函数\r\n    function test_getUsdValue_returns_correct_price() public {\r\n        uint256 ethAmount = 10 ether;\r\n        //uint256 expectedWethValue = 2000e8;\r\n        // 2000$/ETH * 10 ether = 20000 e18\r\n        uint256 expectedWethValue = 20000e18;\r\n        uint256 wethUsdPrice = dsce.getUsdValue(weth,ethAmount);\r\n        assertEq(wethUsdPrice,expectedWethValue);\r\n    }\r\n\r\n```\r\n\r\n#### 质押函数检测\r\n\r\n##### 转账功能\r\n\r\n之前写的函数\r\n\r\n```solidity\r\nfunction despositCollateral(address tokenCollateralAddress\r\n, uint256 amountCollateral) public  {\r\n        _collateralDeposited[msg.sender][tokenCollateralAddress] += amountCollateral;\r\n        emit CollateralDeposited(msg.sender, tokenCollateralAddress, amountCollateral);\r\n        bool success = IERC20(tokenCollateralAddress).transferFrom(msg.sender, address(this), amountCollateral);\r\n        if(!success){\r\n            revert DSCEngine__TransferFailed();\r\n        }\r\n```\r\n\r\n触发revert的条件是success的值为假。这个值跟transferFrom函数有关。也就是我们需要创建一个transferFrom不完善的 ERC20Mock 来模拟这个环境。我们把之前写的DSC代币合约中的transferFrom函数重写置空，return false，把功能破坏就行了。\r\n\r\n```solidity\r\n  function transferFrom(address _from, address _to, uint256 _amount) public override returns (bool) {\r\n        return false;\r\n    }\r\n```\r\n\r\n我们之前在setUp函数中部署的dsce合约是跟这个不同，因为各个功能都是完好的。所以我们等于是要重新起一个环境，只不过是dsc代币合约不同。\r\n\r\n至此，我们需要 代币地址，DSCEngine，铸造代币，授权转账\r\n\r\n**对应的测试**\r\n\r\n- 创建代币对象，参数赋值\r\n\r\n  ```solidity\r\n  MockFailedTransferFrom mockDsc = new MockFailedTransferFrom();\r\n  tokenAddresses = [address(mockDsc)];\r\n  feedAddresses = [ethUsdPriceFeed];\r\n  ```\r\n\r\n- 创建engine对象，参数赋值\r\n\r\n  ```solidity\r\n   vm.prank(owner);\r\n          DSCEngine mockDsce = new DSCEngine(tokenAddresses, feedAddresses, address(mockDsc));\r\n  ```\r\n\r\n- 铸币\r\n\r\n  ```solidity\r\n  mockDsc.mint(user, amountCollateral);\r\n  ```\r\n\r\n- 授权转账\r\n\r\n  ```solidity\r\n  vm.startPrank(owner);\r\n  ERC20Mock(address(mockDsc)).approve(address(dsce),100 ether);\r\n  vm.expectRevert(DSCEngine.DSCEngine__TransferFailed.selector);\r\n  dsce.depositCollateral(address(mockDsc),100 ether);\r\n  vm.stopPrank();\r\n  ```\r\n\r\n##### modifier测试\r\n\r\n###### 测试抵押品为0的情况\r\n\r\n- 测试 depositCollateral 函数在抵押品为0时会 revert\r\n\r\n  ```solidity\r\n  function testRevertIfCollateralZero() public {\r\n          vm.startPrank(user);\r\n          ERC20Mock(weth).approve(address(dsce),100 ether);\r\n          vm.expectRevert(DSCEngine.DSCEngine__MoreThanZero.selector);\r\n          dsce.depositCollateral(weth,0);\r\n          vm.stopPrank();\r\n      }\r\n  \r\n  ```\r\n\r\n###### 测试代币合法性\r\n\r\n- 函数在代币不允许时会 revert\r\n\r\n  跟前面的情况一样，创建一个不在允许列表里的代币进行测试\r\n\r\n  ```solidity\r\n  function testRevertsWithUnapprovedCollateral() public {\r\n          ERC20Mock mockDsc = new ERC20Mock(\"Mock Dsc\",\"MDC\",user,100 ether);\r\n          vm.startPrank(user);\r\n          mockDsc.approve(address(dsce),10 ether);\r\n          vm.expectRevert(DSCEngine.DSCEngine__TokenNotAllowed.selector);\r\n          dsce.depositCollateral(address(mockDsc),10 ether);\r\n          vm.stopPrank();\r\n      }\r\n  ```\r\n\r\n###### modifier定义复用代码\r\n\r\n```solidity\r\n modifier depositedCollateral() {\r\n        vm.startPrank(user);\r\n        ERC20Mock(weth).approve(address(dsce), amountCollateral);\r\n        dsce.depositCollateral(weth, amountCollateral);\r\n        vm.stopPrank();\r\n        _;\r\n    }\r\n```\r\n\r\n##### 测试账户信息\r\n\r\n测试预期铸造的dsc数量跟getAccountInformation函数返回的值是否一致\r\n\r\n合约函数\r\n\r\n```solidity\r\n function getAccountInformation(address user)\r\n        public\r\n        view\r\n        returns (uint256 totalDscMinted, uint256 totalCollateralValue)\r\n    {\r\n        (totalDscMinted, totalCollateralValue) = _getAccountInformation(user);\r\n    }\r\n \r\n function getTokenAmountFromUsd(address tokenCollateralAddress, uint256 usdAmountIn) public view returns (uint256) {\r\n        AggregatorV3Interface priceFeed = AggregatorV3Interface(priceFeeds[tokenCollateralAddress]);\r\n        (, int256 price,,,) = priceFeed.latestRoundData();\r\n        return (usdAmountIn * _PRECISION) / (uint256(price) * _ADDITIONAL_FEED_PRECISION);\r\n    }\r\n```\r\n\r\n对应的测试\r\n\r\n```solidity\r\n  // 测试 账户的信息\r\n    function test_getInformationOfUser() public depositedCollateral {\r\n       (uint256 totalDscMinted,uint256 collateralValueInUsd) = dsce.getAccountInformation(user);\r\n        uint256 expectedDepositAmount = dsce.getTokenAmountFromUsd(weth,collateralValueInUsd);\r\n        assertEq(totalDscMinted,0);\r\n        assertEq(expectedDepositAmount,100 ether);\r\n    }\r\n```\r\n\r\n\r\n\r\n#### 质押铸造函数测试\r\n\r\n##### 测试健康因子\r\n\r\n对于正常情况下\r\n\r\n```\r\nDSC.max = totalColleral * price * 利率\r\n```\r\n\r\n为了测试是否会因为健康因子而revert，我们直接不乘以利率，来去铸造对应数量的dsc代币\r\n\r\n```solidity\r\n function testRevertsIfMintedDscBreaksHealthFactor() public {\r\n        // 获取weth的价格\r\n        (,int256 price,,,) = MockV3Aggregator(wethUsdPriceFeed).latestRoundData();\r\n        // 计算没有经过利率转化mint的dsc数量\r\n        uint256 amountToMint = (amountCollateral * (uint256(price)) * dsce.getAdditionalFeedPrecision()) / 1e18;\r\n        vm.startPrank(user);\r\n        ERC20Mock(weth).approve(address(dsce),amountCollateral);\r\n        vm.expectRevert(DSCEngine.DSCEngine__HealthFactorIsBroken.selector);\r\n        dsce.depositCollateralAndMintDsc(weth,amountCollateral,amountToMint);\r\n        vm.stopPrank();\r\n     }\r\n```\r\n\r\n##### 测试等值铸造dsc\r\n\r\n```solidity\r\n\tfunction testCanMintDsc() public depositedCollateral {\r\n        vm.prank(user);\r\n        dsce.mintDsc(10 ether);\r\n        // 获取用户铸造的dsc数量\r\n        (uint256 totalDscMinted,uint256 collateralValueInUsd) = dsce.getAccountInformation(user);\r\n        uint256 userBalance = totalDscMinted;\r\n        assertEq(userBalance,10 ether);\r\n    }\r\n```\r\n\r\n#### 赎回函数检测\r\n\r\n##### burnDsc零参检测\r\n\r\n测试当burn的dsc数量为0时会 revert\r\n\r\n```solidity\r\n\tfunction testRevertsIfBurnAmountIsZero() public {\r\n        vm.startPrank(user);\r\n        ERC20Mock(weth).approve(address(dsce),amountCollateral);\r\n        dsce.depositCollateralAndMintDsc(weth,amountCollateral,amountToMint);\r\n        vm.expectRevert(DSCEngine.DSCEngine__MoreThanZero.selector);\r\n        dsce.burnDsc(0);\r\n        vm.stopPrank();\r\n    }\r\n```\r\n\r\n##### 转账功能\r\n\r\n跟前面检测质押函数一样，只是使用 transfer 方法进行转账，那么我们重写transfer函数即可，返回false参数\r\n\r\n**mock合约**\r\n\r\n```solidity\r\n// SPDX-License-Identifier: MIT\r\npragma solidity ^0.8.20;\r\n\r\nimport {ERC20Burnable, ERC20} from \"lib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol\";\r\nimport {Ownable} from \"lib/openzeppelin-contracts/contracts/access/Ownable.sol\";\r\n\r\n// 在 OpenZeppelin 合约包的未来版本中，必须使用合约所有者的地址声明 Ownable\r\n// 作为参数。\r\n// 例如：\r\n// constructor（） ERC20（\"去中心化稳定币\"， \"DSC\"） ownable（0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266） {}\r\ncontract MockTransferFailed is ERC20Burnable, Ownable {\r\n    error DecentralizedStableCoin__AmountMustBeGreaterThanZero();\r\n    error DecentralizedStableCoin__BurnAmountExceedsBalance();\r\n    error DecentralizedStableCoin__CannotMintToZeroAddress();\r\n\r\n    event Burned(address indexed from, uint256 amount);\r\n    event Minted(address indexed to, uint256 amount);\r\n\r\n    constructor() ERC20(\"DecentralizedStableCoin\", \"DSC\") Ownable(msg.sender) {}\r\n\r\n    function burn(uint256 _amount) public override onlyOwner {\r\n        uint256 balance = balanceOf(msg.sender);\r\n\r\n        if (_amount \u003c 0) {\r\n            revert DecentralizedStableCoin__AmountMustBeGreaterThanZero();\r\n        }\r\n\r\n        if (balance \u003c _amount) {\r\n            revert DecentralizedStableCoin__BurnAmountExceedsBalance();\r\n        }\r\n\r\n        super.burn(_amount);\r\n        emit Burned(msg.sender, _amount);\r\n    }\r\n\r\n    function mint(address _to, uint256 _amount) public onlyOwner returns (bool) {\r\n        if (_amount \u003c= 0) {\r\n            revert DecentralizedStableCoin__AmountMustBeGreaterThanZero();\r\n        }\r\n\r\n        if (_to == address(0)) {\r\n            revert DecentralizedStableCoin__CannotMintToZeroAddress();\r\n        }\r\n\r\n        _mint(_to, _amount);\r\n\r\n        emit Minted(_to, _amount);\r\n        return true;\r\n    }\r\n\r\n    function transfer(address to, uint256 amount) public override returns (bool) {\r\n        return false;\r\n    }\r\n}\r\n\r\n```\r\n\r\n**对应的测试**\r\n\r\n```solidity\r\n// 测试赎回函数\r\n    // 测试赎回函数在转账失败时会 revert\r\n    function test_redeemCollateral_reverts_if_transfer_failed() public  {\r\n        address owner = msg.sender;\r\n        vm.prank(owner);\r\n        MockTransferFailed mockDsc = new MockTransferFailed();\r\n        vm.prank(owner);\r\n        mockDsc.mint(owner,100 ether);\r\n        tokenAddresses.push(address(mockDsc));\r\n        priceFeedAddresses.push(wethUsdPriceFeed);\r\n        vm.prank(owner);\r\n        DSCEngine Mockdsce = new DSCEngine(tokenAddresses,priceFeedAddresses,address(mockDsc));\r\n        vm.prank(owner);\r\n        mockDsc.transferOwnership(address(Mockdsce));\r\n        vm.startPrank(owner);\r\n        ERC20Mock(address(mockDsc)).approve(address(Mockdsce),10 ether);\r\n        Mockdsce.depositCollateral(address(mockDsc),10 ether);\r\n        vm.expectRevert(DSCEngine.DSCEngine__TransferFailed.selector);\r\n        Mockdsce.redeemCollateral(address(mockDsc),10 ether);\r\n        vm.stopPrank();\r\n    }   \r\n```\r\n\r\n##### 赎回零参检测\r\n\r\n测试赎回函数是否可以赎回 0 抵押品\r\n\r\n```solidity\r\n\tfunction test_redeemCollateral_reverts_if_amount_is_zero() public {\r\n        vm.startPrank(user);\r\n        ERC20Mock(weth).approve(address(dsce),amountCollateral);\r\n        dsce.depositCollateral(weth,amountCollateral);\r\n        vm.expectRevert(DSCEngine.DSCEngine__MoreThanZero.selector);\r\n        dsce.redeemCollateral(weth,0);\r\n        vm.stopPrank();\r\n    }\r\n```\r\n\r\n##### 赎回功能\r\n\r\n```solidity\r\n// 测试是否可以正常赎回\r\n   function testCanRedeemCollateral() public depositedCollateral {\r\n        uint256 startingUserWethBalance = dsce.getCollateralBalanceOfUser(user,weth);\r\n        assertEq(startingUserWethBalance,amountCollateral);\r\n        vm.prank(user);\r\n        dsce.redeemCollateral(weth,amountCollateral);\r\n        uint256 endingUserWethBalance = dsce.getCollateralBalanceOfUser(user,weth);\r\n        assertEq(endingUserWethBalance,0);\r\n   }\r\n```\r\n\r\n##### 预期事件返回参数\r\n\r\n```solidity\r\n // 测试 emitCollateralRedeemedWithCorrectArgs 函数\r\n   function testEmitCollateralRedeemedWithCorrectArgs() public depositedCollateral {\r\n        vm.expectEmit(true,true,false,true,address(dsce));\r\n        emit CollateralRedeemed(user,weth,amountCollateral);\r\n        vm.startPrank(user);\r\n        dsce.redeemCollateral(weth, amountCollateral);\r\n        vm.stopPrank();\r\n    }\r\n```\r\n\r\n- 这里使用  vm.expectEmit(1，2，3，4) 方法\r\n\r\n- 1 到 3 如果值为 true，则表示检查 indexed 参数，反之如果该事件参数前没有indexed，则是 false 值\r\n\r\n- 4 如果值为 true，则表示检查所有非indexed参数，反之则不检查\r\n\r\n  方法示例\r\n\r\n  ```solidity\r\n  // 假设我们有这样一个事件\r\n  event Transfer(\r\n      address indexed from,    // 第一个 indexed 参数\r\n      address indexed to,      // 第二个 indexed 参数\r\n      uint256 amount          // 非 indexed 参数\r\n  );\r\n  \r\n  function testEventEmission() public {\r\n      address user1 = address(1);\r\n      address user2 = address(2);\r\n      uint256 amount = 100;\r\n  \r\n      // 1. 检查所有参数\r\n      vm.expectEmit(true, true, false, true, address(token));\r\n      emit Transfer(user1, user2, amount);\r\n      token.transfer(user1, user2, amount);\r\n      // 解释：\r\n      // - true：检查 from (user1)\r\n      // - true：检查 to (user2)\r\n      // - false：没有第三个 indexed 参数\r\n      // - true：检查 amount (100)\r\n      // - address(token)：事件从这个合约发出\r\n  \r\n      // 2. 只检查发送者和金额\r\n      vm.expectEmit(true, false, false, true, address(token));\r\n      emit Transfer(user1, address(0), amount);\r\n      token.transfer(user1, user2, amount);\r\n      // 解释：接收者可以是任何地址，不影响测试通过\r\n  \r\n      // 3. 只检查发送者\r\n      vm.expectEmit(true, false, false, false, address(token));\r\n      emit Transfer(user1, address(0), 0);\r\n      token.transfer(user1, user2, amount);\r\n      // 解释：只检查发送者是 user1，其他参数可以是任何值\r\n  }\r\n  ```\r\n\r\n#### 赎回并销毁dsc函数\r\n\r\n##### modifier定义复用代码\r\n\r\n```solidity\r\n    modifier depositedCollateralAndMintedDsc() {\r\n        vm.startPrank(user);\r\n        ERC20Mock(weth).approve(address(dsce), amountCollateral);\r\n        dsce.depositCollateralAndMintDsc(weth, amountCollateral, amountToMint);\r\n        vm.stopPrank();\r\n        _;\r\n    }\r\n\r\n```\r\n\r\n##### burnDsc零参检测\r\n\r\n```solidity\r\n// 测试当burn的dsc数量为0时会 revert\r\n    function testMustRedeemMoreThanZero() public depositedCollateralAndMintedDsc {\r\n        vm.startPrank(user);\r\n        dsc.approve(address(dsce),amountToMint);\r\n        vm.expectRevert(DSCEngine.DSCEngine__MoreThanZero.selector);\r\n        dsce.redeemCollateralForDsc(weth,amountCollateral,0);\r\n        vm.stopPrank();\r\n    }\r\n```\r\n\r\n##### 赎回销毁\r\n\r\n这里要注意，第一次授权是因为你要存入抵押品，第二次授权是因为你要让合约销毁你的dsc代币\r\n\r\n```solidity\r\nfunction testCanRedeemCollateralForDsc() public {\r\n        vm.startPrank(user);\r\n        // 1. 先批准 WETH 的使用权\r\n        ERC20Mock(weth).approve(address(dsce), amountCollateral);\r\n        // 2. 存入抵押品并铸造 DSC\r\n        dsce.depositCollateralAndMintDsc(weth, amountCollateral, amountToMint);\r\n        \r\n        // 3. 记录初始抵押品余额\r\n        uint256 startingUserWethBalance = dsce.getCollateralBalanceOfUser(user, weth);\r\n        assertEq(startingUserWethBalance, amountCollateral);\r\n        \r\n        // 4. 批准 DSC 的使用权（用于销毁）\r\n        dsc.approve(address(dsce), amountToMint);\r\n        \r\n        // 5. 赎回抵押品并销毁 DSC\r\n        dsce.redeemCollateralForDsc(weth, amountCollateral, amountToMint);\r\n        \r\n        // 6. 验证抵押品已经全部赎回\r\n        uint256 endingUserWethBalance = dsce.getCollateralBalanceOfUser(user, weth);\r\n        assertEq(endingUserWethBalance, 0);\r\n        vm.stopPrank();\r\n    }\r\n```\r\n\r\n##### 验证计算健康因子函数\r\n\r\n```solidity\r\nfunction testHealthFactorIsBroken() public depositedCollateralAndMintedDsc {\r\n        // 1. 获取抵押品的 USD 价值\r\n        uint256 collateralValueInUsd = dsce.getUsdValue(weth, amountCollateral);\r\n        // 2. 获取用户信息\r\n        (uint256 totalDscMinted,) = dsce.getAccountInformation(user);\r\n        // 3. 计算健康因子\r\n        uint256 expectedHealthFactor = dsce.calculateHealthFactor(totalDscMinted,collateralValueInUsd);\r\n        // 4. 验证健康因子是否破损\r\n        assertEq(dsce.getHealthFactor(user),expectedHealthFactor);\r\n    }\r\n    \r\n//价格变化后\r\nfunction testHealthFactorIsNotBroken() public depositedCollateralAndMintedDsc {\r\n        // 1. 将 ETH 价格更新为 $1000\r\n        MockV3Aggregator(wethUsdPriceFeed).updateAnswer(1000e8); // 注意这里应该是 1000e8 而不是 1000e18\r\n        \r\n        // 2. 新的健康因子计算\r\n        // 抵押品价值 = 10 ETH * $1000 = $10,000\r\n        // 考虑清算阈值后 = $10,000 * 50% = $5,000\r\n        // 健康因子 = $5,000 / $100 = 50\r\n        \r\n        uint256 healthFactor = dsce.getHealthFactor(user);\r\n        assertEq(healthFactor, 50 ether); \r\n\r\n    }\r\n```\r\n\r\n#### 测试清算函数\r\n\r\n##### 清算函数\r\n\r\n这里可以直接设置eth下跌的价格，也可以通过调用函数更新价格，达到动态下跌的场景。我们这里结合在两种去模拟，eth先进一步下跌到 18u，当我们调用清算函数时，清算函数会调用mock代币的burn函数，使得价格再一次更新。\r\n\r\n**mock合约**\r\n\r\n```solidity\r\n// SPDX-License-Identifier: MIT\r\n\r\n// This is considered an Exogenous, Decentralized, Anchored (pegged), Crypto Collateralized low volitility coin\r\n\r\n// Layout of Contract:\r\n// version\r\n// imports\r\n// errors\r\n// interfaces, libraries, contracts\r\n// Type declarations\r\n// State variables\r\n// Events\r\n// Modifiers\r\n// Functions\r\n\r\n// Layout of Functions:\r\n// constructor\r\n// receive function (if exists)\r\n// fallback function (if exists)\r\n// external\r\n// public\r\n// internal\r\n// private\r\n// view \u0026 pure functions\r\n\r\npragma solidity ^0.8.20;\r\n\r\nimport { ERC20Burnable, ERC20 } from \"@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol\";\r\nimport { Ownable } from \"@openzeppelin/contracts/access/Ownable.sol\";\r\nimport { MockV3Aggregator } from \"./MockV3Aggregator.sol\";\r\n\r\n/*\r\n * @title DecentralizedStableCoin\r\n * @author Patrick Collins\r\n * Collateral: Exogenous\r\n * Minting (Stability Mechanism): Decentralized (Algorithmic)\r\n * Value (Relative Stability): Anchored (Pegged to USD)\r\n * Collateral Type: Crypto\r\n *\r\n* This is the contract meant to be owned by DSCEngine. It is a ERC20 token that can be minted and burned by the\r\nDSCEngine smart contract.\r\n */\r\ncontract MockMoreDebtDSC is ERC20Burnable, Ownable {\r\n    error DecentralizedStableCoin__AmountMustBeMoreThanZero();\r\n    error DecentralizedStableCoin__BurnAmountExceedsBalance();\r\n    error DecentralizedStableCoin__NotZeroAddress();\r\n\r\n    address mockAggregator;\r\n\r\n    /*\r\n    In future versions of OpenZeppelin contracts package, Ownable must be declared with an address of the contract owner\r\n    as a parameter.\r\n    For example:\r\n    constructor() ERC20(\"DecentralizedStableCoin\", \"DSC\") Ownable(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266) {}\r\n    Related code changes can be viewed in this commit:\r\n    https://github.com/OpenZeppelin/openzeppelin-contracts/commit/13d5e0466a9855e9305119ed383e54fc913fdc60\r\n    */\r\n    constructor(address _mockAggregator) ERC20(\"DecentralizedStableCoin\", \"DSC\") Ownable(msg.sender) {\r\n        mockAggregator = _mockAggregator;\r\n    }\r\n\r\n    function burn(uint256 _amount) public override onlyOwner {\r\n        // We crash the price\r\n        MockV3Aggregator(mockAggregator).updateAnswer(0);\r\n        uint256 balance = balanceOf(msg.sender);\r\n        if (_amount \u003c= 0) {\r\n            revert DecentralizedStableCoin__AmountMustBeMoreThanZero();\r\n        }\r\n        if (balance \u003c _amount) {\r\n            revert DecentralizedStableCoin__BurnAmountExceedsBalance();\r\n        }\r\n        super.burn(_amount);\r\n    }\r\n\r\n    function mint(address _to, uint256 _amount) external onlyOwner returns (bool) {\r\n        if (_to == address(0)) {\r\n            revert DecentralizedStableCoin__NotZeroAddress();\r\n        }\r\n        if (_amount \u003c= 0) {\r\n            revert DecentralizedStableCoin__AmountMustBeMoreThanZero();\r\n        }\r\n        _mint(_to, _amount);\r\n        return true;\r\n    }\r\n}\r\n```\r\n\r\n**测试函数**\r\n\r\n```solidity\r\n// 测试清算函数\r\n    function testMustImproveHealthFactorOnLiquidation() public {\r\n        // 1. 设置测试环境\r\n        // 创建一个特殊的 DSC mock 合约，这个合约在 burn 时会将价格崩溃到 0\r\n        MockMoreDebtDSC mockDsc = new MockMoreDebtDSC(wethUsdPriceFeed);\r\n        // 设置支持的代币和价格源\r\n        tokenAddresses.push(weth);\r\n        priceFeedAddresses.push(wethUsdPriceFeed);\r\n        \r\n        // 部署新的 DSCEngine 并转移 DSC 的所有权\r\n        address owner = msg.sender;\r\n        vm.prank(owner);\r\n        DSCEngine mockDsce = new DSCEngine(tokenAddresses, priceFeedAddresses, address(mockDsc));\r\n        // 这里的owner是当前测试合约\r\n        mockDsc.transferOwnership(address(mockDsce));\r\n\r\n        // 2. 设置被清算用户\r\n        vm.startPrank(user);\r\n        // 用户批准 DSCEngine 使用其 WETH\r\n        ERC20Mock(address(weth)).approve(address(mockDsce), amountCollateral);\r\n        // 用户存入 10 ETH 并铸造 100 DSC\r\n        mockDsce.depositCollateralAndMintDsc(weth, amountCollateral, amountToMint);\r\n        vm.stopPrank();\r\n\r\n        // 3. 设置清算人\r\n        // 给清算人铸造 1 ETH\r\n        uint256 collateralToLiquidate = 1 ether;\r\n        ERC20Mock(weth).mint(liquidator, collateralToLiquidate);\r\n\r\n        // 4. 清算人的操作\r\n        vm.startPrank(liquidator);\r\n        // 清算人批准 DSCEngine 使用其 WETH\r\n        ERC20Mock(address(weth)).approve(address(mockDsce), collateralToLiquidate);\r\n        // 设置要清算的债务数量\r\n        uint256 debtToCover = 10 ether;\r\n        // 清算人也存入抵押品并铸造 DSC\r\n        mockDsce.depositCollateralAndMintDsc(weth, collateralToLiquidate, amountToMint);\r\n        // 批准 DSCEngine 使用清算人的 DSC\r\n        mockDsc.approve(address(mockDsce), debtToCover);\r\n\r\n        // 5. 触发清算条件\r\n        // 将 ETH 价格更新为 $18，导致用户健康因子破坏\r\n        int256 ethUsdUpdatedPrice = 18e8;\r\n        MockV3Aggregator(wethUsdPriceFeed).updateAnswer(ethUsdUpdatedPrice);\r\n\r\n        // 6. 执行清算\r\n        // 预期清算会失败，因为清算后的健康因子仍然是破坏的\r\n        // 清算 10 DSC 的数量太小，原来债务时 100 dsc,无法显著改善健康因子\r\n        vm.expectRevert(DSCEngine.DSCEngine__HealthFactorIsBroken.selector);\r\n        mockDsce.liquidate(weth, user , debtToCover);\r\n        vm.stopPrank();\r\n    }\r\n```\r\n\r\n##### 健康因子正常时清算？\r\n\r\n```solidity\r\n // 测试处于健康因子正常时，是否可以清算\r\n    function testLiquidationDoesNotHappenIfHealthFactorIsAboveThreshold() public depositedCollateralAndMintedDsc{\r\n        ERC20Mock(weth).approve(address(dsce),amountCollateral);\r\n        vm.startPrank(liquidator);\r\n        ERC20Mock(weth).mint(liquidator,amountCollateral);\r\n        ERC20Mock(weth).approve(address(dsce),amountCollateral);\r\n        dsce.depositCollateralAndMintDsc(weth,amountCollateral,amountToMint);\r\n        uint256 debtToCover = 10 ether;\r\n        ERC20Mock(weth).approve(address(dsce),debtToCover);\r\n        vm.expectRevert(DSCEngine.DSCEngine__HealthFactorIsNotBroken.selector);\r\n        dsce.liquidate(weth,user,debtToCover);\r\n        vm.stopPrank();\r\n    }\r\n```\r\n\r\n##### modifier定义复用代码\r\n\r\n```solidity\r\nmodifier liquidated(){\r\n        vm.startPrank(user);\r\n        ERC20Mock(weth).approve(address(dsce),amountCollateral);\r\n        dsce.depositCollateralAndMintDsc(weth,amountCollateral,amountToMint);\r\n        vm.stopPrank();\r\n        int256 ethUsdUpdatedPrice = 18e8;\r\n\r\n        MockV3Aggregator(wethUsdPriceFeed).updateAnswer(ethUsdUpdatedPrice);\r\n        uint256 userHealthFactor = dsce.getHealthFactor(user);\r\n\r\n        ERC20Mock(address(weth)).mint(liquidator,collateralToCover);\r\n        vm.startPrank(liquidator);\r\n        ERC20Mock(weth).approve(address(dsce),collateralToCover);\r\n        dsce.depositCollateralAndMintDsc(weth,collateralToCover,amountToMint);\r\n        dsc.approve(address(dsce),amountToMint);\r\n        dsce.liquidate(weth,user,amountToMint);\r\n        vm.stopPrank();\r\n        _;\r\n    }\r\n```\r\n\r\n##### 测试清算返回值\r\n\r\n整个过程\r\n\r\n```solidity\r\n清算过程模拟：\r\n初始状态：\r\n用户：10 ETH 抵押品，100 DSC 债务\r\nETH 价格：$2000\r\n健康因子：10\r\n价格暴跌后：\r\nETH 价格：$18\r\n抵押品价值：$180\r\n可用价值：$90\r\n健康因子：0.9\r\n清算操作：\r\n清算人支付：100 DSC\r\n清算人获得：\r\n基础抵押品：100 DSC 等值的 ETH\r\n额外奖励：10% 的 ETH\r\n总计获得：约 6.11 ETH（110 DSC / $18）\r\n最终状态：\r\n用户债务：0 DSC\r\n用户剩余抵押品：约 3.89 ETH\r\n清算人获得：约 6.11 ETH\r\n这个测试验证了完整的清算流程，包括：\r\n价格暴跌触发清算条件\r\n清算人准备足够的 DSC\r\n执行清算并获得抵押品\r\n系统恢复健康状态（因为债务被完全清除）\r\n```\r\n\r\n**测试**\r\n\r\n```solidity\r\n function testLiquidationPayoutIsCorrect() public liquidated{\r\n          uint256 liquidatorWethBalance = ERC20Mock(weth).balanceOf(liquidator);\r\n        uint256 expectedWeth = dsce.getTokenAmountFromUsd(weth, amountToMint)\r\n            + (dsce.getTokenAmountFromUsd(weth, amountToMint) * dsce.getLiquidationBonus() / dsce.getLiquidationPrecision());\r\n        uint256 hardCodedExpected = 6_111_111_111_111_111_110;\r\n        assertEq(liquidatorWethBalance, hardCodedExpected);\r\n        assertEq(liquidatorWethBalance, expectedWeth);\r\n    }\r\n```\r\n\r\n##### 测试剩余抵押拼价值\r\n\r\n整个过程\r\n\r\n```\r\n这个测试验证了：\r\n清算过程中抵押品扣除的准确性\r\n用户剩余抵押品的计算正确性\r\n确保用户在清算后仍然保留了应得的抵押品\r\n具体数值：\r\n初始：用户有 10 ETH（$180）\r\n清算：约 6.11 ETH（$110）\r\n剩余：约 3.89 ETH（$70）\r\n这个测试很重要，因为它确保：\r\n清算机制不会错误地拿走过多抵押品\r\n用户的剩余资产被正确保留\r\n整个清算计算过程的准确性\r\n```\r\n\r\n**测试**\r\n\r\n```solidity\r\n function testUserStillHasSomeEthAfterLiquidation() public liquidated {\r\n        // Get how much WETH the user lost\r\n        uint256 amountLiquidated = dsce.getTokenAmountFromUsd(weth, amountToMint)\r\n            + (dsce.getTokenAmountFromUsd(weth, amountToMint) * dsce.getLiquidationBonus() / dsce.getLiquidationPrecision());\r\n\r\n        uint256 usdAmountLiquidated = dsce.getUsdValue(weth, amountLiquidated);\r\n        uint256 expectedUserCollateralValueInUsd = dsce.getUsdValue(weth, amountCollateral) - (usdAmountLiquidated);\r\n\r\n        (, uint256 userCollateralValueInUsd) = dsce.getAccountInformation(user);\r\n        uint256 hardCodedExpectedValue = 70_000_000_000_000_000_020;\r\n        assertEq(userCollateralValueInUsd, expectedUserCollateralValueInUsd);\r\n        assertEq(userCollateralValueInUsd, hardCodedExpectedValue);\r\n    }\r\n\r\n```","title":"稳定币项目构建 (二）"},"history":null,"timestamp":1741100983,"version":1}