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
  • Test Contracts
  • Assertions
  • Running Tests
  • Implementation
  • Cheatcodes
  1. Foundry

Testing

Forge is a command-line tool that ships with Foundry. Forge tests, builds, and deploys your smart contracts.

PreviousImporting ContractsNextstdError.arithmeticError

Last updated 3 years ago

Test Contracts

  • Testing in Solidity is different from other frameworks like brownie.

  • Tests are written in in Solidity -> If the test function reverts, the test fails, otherwise it passes.

    • Essentially we are writing a separate contract containing test functions.

    • Test contracts end with .t.sol

    • Usually, tests will be placed in src/test (or ./test in windows)

Test contracts are deployed to 0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84.

  • If you deploy a contract within your test, then 0xb4c...7e84 will be its deployer.

  • If the contract deployed within a test gives special permissions to its deployer, such as Ownable.sol's onlyOwner modifier, then the test contract 0xb4c...7e84 will have those permissions.

Assertions

Assertion references:

Running Tests

  • Forge can run your tests with the command.

  • Forge will look for the tests anywhere in your source directory.

  • Any contract with a function that starts with test is considered to be a test.

Running a specific test function

  • forge test --match-contract ComplicatedContractTest --match-test testDeposit

  • run the tests in the test contract ComplicatedContractTest with testDeposit in the function name.

Inverse versions of these flags also exist:

  • --no-match-contract

  • --no-match-test

Implementation

pragma solidity ^0.8.13;

import "ds-test/test.sol";
import 'src/SimpleNameRegister.sol';

contract SimpleNameRegisterTest is DSTest {
    //declare state var.
    SimpleNameRegister simpleNameRegister;
    address owner;
    string test;

    function setUp() public {
        simpleNameRegister = new SimpleNameRegister();
        owner = 0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84; //test contract deployer   
        test = "what";        
    }
    function testRegisterDepends() public {
        simpleNameRegister.registerName(test);
        bool success = (owner == simpleNameRegister.nameOwner(test));
        assertTrue(success);
    }

    function testRelinquishDepends() public {
        simpleNameRegister.relinquishName(test);
        bool success = (simpleNameRegister.nameOwner(test) == address(0));
        assertTrue(success);
    }
  • import the contract to be tested (import 'src/SimpleNameRegister.sol';)

  • deploy it via factory pattern

function setUp()

The setup function is invoked before each test case is run:

  • serves to setup the necessary variables and conditions for your test functions to operate.

  • here we use it to deploy a fresh instance of SimpleNameRegister.sol before each test case is ran via simpleNameRegister = new SimpleNameRegister()

Each test is run as independent cases -> changes made in a prior test function will not spill out of scope into a following test function

To show that there is no spillover:

  • create storage string variable test and assign it "what"

  • The first function testRegisterDepends() registers ownership of name "what".

  • Then we call testRelinquishDepends() to check if we can relinquish ownership.

testRelinquishDepends() fails, indicating that the effects from testRegisterDepends() do not spill into testRelinquishDepends().

Since setUp() is invoked before each test case -> each test function interacts with a seperate instance of SimpleNameRegister.sol

Cheatcodes

  • manipulate the state of the blockchain -> change block number

  • change your identity

  • test for specific reverts and events.

Cheatcodes are functions on contract at address: 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D

To execute a cheatcode function, we need to interface with this contract from our test contract.

Therefore in our test contract we need to do 3 things:

  1. import interface Cheatcodes{} (or just copypasta the interface above your contract like below)

  2. declare interface object as state variable

  3. assignment of state variable: pass the cheatcode contract address into the interface object

If you are using ds-test, then this address is assigned in a constant named HEVM_ADDRESS. No need to remember the full address.

Implementing Cheatcode: Prank

  • prank: changes the next call's msg.sender to the input address.

  • This allows us to modify msg.sender in between function calls.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "ds-test/test.sol";
import 'src/SimpleNameRegister.sol';

interface CheatCodes {
    function prank(address) external;
    function expectRevert(bytes calldata) external;
    function startPrank(address) external;
    function stopPrank() external;
}

contract SimpleNameRegisterTest is DSTest {
    
    // declare state var.
    SimpleNameRegister simpleNameRegister;
    CheatCodes cheats;
    address adversary;

    function setUp() public {
        simpleNameRegister = new SimpleNameRegister();
        cheats = CheatCodes(HEVM_ADDRESS);
        adversary = 0xE6A2e85916802210147e366D4431f5ca4dD51a78;
    }

    // user can register an available name
    function testRegisterName(string memory _testString) public {
        simpleNameRegister.registerName(_testString);
        bool success = (address(this) == simpleNameRegister.nameOwner(_testString));
        assertTrue(success);
    }
    
    // user can register an available name and relinquish it
    function testRelinquishName(string memory _testString) public {
        simpleNameRegister.registerName(_testString);   
        simpleNameRegister.relinquishName(_testString);
        bool success = (simpleNameRegister.nameOwner(_testString) == address(0));
        assertTrue(success);
    }

    // user cannot relinquish a name that does not belong to them
    function testRelinquishAsNotOwner(string memory _testString) public {
        simpleNameRegister.registerName(_testString);   
        cheats.startPrank(adversary);
        cheats.expectRevert(bytes("The provided name does not belong to you!"));
        simpleNameRegister.relinquishName(_testString);        
        cheats.stopPrank();
    }
    
    // user cannot register a name that already has an owner
    function testRegisterUnavailableName(string memory _testString) public {
        simpleNameRegister.registerName(_testString);   
        cheats.startPrank(adversary);
        cheats.expectRevert(bytes("The provided name has already been registered!"));
        simpleNameRegister.registerName(_testString);   
        cheats.stopPrank();
    }
}

startPrank

stopPrank

expectRevert

We can do so using expectRevert(bytes calldata); it will expect a revert to occur on the next call.

  • insert it before making a function call that is expected to fail/revert

  • if the next call does not revert, then expectRevert will.

  • After calling expectRevert, calls to other cheatcodes before the reverting call are ignored.

For require statements, provide the error string as defined in the require:

cheats.expectRevert(bytes("The provided name does not belong to you!"));

If we had used prank instead of start/stop prank, the test function would not have worked as prank only modifies the next function call.

Therefore it would not have worked in this scenario as the next call is cheats.expectRevert() before simpleNameRegister.registerName(_testString)

Reference:

Sets msg.sender for all subsequent calls until is called.

Stops , resetting msg.sender and tx.origin to the values before startPrank was called.

https://book.getfoundry.sh/reference/ds-test.html
https://book.getfoundry.sh/reference/forge-std/index.html
forge test
https://book.getfoundry.sh/cheatcodes/
stopPrank
startPrank
factory pattern
startPrank
stopPrank