eth_multicallV1

We have been working with Geth and Nethermind on Execution Spec 484 proposal about eth_multicallV1 to add a new JSON-RPC method to Ethereum main net clients. ETH_multicallV1 was presented in in Execution Layer Meeting 175 and in RPC Standardization meeting in DevConnect Istanbul, slides here. You can also try the most recent multi-call version in live action on main net in The Interceptor browser extension, you can read more about that the extension in The Interceptor - A transaction simulator extension.

Why do we need a new JSON-RPC method?

Multi calling in the context of Ethereum refers to the capability of executing a series of interdependent calls on the Ethereum state. Each call in this sequence relies on the changes brought about by the preceding one. The essence of multi calling lies in its ability to simulate potential outcomes by considering the inclusion of specific transactions. Additionally, multi calling facilitates the extraction of pertinent data from the blockchain, allowing us to inquire about existing information on the chain. Currently there exists multiple ways to achieve this, such as:

  1. Alchemy forking

  2. Hardhat forking

  3. Flashbots eth_callBundle

  4. Multicall contracts

While each of these methods are cool, and have their merits, none of them are standardized. Also with the multi calling contract's, we cannot simulate multiple transactions, just multiple calls from the same contract.

Introducing eth_multicallV1. This new method is a standardized way to do multi calls via calling JSON-RPC directly. The method is aimed to function similar to eth_call but with significantly improved features. You can do everything eth_call can do and more.

On top of simply being able to make multiple calls, eth_multicallV1 enables:

  1. Calls are split to blocks - We can define custom block variables!

  2. State overrides - We can replace code and balance of accounts!

  3. Precompile override - We can replace precompiles with any code!

  4. Event logs & ETH Transfer logs - we can see more on what happens besides of one call result

  5. Validation mode - make the simulation stricter to be more certain what you include is also possible on-chain

In this blog post, we delve into a more detailed exploration of the five new features introduced in multi call.

1) Simulate Calls inside blocks

With the eth_call function, you can execute a single call within a user-defined block. In contrast, with eth_multicallV1, our goal is to empower you to make calls across different blocks and provide the flexibility to override block parameters for each of these calls.

Here's a simple eth_multicallV1 call that showcases on how you can define two blocks 1001 (0x3e9) and 2001 (0x7d1) and put calls inside them:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "eth_multicallV1",
  "params": [
    {
      "blockStateCalls": [
        {
          "blockOverrides": {
            "number": "0x3e9",
            "time": "0x3e9",
            "gasLimit": "0x3ec",
            "feeRecipient": "0xc200000000000000000000000000000000000000",
            "prevRandao": "0xc300000000000000000000000000000000000000000000000000000000000000",
            "baseFeePerGas": "0x3ef"
          },
          "calls": [ "eth_calls" ]
        },
        {
          "blockOverrides": {
            "number": "0x7d1",
            "time": "0x7d1",
            "gasLimit": "0x3ec",
            "feeRecipient": "0xc200000000000000000000000000000000000000",
            "prevRandao": "0xc300000000000000000000000000000000000000000000000000000000000000",
            "baseFeePerGas": "0x3ef"
          }
          "calls": [ "more eth_calls" ]
        }
      ]
    },
    "latest"
  ]
}

Additionally, you can observe that there are six parameters available for override: number, time, gasLimit, feeRecipient, prevRandao, and baseFeePerGas. You have the flexibility to set these values as per your preferences, with the exception of number and time, which must increase block after block. We do not permit time or block numbers to move backward.

A noteworthy aspect of these variables is the allowance for time and blocks to leap forward beyond the usual limits within a chain. For instance, you can define block 10 and subsequently define block 200 000, enabling you to simulate events significantly into the future without the need to define each intermediate block. If you're interested in understanding the implications for blocks 11 to 199,999, read about phantom blocks.

2) State overrides

The multi-call method allows us to override balance, code, nonce and state for each account. The state variable can be used to replace the whole state of an account and stateDiff can be used to just add to the existing state.

