Skip to content

Latest commit

 

History

History
216 lines (173 loc) · 8.23 KB

File metadata and controls

216 lines (173 loc) · 8.23 KB

Cheatcode Tutorial

Cheatcodes give you more power and flexibility when writing your tests. They unlock the entire blockchain's state and transaction context for you, as well as empowering you with things like deriving an address from a private key.

Below is an example test contract. Each test function would be ran upon calling forge test. Look through each test and the associated comments to see how each might be useful to you.

import "ds-test/test.sol";

interface Vm {
    // Set block.timestamp (newTimestamp)
    function warp(uint256) external;
    // Set block.height (newHeight)
    function roll(uint256) external;
    // Loads a storage slot from an address (who, slot)
    function load(address,bytes32) external returns (bytes32);
    // Stores a value to an address' storage slot, (who, slot, value)
    function store(address,bytes32,bytes32) external;
    // Signs data, (privateKey, digest) => (v, r, s)
    function sign(uint256,bytes32) external returns (uint8,bytes32,bytes32);
    // Gets address for a given private key, (privateKey) => (address)
    function addr(uint256) external returns (address);
    // Performs a foreign function call via terminal, (stringInputs) => (result)
    function ffi(string[] calldata) external returns (bytes memory);
    // Calls another contract with a specified `msg.sender`, (newSender, contract, input) => (success, returnData)
    function prank(address, address, bytes calldata) external payable returns (bool, bytes memory);
    // Sets an address' balance, (who, newBalance)
    function deal(address, uint256) external;
    // Sets an address' code, (who, newCode)
    function etch(address, bytes calldata) external;
}

contract Bar {
    uint256 private youCantReadMe;

    function setYCRM(uint256 val) public {
        youCantReadMe = val;   
    }

    function checkSender() public returns (address) {
        return msg.sender;
    }

    receive() {
        require(false, "I cannot receive ether");
    }
}

contract BarTest is DSTest {
    // Cheatcodes live at a specific address; you can think of them as
    // precompiles in a sense. It is derived from:
    // address vm == uint160(uint256(keccak256('hevm cheat code')));

    Vm vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);

    Bar bar;

    function setUp() public {
        // This is effective a "beforeEach" block. It will run
        // prior to each test
        bar = new Bar();
    }

    // `warp` changes the block timestamp
    function testWarp() public {
        // block.timestamp is by default set to 0, unless otherwise
        // specified in environment variables
        assertEq(block.timestamp, 0);

        // sets block.timestamp
        vm.warp(100);

        assertEq(block.timestamp, 100);
    }

    // `roll` changes the block number/height
    function testRoll() public {
        // block.height is by default set to 0, unless otherwise
        // specified in environment variables
        assertEq(block.height, 0);

        // sets block.height
        vm.roll(100);

        assertEq(block.height, 100);
    }

    // `load` lets you read a storage slot from another
    // contract
    function testLoad() public {
        // We have no way to read the private `youCantReadMe` variable
        // in the `Bar` contract, in normal solidity.
        //
        // To allow us to test against private variables state changes,
        // if you know the storage slot of the variable you can call `load`

        // We provide the address we want to load storage from,
        // and the slot we want to read. in this case, we know it is in
        // slot 0

        bar.setYCRM(100);

        uint256 youCantReadMe = uint256(vm.load(address(bar), bytes32(uint256(0))));

        assertEq(youCantReadMe, 100);
    }

    // `store` lets you store a storage slot from another
    // contract
    function testStore() public {
        // This is much the same has `load` but lets us *set* arbitrary
        // storage for another contract instead of read.

        // We provide the address and slot we want to store a value to.
        //
        //         target             slot              value to store
        //       v----------v  v-----------------v  v-------------------v
        vm.store(address(bar), bytes32(uint256(0)), bytes32(uint256(100)));

        uint256 youCantReadMe = uint256(vm.load(address(bar), bytes32(uint256(0))));

        assertEq(youCantReadMe, 100);
    }

    // `sign` lets you sign a digest, like a `permit`/EIP-2612 digest, given a private key as a uint256.
    // `addr` converts a private key into an address
    function testSignAndAddr() public {
        // `addr` computes the address from a private key
        address vmAddr = vm.addr(1);
        bytes32 hash = keccak256("hi there!");

        // internally, `forge` signs the hash for you
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash);

        // we can recover the signer address using solidity's builtin `ecrecover`
        address signer = ecrecover(hash, v, r, s);

        // and our derived address from forge matches the ecrecovered address
        assertEq(vmAddr, signer);
    }

    // `ffi` lets you call a function in the terminal
    // NOTE: this functionality must be enabled in the environment variables otherwise it will fail.
    function testFFI() public {
        // we want to echo out "acab" in our terminal, so we construct the arguments
        string[] memory inputs = new string[](3);
        inputs[0] = "echo";
        inputs[1] = "-n";
        inputs[2] = "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000046163616200000000000000000000000000000000000000000000000000000000";

        // the vm calls out to the terminal and grabs the output
        bytes memory res = vm.ffi(inputs);
        (string memory output) = abi.decode(res, (string));
        assertEq(output, "acab");
    }

    // `prank` lets you change the `msg.sender` of a particular low level call.
    function testPrank() public {
        // we the msg.sender of the new call to be address(1337)
        address newSender = address(1337);
        bytes4 sig = bar.checkSender.selector;
        bytes memory calld = abi.encodePacked(sig);

        // as of now, prank is treated like a low level call
        // this may change in the future to work more like `expectRevert` (an upcoming cheatcode)
        (bool success, bytes memory ret) = vm.prank(newSender, address(bar), calld);
        assertTrue(success);

        // we can decode the return data & check the returned sender of the call
        address barSender = abi.decode(ret, (address));
        assertEq(barSender, newSender);
    }

    // `deal` lets you set the balance of an address *without* performing a transfer
    function testDeal() public {
        address b = address(bar);
        assertEq(b.balance, 0);

        // note: the `Bar` contract *cannot* be transferred Ether as it's `receive` function reverts
        // but since `deal` doesn't perform a transfer we can do this.
        vm.deal(b, 1338);
        assertEq(b.balance, 1338);
    }

    // `etch` sets the code of a particular address. This can be useful for mocking particular contracts at particular addresses
    function testEtch() public {
        address rewriteCode = address(1337);
        
        // we want the new code to be 0x1338 of address(1337)
        bytes memory newCode = hex"1338";
        vm.etch(rewriteCode, newCode);

        // call a helper function to get the code
        bytes memory nCode = getCode(rewriteCode);
        assertEq(string(newCode), string(nCode));
    }

    function getCode(address who) internal returns (bytes memory oCode) {
        assembly {
            // retrieve the size of the code, this needs assembly
            let size := extcodesize(who)
            // allocate output byte array - this could also be done without assembly
            // by using oCode = new bytes(size)
            oCode := mload(0x40)
            // new "memory end" including padding
            mstore(0x40, add(oCode, and(add(add(size, 0x20), 0x1f), not(0x1f))))
            // store length in memory
            mstore(oCode, size)
            // actually retrieve the code, this needs assembly
            extcodecopy(who, add(oCode, 0x20), 0, size)
        }
    }
}