Smart Contract Development
  • Introduction
    • What is a Transaction
    • Accounts and Signing
    • What is a smart contract
  • Learning Solidity
    • Introduction
    • Module 1
      • Variable Types
      • Variable Scope: State & Local variables
      • Global variables
      • Functions
        • View and Pure
        • Shadowing in Fuctions
      • Mapping
      • Require
      • Events
    • Project #1: Simple Registry
    • Module 2
      • Constructor
      • Data Location: Value & Reference
      • Interface
      • Import
        • Importing in Foundry
      • Inheritance
      • ERC-20
      • Checks-effect-interaction pattern
    • Project #2: Basic Vault
    • Module 3
      • Payable
      • Receive
      • Fallback
      • Returns
    • Project #3: ERC20+ETH Wrapper
    • Module 4
      • Immutable and Constant
      • Fixed-point Math
      • Abstract contracts
      • ERC-4626
      • Modifier + Inheritance +Ownable
      • Type
    • Project #4: Fractional Wrapper
    • Module 5
      • If-else
      • Libraries
        • TransferHelper
      • Chainlink Oracle
    • Project #5: Collateralized Vault
  • Compendium
    • Solidity Basics
      • Variable Types
      • Value Types
        • address
        • enum
      • Reference Types
        • strings
        • mappings
        • struct
        • Arrays
        • Multi-Dimensional arrays
      • Global Objects
      • Functions
        • Function types
        • Constructor Function
        • Transaction vs Call
        • Require, Revert, Assert
      • Function signature + selectors
      • Payable
        • Payable + withdraw
        • msg.value & payable functions
      • Receive
      • Fallback function (sol v 0.8)
        • Fallback function (sol v 0.6)
      • call, staticcall, delegatecall
    • Return & Events
    • Control Variable Visibility
    • Local Variables (Storage v Memory)
    • Data Location and Assignment Behaviors
    • Modifiers & Inheritance & Import
      • import styles
    • Interface & Abstract Contracts
    • ABI & Debugging
    • Libraries
    • Conditional(ternary) operators
    • Smart Contract Life-cycle
      • Pausing Smart Contracts
      • Destroying Smart Contracts
    • Merkle Trie and MPT
    • Merkle Tree Airdrop
  • Try & catch
  • Ethereum Signatures
  • EVM, Storage, Opcodes
    • EVM
    • Wei, Ether, Gas
    • Storage
    • ByteCode and Opcodes
    • Transaction costs & Execution costs
  • Reading txn input data
  • Data Representation
  • Yul
    • Yul
      • Intro
      • Basic operations
      • Storage Slots
      • Storage of Arrays and Mappings
      • Memory Operations
      • Memory: how solidity uses memory
      • Memory: Return, Require, Tuples and Keccak256
      • Memory: Logs and Events
      • Inter-contract calls
      • calldata
      • free memory pointer
    • Yul Exercises
      • read state variable
      • read mapping
      • iterate Array, Return Sum
    • memory-safe
  • Upgradable Contracts
    • Upgradability & Proxies
    • UUPS Example
    • Minimal Proxy Example
    • TPP Example
    • 🚧Diamond
      • On Storage
  • Gas Opt
    • Block Limit
    • gasLimit & min cost
    • Solidity Optimiser
    • Memory v calldata
    • Memory caching vs direct storage vs pointers
    • < vs <=
    • reverting early
    • X && Y, ||
    • constant and immutable
    • caching sload into mload
    • Syntactic Sugar
    • using unchecked w/o require
    • Compact Strings
    • Calling a view function
    • Custom errors over require
    • usage of this.
      • multiple address(this)
  • ERCs & EIPs
    • ERC-20.sol
      • Core functions
      • transfer()
      • transferFrom()
      • TLDR transfer vs transferFrom
    • Landing
      • ERC721.sol
      • EIP-721
        • LooksRare
        • Page 1
      • ERC-1271
      • EIP-2981
      • ERC-165
      • EIP-1167: Minimal Proxy Contract
    • VRFConsumerBase
    • UniswapV2Library
  • Yield Mentorship 2022
    • Projects
      • #1 Simple Registry
      • #2 Basic Vault
      • #3 ERC20+ETH Wrapper
        • setFailTransferTrue
      • #4 Fractional Wrapper
      • #5 Collateralized Vault
        • Process
        • Vault.sol
        • Testing
        • Chainlink Oracles
        • Pricing + Decimal scaling
        • Refactor for Simplicity
      • #9 Flash Loan Vault
        • Implementing ERC3156
        • Full code for lender
        • Ex-rate calculation
    • State Inheritance Testing
    • Testing w/ Mocks
    • Yield Style Guide
    • Github Actions
    • TransferHelper.sol
    • math logic + internal fn
    • Interfaces: IERC20
  • Foundry
    • Overview
    • Importing Contracts
    • Testing
      • stdError.arithmeticError
      • assume vs bound
      • Traces
      • label & console2
      • std-storage
  • Smart Contract Security
    • Damn Vulnerable Defi
      • 1. Unstoppable
      • 2. Naive receiver
      • 3. Truster
      • 4. Side Entrance
      • 5. The Rewarder
      • 6. Selfie
      • 7. Compromised
      • 8. Puppet
      • 9. Puppet V2
      • 10 - Free Rider
    • Merkle Tree: shortened proof attack
  • Fixed-Point Math
    • AMM Math
  • Solidity Patterns
    • checks-effects-interactions pattern
    • Router // batch
    • claimDelegate: stack unique owners
    • claimDelegate: cache previous user
  • Array: dup/ascending check
  • Deployment
    • Behind the Scenes
    • Interacting with External Contracts
    • Logging, Events, Solidity, Bloom Filter
  • Misc
    • Mnemonic Phrases
    • Bidul Ideas
  • Archive
    • Brownie Framework
      • Brownie basics
        • storing wallets in .env
        • Deployment to ganache
        • Interacting with contract
        • Unit Testing
        • Testnet deployment
        • Interacting w/ deployed contract
        • Brownie console
      • Brownie Advanced
        • Dependencies: import contracts
        • helpful_scripts.py
        • verify and publish
        • Forking and Mocking
        • Mocking
        • Forking
      • Testing
      • Scripts Framework
        • deploy.py
        • get_accounts
        • deploy_mocks()
        • fund_with_<token>()
      • Brownie Networks
    • Brownie Projects
      • SharedWallet
        • Multiple Beneficiaries
        • Common Code Contract
        • Adding Events
        • Renounce Ownership
        • Separate Files
      • Supply Chain
        • ItemManager()
        • Adding Events
        • Adding unique address to each item
      • Lottery
      • Aave - Lending and Borrowing
        • Approve & Deposit
        • Borrow
      • NFT
      • Advanced Collectible
        • adv_deploy() + Testing
        • Create Metadata
        • Setting the TokenURI
    • node npm
    • Ganache
    • Truffle
    • Remix
    • Installing Env