This account state manipulation enables us to do at least these things:

  1. Set your accounts balance and mint yourself some ETH to use in DEFI

  2. Change the state of a ERC20 smart contract to mint yourself the given token

  3. Replace existing smart contract with new code that bypasses the checks the contract would normally do

  4. "Deploy" a smart contract and call it right away, without actually needing to deploy it on the actual chain

Consider a simple smart contract that can be used to retrieve accounts balance

pragma solidity ^0.8.18;

contract BalanceGetter {
	function getBalance(address addr) view external returns (uint256) {
		return address(addr).balance;
	}
}

Here's a call that sets our balance, deploys the BalanceGetter contract by directly setting some accounts state to contain that code and then calls it to request our balance:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "eth_multicallV1",
  "params": [
    {
      "blockStateCalls": [
        {
          "stateOverrides": {
            "0xc000000000000000000000000000000000000000": {
              "balance": "0x2710"
            },
            "0xc200000000000000000000000000000000000000": {
              "code": "0x608060405234801561001057600080fd5b506004361061002b5760003560e01c8063f8b2cb4f14610030575b600080fd5b61004a600480360381019061004591906100e4565b610060565b604051610057919061012a565b60405180910390f35b60008173ffffffffffffffffffffffffffffffffffffffff16319050919050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006100b182610086565b9050919050565b6100c1816100a6565b81146100cc57600080fd5b50565b6000813590506100de816100b8565b92915050565b6000602082840312156100fa576100f9610081565b5b6000610108848285016100cf565b91505092915050565b6000819050919050565b61012481610111565b82525050565b600060208201905061013f600083018461011b565b9291505056fea2646970667358221220172c443a163d8a43e018c339d1b749c312c94b6de22835953d960985daf228c764736f6c63430008120033"
            }
          },
          "calls": [
            {
              "from": "0xc000000000000000000000000000000000000000",
              "to": "0xc200000000000000000000000000000000000000",
              "input": "0xf8b2cb4f000000000000000000000000c000000000000000000000000000000000000000"
            },
          ]
        }
      ]
    },
    "latest"
  ]
}

Which then returns the following JSON response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": [
    {
      "number": "0x11c7689",
      "hash": "0x0461546a316760d9fa9c82fe2df4ca478d8b3d7a3a02b609eed2ac663eca7804",
      "timestamp": "0x6560c64c",
      "gasLimit": "0x1c9c380",
      "gasUsed": "0x55b3",
      "feeRecipient": "0x6d2e03b7effeae98bd302a9f836d0d6ab0002766",
      "baseFeePerGas": "0x9c7b194d4",
      "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000",
      "calls": [
        {
          "returnData": "0x0000000000000000000000000000000000000000000000000000000000002710",
          "logs": [],
          "gasUsed": "0x55b3",
          "status": "0x1"
        }
      ]
    }
  ]
}

As evident from the provided response, it is noteworthy that the verbosity surpasses typical expectations for a singular eth_call. Let's delve into the intricacies of the response. Initially, we observe the return of an array of results (result: []), where each array member represents a block containing the outcomes of the executed calls. As previously explained, multi-call encapsulates calls within locks, and the response includes comprehensive information about both the blocks and the corresponding call results.

Inside a block result, you can see that the there's calls return:

"calls": [
    {
      "returnData": "0x0000000000000000000000000000000000000000000000000000000000002710",
      "logs": [],
      "gasUsed": "0x55b3",
      "status": "0x1"
    }
  ]

We observe the retrieved returnData, representing our balance in alignment with the expected result from the contract. Additionally, we include the logs in the returned data, although in this particular example, no logs are present. This inclusion of logs marks a notable enhancement over the previous eth_call method, which only returned the old returnData field. Beyond these aspects, we provide the gasUsed field, indicating the amount of gas consumed during the execution of the call. This information proves valuable for gas estimation calculations and comes at negligible cost for nodes, as gas accounting is an integral part of the process.

3) Precompile override

