Solidity: abi.encode() vs abi.encodePacked() vs abi.encodeWithSignature() vs abi.encodeCall()

Solidity: abi.encode() vs abi.encodePacked() vs abi.encodeWithSignature() vs abi.encodeCall()

There are some encode/decode functions in Solidity, for instance:

  • abi.encode() will concatenate all values and add padding to fit into 32 bytes for each values.
    • To integrate with other contracts, you should use abi.encode().
  • abi.encodePacked() will concatenate all values in the exact byte representations without padding.
    • If you only need to store it, you should use abi.encodePacked() since it's smaller.
  • abi.encodeWithSignature() is mainly used to call functions in another contract.
  • abi.encodeCall() is the type-safe version of abi.encodeWithSignature(), required 0.8.11+.
pragma solidity >=0.8.19;

import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import "forge-std/Test.sol";

contract MyTest is Test {
    function test_abi_encode() public {
        bytes memory result = abi.encode(uint8(1), uint16(2), uint24(3));
        console.logBytes(result);
        // 0x000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003
        // total 32 bytes * 3 = 96 bytes
    }

    function test_abi_encodePacked() public {
        bytes memory resultPacked = abi.encodePacked(uint8(1), uint16(2), uint24(3));
        console.logBytes(resultPacked);
        // 0x010002000003
        // total 1 byte + 2 bytes + 3 bytes = 6 bytes
    }

    function test_abi_encodeWithSignature() public {
        address weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
        address vitalik = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045;
        bytes memory data = abi.encodeWithSignature("balanceOf(address)", vitalik);
        console.logBytes(data);
        (bool success, bytes memory result) = weth.call(data);
        console.logBool(success);
        console.logUint(abi.decode(result, (uint256)));
    }

    function test_abi_encodeCall() public {
        address weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
        address vitalik = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045;
        bytes memory data = abi.encodeCall(IERC20.balanceOf, (vitalik));
        console.logBytes(data);
        (bool success, bytes memory result) = weth.call(data);
        console.logBool(success);
        console.logUint(abi.decode(result, (uint256)));
    }
}
forge test --mc "MyTest" -vv --fork-url https://rpc.flashbots.net

ref:
https://github.com/AmazingAng/WTF-Solidity/tree/main/27_ABIEncode
https://trustchain.medium.com/abi-functions-explained-in-solidity-bd93cf88bdf2

Solidity: Read Contract Storage by Slots with Foundry

Solidity: Read Contract Storage by Slots with Foundry

State variables are stored in different "slots" in a contract, and each slot is 32 bytes (256 bits). However, multiple adjacent state variables declared in the contract may be packed into the same slot by the Solidity compiler if their combined size does not exceed 32 bytes.

When it comes to mappings and dynamic arrays, things get complicated. As an example, consider the following contract:

contract MyContract {
    struct Market {
        uint256 marketId; // slot: key's slot + 0
        bytes32 priceFeedId; // slot: key's slot + 1
    }

    address public owner; // slot 0
    uint256 public counter; // slot 1
    mapping(uint256 => Market) public marketMap; // slot 2

    constructor() {
        owner = msg.sender;
    }

    function createMarket(uint256 marketId, bytes32 priceFeedId) external {
        marketMap[marketId] = Market(marketId, priceFeedId);
    }

    function getMarket(uint256 marketId) external view returns (Market memory) {
        return marketMap[marketId];
    }
}

The elements (values) of a mapping are stored in different storage slots that are computed using keccak256() with the key and the slot number of that mapping. Two arbitrary keys' slots are very unlikely to be adjacent since the slot number is derived from a hash function. Nevertheless, the slots of members within a struct are laid out sequentially, marketId and priceFeedId in the above case, so they will be adjacent.

You could use vm.load() or stdstore.read() to read values by slots directly in Foundry. Though the later method requires the state variable to be public.

import "forge-std/Test.sol";

contract MyTest is Test {
    using stdStorage for StdStorage;

    MyContract myContract;
    uint256 ownerSlot = 0;
    uint256 mappingSlot = 2;

    MyContract.Market market;
    uint256 key = 123;

    function setUp() public {
        myContract = new MyContract();
        myContract.createMarket(123, 0x4567890000000000000000000000000000000000000000000000000000000000);

        market = myContract.getMarket(key);
    }

    function test_VmLoad() public {
        // slot 0
        console.log(ownerSlot);
        bytes32 ownerRaw = vm.load(address(myContract), bytes32(ownerSlot));
        address owner = address(uint160(uint256(ownerRaw)));
        console.logAddress(owner);
        assertEq(myContract.owner(), owner);

        uint256 keySlot = uint256(keccak256(abi.encode(key, mappingSlot)));

        // slot 88533158270886526242754899650855253077067544535846224911147251622586185207028
        uint256 marketIdSlotOffset = 0;
        bytes32 marketIdSlot = bytes32(keySlot + marketIdSlotOffset);
        console.log(uint256(marketIdSlot));
        uint256 marketId = uint256(vm.load(address(myContract), marketIdSlot));
        console.log(marketId);
        assertEq(market.marketId, marketId);

        // slot 88533158270886526242754899650855253077067544535846224911147251622586185207029
        uint256 priceFeedIdSlotOffset = 1;
        bytes32 priceFeedIdSlot = bytes32(keySlot + priceFeedIdSlotOffset);
        console.log(uint256(priceFeedIdSlot));
        bytes32 priceFeedId = vm.load(address(myContract), priceFeedIdSlot);
        console.logBytes32(priceFeedId);
        assertEq(market.priceFeedId, priceFeedId);
    }

    function test_StdStore() public {
        // slot 0
        console.log(ownerSlot);
        address owner = stdstore.target(address(myContract)).sig("owner()").read_address();
        console.logAddress(owner);
        assertEq(myContract.owner(), owner);

        // slot 88533158270886526242754899650855253077067544535846224911147251622586185207028
        console.log(stdstore.target(address(myContract)).sig("marketMap(uint256)").with_key(key).depth(0).find());
        uint256 marketId = stdstore
            .target(address(myContract))
            .sig("marketMap(uint256)")
            .with_key(key)
            .depth(0)
            .read_uint();
        console.log(marketId);
        assertEq(market.marketId, marketId);

        // slot 88533158270886526242754899650855253077067544535846224911147251622586185207029
        console.log(stdstore.target(address(myContract)).sig("marketMap(uint256)").with_key(key).depth(1).find());
        bytes32 priceFeedId = stdstore
            .target(address(myContract))
            .sig("marketMap(uint256)")
            .with_key(key)
            .depth(1)
            .read_bytes32();
        console.logBytes32(priceFeedId);
        assertEq(market.priceFeedId, priceFeedId);
    }
}

// run: forge test --mc MyTest -vv

ref:
https://docs.soliditylang.org/en/v0.8.19/internals/layout_in_storage.html
https://book.getfoundry.sh/cheatcodes/load
https://book.getfoundry.sh/reference/forge-std/std-storage

You could also find the slot numbers of each top-level state variables in OpenZepplin Upgrades Plugin's manifest file (the {network}.json) if your contracts are upgradeable.

In addition to that, Foundry's cast as well as provides a command to read storage at a certain slot:

// read the slot 2 (decimals) of WETH on Ethereum
// WETH slot 0: name
// WETH slot 1: symbol
// WETH slot 2: decimals
cast storage 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 2 --rpc-url https://rpc.flashbots.net/

ref:
https://book.getfoundry.sh/reference/cast/cast-storage
https://etherscan.io/address/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2#code