Powered by GitBook
On this page
  • StateZero:
  • StateDeposited
  • StateBorrowed
  • StateLiquidation
  • (setup price to exceed)
  1. Yield Mentorship 2022
  2. Projects
  3. #5 Collateralized Vault

Testing

StateZero:

(Vault has 10000 DAI)

  • testVaultHasDAI

  • User deposits WETH

  • 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: UNLICENSED
pragma solidity ^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";


abstract contract StateZero is Test {
    Dai public dai;
    WETH9 public weth;
    MockV3Aggregator public priceFeed;

    Vault public vault;
    address user;
    address deployer;

    event Deposit(address indexed collateralAsset, address indexed user, uint collateralAmount);  
    event Borrow(address indexed debtAsset, address indexed user, uint debtAmount); 
    event Repay(address indexed debtAsset, address indexed user, uint debtAmount);
    event Withdraw(address indexed collateralAsset, address indexed user, uint collateralAmount);  
    event Liquidation(address indexed collateralAsset, address indexed debtAsset, address indexed user, uint debtToCover, uint liquidatedCollateralAmount);


    function setUp() public virtual {
        //vm.chainId(4);
        dai = new Dai(4);
        vm.label(address(dai), "dai contract");

        weth = new WETH9();
        vm.label(address(weth), "weth contract");

        priceFeed = new MockV3Aggregator(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 = new Vault(address(dai), address(weth), address(priceFeed),8,10);
        vm.label(address(vault), "vault contract");
        dai.mint(address(vault), 10000*10**18);
    }
}

contract StateZeroTest is StateZero {

    function testVaultHasDAI() public {
        console2.log("Check Vault has 100 DAI on deployment");
        uint vaultDAI = dai.balanceOf(address(vault));
        assertTrue(vaultDAI == 10000*10**18);
    }

    function testUserDepositsWETH () public {
        vm.deal(user, 1 ether);
        vm.startPrank(user);

        weth.deposit{value: 1 ether}();
        assertTrue(weth.balanceOf(user) == 1 ether); 

        vm.expectEmit(true, true, false, true);
        emit Deposit(address(weth), user, 1 ether);

        weth.approve(address(vault), 1 ether);
        vault.deposit(1 ether);
        assertTrue(vault.deposits(user) == 1 ether);

        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: 1 ether}();

StateDeposited

(Vault has 10000 DAI, 1 WETH) | (user deposited 1 WETH. can borrow, withdraw)

setUp()

abstract contract StateDeposited is StateZero {

    function setUp() public override virtual {
        super.setUp();

        // user deposits 1 WETH into Vault
        vm.deal(user, 1 ether);
        vm.startPrank(user);
        weth.deposit{value: 1 ether}();
        weth.approve(address(vault), 1 ether);
        vault.deposit(1 ether);
        vm.stopPrank();
    }
}

contract StateDepositedTest is StateDeposited {
    // Vault has 100 DAI & 1 WETH deposited by user

    function testCannotWithdrawInExcess(uint wethAmount) public {
        vm.assume(wethAmount > 1 ether);
        vm.prank(user);
        vm.expectRevert(stdError.arithmeticError);
        vault.withdraw(wethAmount);
    }

    function testCannotBeLiquidatedWithoutDebt() public {
        vault.liquidation(user);
        assertTrue(vault.deposits(user) == 1 ether);
    }

    function testCannotBorrowInExcess(uint excessDebt) public {
        uint maxDebt = vault.getMaxDebt(user);
        vm.assume(excessDebt > maxDebt);
        
        vm.prank(user);
        vm.expectRevert("Insufficient collateral!");
        vault.borrow(excessDebt);
    }

    function testWithdraw(uint wethAmount) public {
        vm.assume(wethAmount > 0);
        vm.assume(wethAmount <= 1 ether);
        
        uint userInitialDeposit = vault.deposits(user);
        vm.prank(user);

        vm.expectEmit(true, true, false, true);
        emit Withdraw(address(weth), user, wethAmount);
    
        vault.withdraw(wethAmount);

        assertTrue(vault.deposits(user) == userInitialDeposit - wethAmount);
        assertTrue(weth.balanceOf(user) == wethAmount);
    }

    function testBorrow(uint daiAmount) public {
        uint maxDebt = vault.getMaxDebt(user);
        vm.assume(daiAmount > 0);
        vm.assume(daiAmount <= maxDebt);        
        
        vm.expectEmit(true, true, false, true);
        emit Borrow(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()

abstract contract StateBorrowed is StateDeposited {
    function setUp() public override virtual {
        super.setUp();

        // user borrows 1/2 of maxDebt
        vm.startPrank(user);
        uint halfDebt = vault.getMaxDebt(user)/2;
        vault.borrow(halfDebt);
        vm.stopPrank();
    }
}

contract StateBorrowedTest is StateBorrowed {

    function testCannotBorrowExceedingMargin(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);
    }

    function testCannotWithdrawExceedingMargin(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);
    }

    function testCannotBeLiquidatedAtSamePrice() 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);
    }

    function testRepay(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);
        emit Repay(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)

abstract contract StateLiquidated is StateBorrowed {
    function setUp() public override virtual {
        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);
    }
}

contract StateLiquidatedTest is StateLiquidated {

    function testLiquidationOnPriceAppreciation() 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);
        emit Liquidation(address(weth),address(dai), user, userDebt, userDeposit);

        vault.liquidation(user);
        assertTrue(vault.deposits(user) == 0);
        assertTrue(vault.debts(user) == 0);
    }

    function testOnlyOwnerCanCallLiquidate() 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

PreviousVault.solNextChainlink Oracles

Last updated 3 years ago