Solidity Testing – Extracting Input Data when Writing Forge Tests and POCs

foundrysoliditytestingunittesting

I would like to write a test in Foundry but I need to be able to first read data from a previous transaction that was submitted in some point in the past.

For example, the transaction I'm interested in was to some contract, the function called in that transaction was to deposit(address token, uint256 amount).

Is there a way for me to test:

  1. submit a transaction that will be tracked
  2. get that transaction's receipt/hash
  3. I know the transaction function signature (i.e. deposit(address,uint256)), look at what arguments the transaction was sent with.
  4. Arguments are what I expected, e.g. AssertEq(arg1, expectedAddress), AssertEq(arg2, expectedUint)

This is for a POC I'm trying to write while learning Foundry and writing tests.

Best Answer

Assuming that you are referring to a transaction that was included in previous blocks for a particular chain, you can get the transaction receipt using cast and decode the transaction data within the test itself (refer here). This coupled with foundry vm.ffi will give you the test you need. For Eg : Here I have shown how to decode a simple ERC20 transfer transaction using cast within a forge test and then assert the decoded output.

// SPDX-License-Identifier: CAL
pragma solidity =0.8.19; 
import {Test, console2} from "forge-std/Test.sol";
import "openzeppelin-contracts/contracts/utils/Strings.sol";

contract TransactionTest is Test {  
    // Decode transaction calldata with function selector
    function extractCalldata(bytes memory calldataWithSelector) internal pure returns (bytes memory) {
        bytes memory calldataWithoutSelector;
        require(calldataWithSelector.length >= 4);
        assembly {
            let totalLength := mload(calldataWithSelector)
            let targetLength := sub(totalLength, 4)
            calldataWithoutSelector := mload(0x40)
            
            // Set the length of callDataWithoutSelector (initial length - 4)
            mstore(calldataWithoutSelector, targetLength)

            // Mark the memory space taken for callDataWithoutSelector as allocated
            mstore(0x40, add(calldataWithoutSelector, add(0x20, targetLength)))

            // Process first 32 bytes (we only take the last 28 bytes)
            mstore(add(calldataWithoutSelector, 0x20), shl(0x20, mload(add(calldataWithSelector, 0x20))))

            // Process all other data by chunks of 32 bytes
            for { let i := 0x1C } lt(i, targetLength) { i := add(i, 0x20) } {
                mstore(add(add(calldataWithoutSelector, 0x20), i), mload(add(add(calldataWithSelector, 0x20), add(i, 0x04))))
            }
        }
        return calldataWithoutSelector;
    } 

    function testTransaction() public { 
        // Target Transaction to decode. (IERC20 transfer transaction)
        string memory txHash = "0xa88280fd1bedbb83b7e4260cda7c77e747eeacee199259b13607809cbffaeaf5";
        // Traget network rpc
        string memory rpcUrl = "https://1rpc.io/matic"; 
        // Cast command to get transaction data
        string memory castCommand = string.concat("cast tx ", txHash, " --rpc-url ", rpcUrl, " input ");

        string[] memory inputs = new string[](3);
        inputs[0] = "bash";
        inputs[1] = "-c";
        inputs[2] = castCommand;
        //Exec cast command
        bytes memory resultReceipt = vm.ffi(inputs);

        //Decode Data 
        (address to,uint256 amount) = abi.decode(extractCalldata(resultReceipt),(address,uint)); 
        assertEq(to,address(0x5A37E156191Befe6923765E20487Bad0a7057c28));
        assertEq(amount,5182246); 
    }
}