As precompiles are essentially accounts on the chain, they can be replaced in the same manner as any other account override. However, precompiles come with a unique characteristic—once overridden, there's no direct access to the original precompile in your call. While you could implement your own Solidity version of ecRecover, this approach tends to be gas-inefficient. To address this challenge, we introduce a multi-call setting called movePrecompileToAddress, enabling you to override a precompile and subsequently relocate it to a new account.

A practical use case illustrating the usefulness of overriding ecrecover is the Uniswap Permit2 swap. To execute such a swap, users must sign an off-chain message, which is then included on-chain to validate the user's approval.

Now, let's explore how to simulate a USDC to WBTC swap from Vitalik's account (0xd8da6bf26964af9d7eed9e03e53415d37aa96045) with an ecrecover override, without having access to Vitalik's private key.

Below you can see the contract that we will be using to replace ecrecover. The contract has couple parts

  1. overrideToAddress mapping that will contain ecrecover parameter hashes that we want to override to return an address of our choosing.

  2. A fallback method is invoked when ecrecover is called, checking for any overrides in the overrideToAddress mapping. If an override is present, the method retrieves the associated account. In the absence of an override, the method defaults to calling a contract at address 0x123456, where the actual implementation of ecrecover is relocated.

pragma solidity ^0.8.18;

contract ecRecoverOverride {
  mapping(bytes32 => address) overrideToAddress;
  fallback (bytes calldata input) external returns (bytes memory) {
    (bytes32 hash, uint8 v, bytes32 r, bytes32 s) = abi.decode(input, (bytes32, uint8, bytes32, bytes32));
    address overridedAddress = overrideToAddress[keccak256(abi.encode(hash, v, r, s))];
    if (overridedAddress == address(0x0)) {
      (bool success, bytes memory data) = address(0x0000000000000000000000000000000000123456).call{gas: 10000}(input);
      require(success, 'failed to call moved ecrecover at address 0x0000000000000000000000000000000000123456');
      return data;
    } else {
      return abi.encode(overridedAddress);
    }
  }
}

Then we will be making the following multi-call query. Interesting part about this multi-call is

  1. We are making two calls. The first one is to approve Uniswaps Permit2 contract and the second is us making the actual swap

  2. For the second transaction (the swap itself) to succeed, we have to override ecrecover residing in address 0x1. We replace this address code, move the precompile to address 0x123456 and fill the overrideToAddress mapping with appropriate override.

