Focus on testing the Vault, ignore the ERC20 mechanisms like testing transferring without approval and such
I.e. cannotWithdraw -> nothing to withdraw. no need to test, handled by token contract
// SPDX-License-Identifier: UNLICENSEDpragmasolidity ^0.8.0;import"lib/forge-std/src/Test.sol";import"lib/forge-std/src/console2.sol";import"src/Vault.sol";import"test/Dai.sol";import"test/WETH.sol";import"test/MockV3Aggregator.sol";abstractcontractStateZeroisTest { Dai public dai; WETH9 public weth; MockV3Aggregator public priceFeed; Vault public vault;address user;address deployer;eventDeposit(addressindexed collateralAsset, addressindexed user, uint collateralAmount); eventBorrow(addressindexed debtAsset, addressindexed user, uint debtAmount); eventRepay(addressindexed debtAsset, addressindexed user, uint debtAmount);eventWithdraw(addressindexed collateralAsset, addressindexed user, uint collateralAmount); eventLiquidation(addressindexed collateralAsset, addressindexed debtAsset, addressindexed user, uint debtToCover, uint liquidatedCollateralAmount);functionsetUp() publicvirtual {//vm.chainId(4); dai =newDai(4); vm.label(address(dai),"dai contract"); weth =newWETH9(); vm.label(address(weth),"weth contract"); priceFeed =newMockV3Aggregator(18,386372840000000); vm.label(address(priceFeed),"priceFeed contract"); deployer =0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84; vm.label(deployer,"deployer"); user =address(1); vm.label(user,"user");//mint 100 DAI for Vault vault =newVault(address(dai),address(weth),address(priceFeed),8,10); vm.label(address(vault),"vault contract"); dai.mint(address(vault),10000*10**18); }}contractStateZeroTestisStateZero {functiontestVaultHasDAI() public { console2.log("Check Vault has 100 DAI on deployment");uint vaultDAI = dai.balanceOf(address(vault));assertTrue(vaultDAI ==10000*10**18); }functiontestUserDepositsWETH () public { vm.deal(user,1ether); vm.startPrank(user); weth.deposit{value:1ether}();assertTrue(weth.balanceOf(user) ==1ether); vm.expectEmit(true,true,false,true);emitDeposit(address(weth), user,1ether); weth.approve(address(vault),1ether); vault.deposit(1ether);assertTrue(vault.deposits(user) ==1ether); vm.stopPrank; }}
setup()
setup users and token contracts and label them (vm.label).
labels are reflected in traces, very useful
pass decimals and initial price into MockV3Aggregator
mint 10000 DAI into vault
StateZeroTest()
use console2.log to give a high-level statement on the purpose of each test -> appears in traces
vm.deal(user, 1 ether) -> give 1 ether to user
user needs to change eth -> weth
how to add msg.value into function call:
weth.deposit{value:1ether}();
StateDeposited
(Vault has 10000 DAI, 1 WETH) | (user deposited 1 WETH. can borrow, withdraw)
setUp()
abstractcontractStateDepositedisStateZero {functionsetUp() publicoverridevirtual { super.setUp();// user deposits 1 WETH into Vault vm.deal(user,1ether); vm.startPrank(user); weth.deposit{value:1ether}(); weth.approve(address(vault),1ether); vault.deposit(1ether); vm.stopPrank(); }}contractStateDepositedTestisStateDeposited {// Vault has 100 DAI & 1 WETH deposited by userfunctiontestCannotWithdrawInExcess(uint wethAmount) public { vm.assume(wethAmount >1ether); vm.prank(user); vm.expectRevert(stdError.arithmeticError); vault.withdraw(wethAmount); }functiontestCannotBeLiquidatedWithoutDebt() public { vault.liquidation(user);assertTrue(vault.deposits(user) ==1ether); }functiontestCannotBorrowInExcess(uint excessDebt) public {uint maxDebt = vault.getMaxDebt(user); vm.assume(excessDebt > maxDebt); vm.prank(user); vm.expectRevert("Insufficient collateral!"); vault.borrow(excessDebt); }functiontestWithdraw(uint wethAmount) public { vm.assume(wethAmount >0); vm.assume(wethAmount <=1ether);uint userInitialDeposit = vault.deposits(user); vm.prank(user); vm.expectEmit(true,true,false,true);emitWithdraw(address(weth), user, wethAmount); vault.withdraw(wethAmount);assertTrue(vault.deposits(user) == userInitialDeposit - wethAmount);assertTrue(weth.balanceOf(user) == wethAmount); }functiontestBorrow(uint daiAmount) public {uint maxDebt = vault.getMaxDebt(user); vm.assume(daiAmount >0); vm.assume(daiAmount <= maxDebt); vm.expectEmit(true,true,false,true);emitBorrow(address(dai), user, daiAmount); vm.prank(user); vault.borrow(daiAmount); }}
testCannotWithdrawInExcess
Cannot withdraw in excess of deposited amount
testCannotBeLiquidatedWithoutDebt
Cannot be wrongly liquidated with no debt
testCannotBorrowInExcess
Cannot borrow in excess of collateral provided
testWithdraw
User can withdraw freely in absence of debt (fuzzing)
testBorrow
User can borrow against collateral provided (fuzzing)
StateBorrowed
user has borrowed half of maxDebt | actions:[borrow,repay,withdraw]
setUP()
abstractcontractStateBorrowedisStateDeposited {functionsetUp() publicoverridevirtual { super.setUp();// user borrows 1/2 of maxDebt vm.startPrank(user);uint halfDebt = vault.getMaxDebt(user)/2; vault.borrow(halfDebt); vm.stopPrank(); }}contractStateBorrowedTestisStateBorrowed {functiontestCannotBorrowExceedingMargin(uint excessDebt) public { console2.log("With existing debt, user should be unable to exceed margin limits");uint maxDebt = vault.getMaxDebt(user); vm.assume(excessDebt > maxDebt); vm.prank(user); vm.expectRevert("Insufficient collateral!"); vault.borrow(excessDebt); }functiontestCannotWithdrawExceedingMargin(uint excessWithdrawl) public { console2.log("With existing debt, user should be unable to withdraw in excess of required collateral");uint collateralRequired = vault.getCollateralRequired(vault.debts(user));uint spareDeposit = vault.deposits(user) - collateralRequired; vm.assume(excessWithdrawl > spareDeposit); vm.prank(user); vm.expectRevert("Collateral unavailable!"); vault.withdraw(excessWithdrawl); }functiontestCannotBeLiquidatedAtSamePrice() public { console2.log("Ceteris paribus, user should not be liquidated");uint userDebt = vault.debts(user);uint userDeposit = vault.deposits(user); vault.liquidation(user);assertTrue(vault.deposits(user) == userDeposit);assertTrue(vault.debts(user) == userDebt); }functiontestRepay(uint repayAmount) public { console2.log("User repays debt");uint userDebt = vault.debts(user); vm.assume(repayAmount <= userDebt); vm.assume(repayAmount >0); vm.expectEmit(true,true,false,true);emitRepay(address(dai), user, repayAmount); vm.startPrank(user); dai.approve(address(vault), repayAmount); vault.repay(repayAmount); vm.stopPrank(); }}
testCannotBorrowExceedingMargin (fuzzing)
With existing debt, user should be unable to exceed margin limits
testCannotWithdrawExceedingMargin (fuzzing)
With existing debt, user should be unable to withdraw in excess of required collateral
testCannotBeLiquidatedAtSamePrice
testRepay (fuzzing)
StateLiquidation
(setup price to exceed)
abstractcontractStateLiquidatedisStateBorrowed {functionsetUp() publicoverridevirtual { super.setUp();// user borrows 1/2 of maxDebt earlier// modify price to cause liquidation// if 1 WETH is converted to less DAI, at mkt price, Vault will be unable to recover DAI lent// DAI/WETH price must appreciate for liquidation (DAI has devalued against WETH) priceFeed.updateAnswer(386372840000000*10**5); }}contractStateLiquidatedTestisStateLiquidated {functiontestLiquidationOnPriceAppreciation() public { console2.log("DAI/WETH price appreciated significantly; user should be liquidated");uint userDebt = vault.debts(user);uint userDeposit = vault.deposits(user); vm.expectEmit(true,true,false,true);emitLiquidation(address(weth),address(dai), user, userDebt, userDeposit); vault.liquidation(user);assertTrue(vault.deposits(user) ==0);assertTrue(vault.debts(user) ==0); }functiontestOnlyOwnerCanCallLiquidate() public { vm.prank(user); vm.expectRevert("Ownable: caller is not the owner"); vault.liquidation(user); }}
update price
priceFeed.updateAnswer(386372840000000*10**5);
testLiquidationOnPriceAppreciation
DAI/WETH price appreciated significantly; user should be liquidated
testOnlyOwnerCanCallLiquidate
Only Owner of contract can call liquidate - onlyOwner modifier