The JSON-RPC Method `eth_simulateV1` is now available on multiple networks

The eth_simulateV1 method is live on the Ethereum mainnet following the release of Geth v1.14.9 and Nethermind v1.28.0. It is also supported on networks like Base, Optimism, and Gnosis.

What is eth_simulateV1?

The method eth_simulateV1 is an enhancement of eth_call, designed to support multiple simultaneous calls and more advanced features. Previously, multicall functionality was achieved through tools such as:

  1. Alchemy forking

  2. Hardhat forking

  3. Flashbots eth_callBundle

  4. Multicall contracts

While these methods have their strengths, they lack standardization. Additionally, multicall contracts are limited to multiple calls within the same contract and cannot simulate multiple transactions.

In contrast, eth_simulateV1 offers a standardized approach (Execution Spec 484) for multicalls via JSON-RPC. It retains the core functionality of eth_call while introducing significant improvements.

Key Features of eth_simulateV1

  1. Calls Across Blocks: Define custom block variables for granular simulations.

  2. State Overrides: Modify account balances or replace contract code during simulations.

  3. Precompile Overrides: Substitute precompiled contracts with custom code.

  4. Enhanced Logging: Access detailed event and ETH transfer logs beyond single-call results.

  5. Validation Mode Enforce stricter rules to ensure on-chain compatibility.

These features provide developers with more power and flexibility, making eth_simulateV1 a robust tool for advanced transaction simulations.

In this blog post, we’ll explore these new features in greater depth and demonstrate their potential for improving Ethereum development workflows.

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_simulateV1, 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_simulateV1 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_simulateV1",
  "params": [
    {
      "blockStateCalls": [
        {
          "blockOverrides": {
            "number": "0x3e9",
            "time": "0x3e9",
            "gasLimit": "0x3ec",
            "feeRecipient": "0xc200000000000000000000000000000000000000",
            "prevRandao": "0xc300000000000000000000000000000000000000000000000000000000000000",
            "baseFeePerGas": "0x3ef",
            "blobBaseFee": "0x15"
          },
          "calls": [ "eth_calls" ]
        },
        {
          "blockOverrides": {
            "number": "0x7d1",
            "time": "0x7d1",
            "gasLimit": "0x3ec",
            "feeRecipient": "0xc200000000000000000000000000000000000000",
            "prevRandao": "0xc300000000000000000000000000000000000000000000000000000000000000",
            "baseFeePerGas": "0x3ef",
            "blobBaseFee": "0x15"
          }
          "calls": [ "more eth_calls" ]
        }
      ]
    },
    "latest"
  ]
}

You can override seven parameters when defining blocks: number, time, gasLimit, feeRecipient, prevRandao, baseFeePerGas, and blobBaseFee. All these parameters, including number and time, can be customized. However, number must always increase, and time must either increase or remain the same.

An interesting feature of these variables is the ability to skip forward in time or blocks, beyond the usual incremental progression of the chain. For example, you can define block 10 and then skip directly to block 20. This allows you to simulate future events without specifying every intermediate block. When using the eth_simulateV1 method, all blocks between 10 and 20 will be simulated and returned.

Note that there is a maximum limit on the number of blocks that can be created in a single call. By default, this limit is 256 blocks, although it can be adjusted on a per-client basis.

Additionally, there are limits on the total gas consumption of the simulation and the size of the return payload. These limits help manage the load that a single eth_simulateV1 call can place on a node. Both the gas and payload size limits can also be customized per client.

2) State Overrides

State overrides allow you to modify the balance, code, nonce, and state of any account. The state variable can replace the entire state of an account, while stateDiff enables you to augment the existing state by applying changes incrementally.

These powerful tools for account state manipulation provide the following capabilities:

  1. Set Account Balances: Adjust any account’s balance, effectively minting ETH for use in DeFi applications.

  2. Token Minting: Modify the state of an ERC20 smart contract to mint tokens directly to your account.

  3. Smart Contract Substitution: Replace the code of an existing smart contract to bypass its typical checks or restrictions.

  4. On-the-Fly Contract Deployment: Simulate the deployment of a smart contract and interact with it immediately, without requiring actual deployment on the 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_simulateV1",
  "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": [
        {
            "baseFeePerGas": "0x0",
            "blobGasUsed": "0x0",
            "calls": [
                {
                    "returnData": "0x0000000000000000000000000000000000000000000000000000000000002710",
                    "logs": [],
                    "gasUsed": "0x55b3",
                    "status": "0x1"
                }
            ],
            "difficulty": "0x0",
            "excessBlobGas": "0x3220000",
            "extraData": "0x",
            "gasLimit": "0x1c9c380",
            "gasUsed": "0x55b3",
            "hash": "0x4342f0ab870175757176d5888e82f27523a6e1d52c0ac20a6ddf599d54ce0e04",
            "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
            "miner": "0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5",
            "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
            "nonce": "0x0000000000000000",
            "number": "0x1455a09",
            "parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
            "parentHash": "0xb30e288a5518544cc71dd24389a21061adab20f45f17c0907054dccf7bf00c01",
            "receiptsRoot": "0x4ce78e593fcb88a20d7bb31c27879f800551f4069371e576f7efad0d9615a960",
            "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
            "size": "0x29b",
            "stateRoot": "0xcfb988bd139a3f44aa79f9cbaba606429d76749fd18e1e8ca9a6612a4e0c8384",
            "timestamp": "0x674f0627",
            "totalDifficulty": "0xc70d815d562d3cfa955",
            "transactions": [
                "0x96c6b7b5b4835fd518fd914feb586452cc797d332f1bd234f72c9e692c4c427a"
            ],
            "transactionsRoot": "0xebe30ac336ac6714af65c684e278799f70965b18ae12e9b368056640ac111650",
            "uncles": [],
            "withdrawals": [],
            "withdrawalsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"
        }
    ]
}