{
	"jsonrpc": "2.0",
	"id": 162,
	"method": "eth_multicallV1",
	"params": [
		{
			"blockStateCalls": [
				{
					"calls": [
						{
							"type": "0x2",
							"from": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
							"nonce": "0x481",
							"maxFeePerGas": "0x10e2249a2c",
							"maxPriorityFeePerGas": "0x5f5e100",
							"gas": "0x12631",
							"to": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
							"value": "0x0",
							"input": "0x095ea7b3000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
							"chainId": "0x1",
							"accessList": []
						},
						{
							"type": "0x2",
							"from": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
							"nonce": "0x482",
							"maxFeePerGas": "0x11491519cc",
							"maxPriorityFeePerGas": "0x5f5e100",
							"gas": "0x424ee",
							"to": "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad",
							"value": "0x0",
							"input": "0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000655f00d400000000000000000000000000000000000000000000000000000000000000030a080c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000160000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000ffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000658686d800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad00000000000000000000000000000000000000000000000000000000655f00e000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000041a6b086e6ffec7e22a7cac3d71494f1c7ec44a85c66156aff9fe881bf1fb99bc053dc332293ea7dce14be4cb689d9b75e920b37deab9ed761325999e0b48a66bf1c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000042c1d800000000000000000000000000000000000000000000000000072b3980a9ab9fe00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000072b3980a9ab9fe",
							"chainId": "0x1",
							"accessList": []
						}
					],
					"stateOverrides": {
						"0x0000000000000000000000000000000000000001": {
							"state": {
								"0x010d8fdb5b1199f6ac26d39281e100201200fbc7de5bcb9710c3dfeb475c65f6": "0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045"
							},
							"code": "0x608060405234801561001057600080fd5b506000366060600080600080868681019061002b9190610238565b935093509350935060008060008686868660405160200161004f94939291906102bd565b60405160208183030381529060405280519060200120815260200190815260200160002060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1603610191576000806212345673ffffffffffffffffffffffffffffffffffffffff166127108b8b6040516100fa929190610341565b60006040518083038160008787f1925050503d8060008114610138576040519150601f19603f3d011682016040523d82523d6000602084013e61013d565b606091505b509150915081610182576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161017990610403565b60405180910390fd5b809750505050505050506101b9565b806040516020016101a29190610464565b604051602081830303815290604052955050505050505b915050805190602001f35b600080fd5b6000819050919050565b6101dc816101c9565b81146101e757600080fd5b50565b6000813590506101f9816101d3565b92915050565b600060ff82169050919050565b610215816101ff565b811461022057600080fd5b50565b6000813590506102328161020c565b92915050565b60008060008060808587031215610252576102516101c4565b5b6000610260878288016101ea565b945050602061027187828801610223565b9350506040610282878288016101ea565b9250506060610293878288016101ea565b91505092959194509250565b6102a8816101c9565b82525050565b6102b7816101ff565b82525050565b60006080820190506102d2600083018761029f565b6102df60208301866102ae565b6102ec604083018561029f565b6102f9606083018461029f565b95945050505050565b600081905092915050565b82818337600083830152505050565b60006103288385610302565b935061033583858461030d565b82840190509392505050565b600061034e82848661031c565b91508190509392505050565b600082825260208201905092915050565b7f6661696c656420746f2063616c6c206d6f7665642065637265636f766572206160008201527f742061646472657373203078303030303030303030303030303030303030303060208201527f3030303030303030303030303030313233343536000000000000000000000000604082015250565b60006103ed60548361035a565b91506103f88261036b565b606082019050919050565b6000602082019050818103600083015261041c816103e0565b9050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061044e82610423565b9050919050565b61045e81610443565b82525050565b60006020820190506104796000830184610455565b9291505056fea26469706673582212207ddee236692b0fb014c4a668a714cba393524150b3782202194780d8b923261464736f6c63430008120033",
							"movePrecompileToAddress": "0x0000000000000000000000000000000000123456"
						}
					},
					"blockOverride": {
						"number": "0x11c507e",
						"prevRandao": "0x1",
						"time": "0x655ef9fb",
						"gasLimit": "0x1c9c380",
						"feeRecipient": "0x88c6c46ebf353a52bdbab708c23d0c81daa8134a",
						"baseFee": "0x68b59f4cb"
					}
				}
			],
			"traceTransfers": true,
			"validation": false
		},
		"0x11c507d"
	]
}

4) Event logs & ETH transfer logs

The eth_call RPC method typically returns only the output corresponding to the return value of the initial function invoked. However, in most cases, contracts interact with users through events distributed at various points during contract execution. In the case of eth_multicallV1, we have incorporated logs into the output to enhance comprehension of the simulation process.

In addition to these logs, we have introduced ERC20-like logs for Ethereum transfers. The logging of ETH movements can be toggled on or off using the traceTransfers flag within the function call.

To illustrate, consider the following example where we override the governance time lock contract of Dope Wars. This allows us to simulate the outcome if a specific governance vote were to succeed. Such simulations can already be conducted using The Interceptor tool.

{
  "jsonrpc": "2.0",
  "id": 204,
  "method": "eth_multicallV1",
  "params": [
    {
      "blockStateCalls": [
        {
          "calls": [
            {
              "type": "0x2",
              "from": "0xdbd38f7e739709fe5bfae6cc8ef67c3820830e0c",
              "nonce": "0x0",
              "maxFeePerGas": "0x0",
              "maxPriorityFeePerGas": "0x0",
              "to": "0xb57ab8767cae33be61ff15167134861865f7d22c",
              "value": "0x0",
              "input": "execute timelock",
              "chainId": "0x1",
              "accessList": []
            }
          ],
          "stateOverrides": {
            "0xb57ab8767cae33be61ff15167134861865f7d22c": {
              "stateDiff": {},
              "code": "Timelock contract replacement bytecode"
            }
          },
        }
      ],
      "traceTransfers": true,
      "validation": false
    },
    "0x11b1f64"
  ]
}

