基础测试编写,加深对该项目的理解 对应代码库: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合约
我们并不需要全部的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);
}
}
// 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 合约
确定传入 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的官网
对于本地链的信息
我们需要将我们的合约部署在链上,这一个状态跟我们平常设置变量是不一样的。
我们需要使用到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
});
}
所有的信息都已经知道,我们需要 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的权限问题
跟我们部署合约时所需要的变量一样
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();
测试 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 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();
}
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);
}
测试当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,其他参数可以是任何值
}
modifier depositedCollateralAndMintedDsc() {
vm.startPrank(user);
ERC20Mock(weth).approve(address(dsce), amountCollateral);
dsce.depositCollateralAndMintDsc(weth, amountCollateral, amountToMint);
vm.stopPrank();
_;
}
// 测试当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 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);
}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!