The response is notably more detailed compared to a standard eth_call. It includes an array of results (result: []), with each element representing a block that contains the outcomes of executed calls. As previously mentioned, eth_simulateV1 organizes calls into blocks, providing comprehensive information about both the blocks and their corresponding call results. In essence, eth_simulateV1 returns the data you would receive from eth_getBlockByNumber for a simulated block. However, since these blocks only exist during the eth_simulateV1 call, this information is included directly in the response.

Inside a block result, information about the call execution is also returned:

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

The returnData, represents 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 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 eth_simulateV1 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);
    }
  }
}

Once the contract is written, we can create the eth_simulateV1 query. This involves two calls: the first is to approve Uniswap's Permit2 contract, and the second is to perform the actual swap. For the the swap to succeed, we need to override the ecrecover located at address 0x1. This requires replacing the code at that address, moving the precompile to address 0x123456, and updating the overrideToAddress mapping with the appropriate override.

{
	"jsonrpc": "2.0",
	"id": 162,
	"method": "eth_simulateV1",
	"params": [
		{
			"blockStateCalls": [
				{
					"calls": [
						{ // approve
							"type": "0x2",
							"from": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
							"nonce": "0x481",
							"maxFeePerGas": "0x10e2249a2c",
							"maxPriorityFeePerGas": "0x5f5e100",
							"gas": "0x12631",
							"to": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
							"value": "0x0",
							"input": "0x095ea7b3000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
							"chainId": "0x1",
							"accessList": []
						},
						{ // swap
							"type": "0x2",
							"from": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
							"nonce": "0x482",
							"maxFeePerGas": "0x11491519cc",
							"maxPriorityFeePerGas": "0x5f5e100",
							"gas": "0x424ee",
							"to": "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad",
							"value": "0x0",
							"input": "0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000655f00d400000000000000000000000000000000000000000000000000000000000000030a080c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000160000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000ffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000658686d800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad00000000000000000000000000000000000000000000000000000000655f00e000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000041a6b086e6ffec7e22a7cac3d71494f1c7ec44a85c66156aff9fe881bf1fb99bc053dc332293ea7dce14be4cb689d9b75e920b37deab9ed761325999e0b48a66bf1c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000042c1d800000000000000000000000000000000000000000000000000072b3980a9ab9fe00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000072b3980a9ab9fe",
							"chainId": "0x1",
							"accessList": []
						}
					],
					"stateOverrides": {
						"0x0000000000000000000000000000000000000001": { 
							"state": { // override the mapping
								"0x010d8fdb5b1199f6ac26d39281e100201200fbc7de5bcb9710c3dfeb475c65f6": "0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045"
							},
                            // replace the ecrecover code with our own code
							"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 only returns the output corresponding to the return value of the first function invoked. However, in many cases, events emitted at various stages of contract execution provide deeper insight into what transpired. To address this, eth_simulateV1 includes logs in its output, offering a more comprehensive view of the execution.

In addition to these logs, eth_simulateV1 also supports ERC20-like logs for Ethereum transfers. The logging of ETH transfers can be toggled on or off using the traceTransfers flag.

For example, by overriding the governance timelock contract of Dope Wars, we can simulate the outcome of a specific governance vote. Such simulations can already be conducted using The Interceptor tool.

{
  "jsonrpc": "2.0",
  "id": 204,
  "method": "eth_simulateV1",
  "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 call result of the eth_simulateV1:

{
  "returnData": "0x",
  "logs": [
    {
      "address": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
      "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, 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee, 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 timelock contract.

5) Validation mode

The last important feature of eth_simulateV1 is validation flag. By default, validation is off and the eth_simulateV1 behaves similar to eth_call, eg. nonce is not being checked, gas is free etc. When the flag is enabled, the client will treat the transactions as if they were actually included on the chain. However, two checks are always being skipped:

  1. You can send from a contract (sender is not EOA check is disabled)

  2. You do not need to provide correct signature fields for transactions. This enables you to spoof any account without access to their private key

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.