And here's the full output of the eth_multicallV1:

{
  "jsonrpc": "2.0",
  "id": 204,
  "result": [
    {
      "number": "0x11b1f65",
      "hash": "0x673fb12c793b9b118d6effdd74e9491a04e1666551f19bdb49fa95b9e134acaf",
      "timestamp": "0x65509098",
      "gasLimit": "0x1c9c380",
      "gasUsed": "0xbe97",
      "feeRecipient": "0x4838b106fce9647bdf1e7877bf73ce8b0bad5f97",
      "baseFeePerGas": "0x429978e78",
      "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000",
      "calls": [
        {
          "returnData": "0x",
          "logs": [
            {
              "address": "0x0000000000000000000000000000000000000000",
              "topics": [
                "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
                "0x000000000000000000000000b57ab8767cae33be61ff15167134861865f7d22c",
                "0x000000000000000000000000ced10840f87a2320fdca1dbe17d4f8e4211840a8"
              ],
              "data": "0x0000000000000000000000000000000000000000000000000f43fc2c04ee0000",
              "blockNumber": "0x11b1f65",
              "transactionHash": "0xdc7f600bef3a06b0864572f85634a4ffa00b8c4318949168727d89b4560b24b0",
              "transactionIndex": "0x0",
              "blockHash": "0x673fb12c793b9b118d6effdd74e9491a04e1666551f19bdb49fa95b9e134acaf",
              "logIndex": "0x0",
              "removed": false
            },
            {
              "address": "0xb57ab8767cae33be61ff15167134861865f7d22c",
              "topics": [
                "0xa560e3198060a2f10670c1ec5b403077ea6ae93ca8de1c32b451dc1a943cd6e7",
                "0x3e6eeeeced3a3b85bb1f37bb260f823dca5e1013558c4d93984762be0154ff21",
                "0x000000000000000000000000ced10840f87a2320fdca1dbe17d4f8e4211840a8"
              ],
              "data": "0x0000000000000000000000000000000000000000000000000f43fc2c04ee0000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
              "blockNumber": "0x11b1f65",
              "transactionHash": "0xdc7f600bef3a06b0864572f85634a4ffa00b8c4318949168727d89b4560b24b0",
              "transactionIndex": "0x0",
              "blockHash": "0x673fb12c793b9b118d6effdd74e9491a04e1666551f19bdb49fa95b9e134acaf",
              "logIndex": "0x1",
              "removed": false
            }
          ],
          "gasUsed": "0xbe97",
          "status": "0x1"
        }
      ]
    }
  ]
}

The execution generated two distinct logs. The initial log captures an Ethereum (ETH) transfer event, discernible by its origin address, 0x0, conforming to the ERC20 standard where the first topic denotes the transfer signature, the second pertains to the from field, and the third to the to field. The data field corresponds to the transferred ETH amount, which, in this instance, is 1.1 ETH. The second log documents our interaction with the time lock contract.

5) Validation mode

Notice the presence of the validation flag in our function calls. When activated, this flag prompts the client to rigorously enforce execution proximity to the ongoing checks. In the context of Geth, enabling validation mode triggers the following checks:

  1. Verification of the correctness of the nonce, ensuring it falls within acceptable limits—neither too high nor too low.

  2. Prevention of nonce overflow.

  3. Calculation of gas prices.

While our execution closely mirrors the blockchain process, we've made a deliberate choice to consistently disable the sender is not EOA check. This decision stems from the considerable utility in allowing calls from contracts.

If you are interested in contributing to eth_multicall development please leave your feedback to the pull request, or by contacting me on twitter @qhuesten for coordination.

Subscribe to Killari - Dark.florist
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.