Solidity: call() vs delegatecall()

Solidity: call() vs delegatecall()

tl;dr: delegatecall runs in the context of the caller contract.

The difference between call and delegatecall in Solidity relates to the execution context:

  • target.call(funcData):
    • the function reads/modifies target contract's storage
    • msg.sender is the caller contract
  • target.delegatecall(funcData)
    • the function reads/modifies caller contract's storage
    • msg.sender is the original sender == caller contract's msg.sender

![[Attachments/call.webp]]

![[Attachments/delegatecall.webp]]

// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.24;

import "forge-std/Test.sol";

contract Target {
    address public owner;
    uint256 public value;

    function setOwnerAndValue(uint256 valueArg) public {
        owner = msg.sender;
        value = valueArg;
    }
}

contract Caller {
    address public owner;
    uint256 public value;

    function callSetOwnerAndValue(address target, uint256 valueArg) public {
        (bool success, ) = target.call(abi.encodeWithSignature("setOwnerAndValue(uint256)", valueArg));
        require(success, "call failed");
    }

    function delegatecallSetOwnerAndValue(address target, uint256 valueArg) public {
        (bool success, ) = target.delegatecall(abi.encodeWithSignature("setOwnerAndValue(uint256)", valueArg));
        require(success, "delegatecall failed");
    }
}

contract MyTest is Test {
    address sender = makeAddr("sender");
    Target target;
    Caller caller;

    function setUp() public {
        target = new Target();
        caller = new Caller();

        assertEq(target.owner(), address(0));
        assertEq(target.value(), 0);
        assertEq(caller.owner(), address(0));
        assertEq(caller.value(), 0);
    }

    function test_callSetOwnerAndValue() public {
        vm.prank(sender);
        caller.callSetOwnerAndValue(address(target), 100);

        // call modifies target contract's state, and target contract's msg.sender is caller contract
        assertEq(target.owner(), address(caller));
        assertEq(target.value(), 100);

        // caller contract's state didn't change
        assertEq(caller.owner(), address(0));
        assertEq(caller.value(), 0);
    }

    function test_delegatecallSetOwnerAndValue() public {
        vm.prank(sender);
        caller.delegatecallSetOwnerAndValue(address(target), 200);

        // target contract's state didn't change
        assertEq(target.owner(), address(0));
        assertEq(target.value(), 0);

        // delegatecall runs in the context of caller contract, so msg.sender is sender
        assertEq(caller.owner(), sender);
        assertEq(caller.value(), 200);
    }
}

ref:
https://medium.com/0xmantle/solidity-series-part-3-call-vs-delegatecall-8113b3c76855

Solidity: Multicall - Aggregate Multiple Contract Calls

Solidity: Multicall - Aggregate Multiple Contract Calls

There are different implementations of multicall:

In the following section, we will use Multicaller as an example to illustrate the process.

The main idea of Multicaller is to aggregate multiple contract function calls into a single one. It's usually to batch contract reads from off-chain apps. However, it could also be used to batch contract writes.

Multiple Contract Reads

import { defaultAbiCoder } from "ethers/lib/utils"

class Liquidator {
    async fetchIsLiquidatableResults(
        marketId: number,
        positions: Position[],
    ) {
        const price = await this.pythService.fetchPythOraclePrice(marketId)

        const targets = new Array(positions.length).fill(this.exchange.address)
        const data = positions.map(position =>
            this.exchange.interface.encodeFunctionData("isLiquidatable", [
                marketId,
                position.account,
                price,
            ]),
        )
        const values = new Array(accountMarkets.length).fill(0)

        return await this.multicaller.callStatic.aggregate(targets, data, values)
    }

    async start() {
        const positions = await this.fetchPositions(marketId)
        const results = await this.fetchIsLiquidatableResults(marketId, positions)

        for (const [i, result] of results.entries()) {
            const isLiquidatable = defaultAbiCoder.decode(["bool"], result)[0]
            const position = positions[i]
            console.log(`${position.account} isLiquidatable: ${isLiquidatable}`)
        }
    }
}

ref:
https://github.com/Vectorized/multicaller/blob/main/API.md#aggregate

Multiple Contract Writes

It requires the target contract is compatible with Multicaller if the target contract needs to read msg.sender.

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

import { LibMulticaller } from "multicaller/LibMulticaller.sol";

contract MulticallerSenderCompatible {
    function _sender() internal view virtual returns (address) {
        return LibMulticaller.sender();
    }
}

contract Exchange is MulticallerSenderCompatible {
    function openPosition(OpenPositionParams calldata params) external returns (int256, int256) {
        address taker = _sender();
        return _openPositionFor(taker, params);
    }
}
class Bot {
    async openPosition() {
        const targets = [
            this.oracleAdapter.address,
            this.exchange.address,
        ]
        const data = [
            this.oracleAdapter.interface.encodeFunctionData("updatePrice", [priceId, priceData]),
            this.exchange.interface.encodeFunctionData("openPosition", [params]),
        ]
        const values = [
            BigNumber.from(0),
            BigNumber.from(0),
        ]

        // update oracle price first, then open position
        const tx = await this.multicaller.connect(taker).aggregateWithSender(targets, data, values)
        await tx.wait()
    }
}

ref:
https://github.com/Vectorized/multicaller/blob/main/API.md#aggregatewithsender

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

Build Docker Images using Google Cloud Build from Your GitHub Repository

Build Docker Images using Google Cloud Build from Your GitHub Repository

Triggering a Docker image build on Google Cloud Build when pushing commits to a GitHub repository.

Create Google Cloud Build Configuration File

Create a cloudbuild.yaml in the root folder:

substitutions:
  _REGION: us-west1
  _REPOSITORY: your-repo
  _BRANCH_TAG: ${BRANCH_NAME//\//-} # Replace / with - in branch names

steps:
  - id: my-app
    name: gcr.io/cloud-builders/docker
    args:
      [
        "build",
        "--cache-from",
        "${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/my-app:${_BRANCH_TAG}",
        "-t",
        "${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/my-app:${_BRANCH_TAG}",
        "-t",
        "${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/my-app:$SHORT_SHA",
        "docker/my-blog/",
      ]
    waitFor: ["-"]

# Cloud Build only pushes tags listed here
images:
  - ${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/my-app:$SHORT_SHA
  - ${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/my-app:${_BRANCH_TAG}

The above config will store your Docker images in Artifact Registry. If you want to push images to another region, simply rename us-west1-docker.pkg.dev to something like asia-east1-docker.pkg.dev.

ref:
https://cloud.google.com/build/docs/configuring-builds/create-basic-configuration
https://cloud.google.com/build/docs/building/store-artifacts-in-artifact-registry

Configure Google Cloud Build with Your GitHub Repository

Go to Google Cloud Dashboard > Google Cloud Build > Triggers > Create trigger:

  • Region: Global
  • Event: Push to a branch
  • Source: 1st Gen
  • Repository:
    • Connect new repository
    • Source code management provider: GitHub (Cloud Build GitHub App)
    • Install Google Cloud Build GitHub app
    • Only select repositories
  • Branch: ^main$
  • Type: Autodetected
  • Location: Repository

That's it, now you could push some commits to your GitHub repository.

ref:
https://console.cloud.google.com/cloud-build/triggers
https://console.cloud.google.com/cloud-build/builds

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