Skip to content

Repository files navigation

Multicall4

release npm coverage license

Caution

This code has not been audited. Use at your own risk. No warranty is provided, express or implied. Do not deploy to production without an independent security review.

Never approve ERC-20 tokens to Multicall4 directly. See Dangers and mitigations.

A Turing-complete call batcher for atomic execution of dynamic user workflows onchain.

Why

Existing multicall primitives (e.g. multicall3, protocol-specific batchers) share the same shape: a hardcoded list of (target, calldata) pairs, executed sequentially with results returned as an array. That covers the easy case - independent reads or independent writes fanned out to save round trips, but it breaks down the moment the actions become dependent on each other.

Concrete examples where the flat-list model fails:

  • Approve exactly the amount a router quoted, then swap using that same amount, in one transaction.
  • Read a pool's reserves, compute the optimal input, and execute the swap without a second RPC round trip or a stale-quote race.
  • Deploy a contract via CREATE2 and immediately initialize it at its own predicted address in the same call.
  • Try one execution path; if it reverts, fall back to another without leaking the failure to the outer caller.

The current workarounds either split calls into multiple transactions, which loses atomicity, exposes an MEV surface, and introduces stale-data races, or require a bespoke Solidity contract per workflow. The underlying gap is expressiveness. On-chain execution primitives don't offer the atomicity guarantees dependent workflows need. The natural evolution of multicall, the one that closes the gap end-to-end, is to make the flow Turing-complete: give the program branching, arithmetic, and inline data flow between calls, all resolved on-chain at execution time, atomically.

What this is

Multicall4 is a Turing-complete call batcher for atomic execution of dynamic user workflows onchain. Under the hood it is a small bytecode interpreter: a single external entrypoint, multicall(bytes calldata program), reads program as a sequence of opcodes over a byte stack and executes it inside one transaction.

The execution model

program is a linear stream of opcode bytes and their inline operands. Each opcode reads its arguments from the top of the stack (and, for PUSH, from the program itself), performs a bounded amount of work, and pushes any results back onto the stack. Multicall4 gives the caller a scriptable environment where each call can:

  • Branch on the outcome of the previous call,
  • Feed the return data of one call into the calldata of the next,
  • Compute values inline instead of pre-baking them into calldata (arithmetic, bitwise, hashing),
  • Deploy contracts and immediately use their address (CREATE, CREATE2),
  • Decide at runtime whether to RETURN a chosen slice of the stack or REVERT with a chosen reason.

The stack is untyped: everything is bytes. Operands are big-endian and their widths are fixed per-opcode. Program indexes and stack offsets are uint32, so a program cannot exceed ~4 GiB. Transaction calldata itself hits this limit long before.

Opcode set (V1)

Byte counts in Inline / Pops / Pushes cells: the first line is the total; subsequent lines number each field with its width in bytes.

Byte Opcode Inline (bytes) Pops (bytes) Pushes (bytes) Description
0x00 PUSH 4 + length
1. length (4)
2. data (length)
0 length
1. data (length)
Copies length bytes of inline program data onto the top of the stack.
0x01 JUMPI 0 5
1. condition (1)
2. jumpIndex (4)
0 If condition != 0, sets the program counter to jumpIndex. Otherwise falls through.
0x02 RETURN 0 8
1. offset (4)
2. size (4)
0 Halts and returns size bytes of the current stack starting at offset.
0x03 REVERT 0 8
1. offset (4)
2. size (4)
0 Reverts with size bytes of the current stack starting at offset as the revert reason.
0x04 PC 0 0 4
1. current program counter (4)
Pushes the current program counter.
0x05 CALL 0 56 + length
1. calldata (length)
2. to (20)
3. value (32)
4. length (4)
5 + return-data length
1. return data (return-data length)
2. return-data length (4)
3. success flag (1)
External call to to with value wei and calldata. Downstream msg.sender is Multicall4 itself.
0x06 CREATE 0 36 + length
1. initcode (length)
2. value (32)
3. length (4)
21
1. deployed address (20, zero on failure)
2. success flag (1)
Deploys a new contract via CREATE with value wei and initcode.
0x07 CREATE2 0 68 + length
1. initcode (length)
2. value (32)
3. salt (32)
4. length (4)
21
1. deployed address (20, zero on failure)
2. success flag (1)
Deploys a new contract via CREATE2 with value wei, salt, and initcode.
0x08 DUP 0 8
1. offset (4)
2. size (4)
size
1. bytes of the resulting stack starting at offset (size)
Duplicates size bytes at offset onto the top of the stack.
0x09 SWAP 0 8
1. offset (4)
2. size (4)
0 Swaps the size bytes at offset with the top size bytes of the stack. Reverts if the two regions overlap.
0x0a POP 0 8
1. offset (4)
2. size (4)
0 Removes size bytes at offset from the stack.
0x0b BLOCKHASH 0 32
1. blockNumber (32)
32
1. blockhash(blockNumber) (32)
Pushes blockhash(blockNumber) (zero for blocks outside the last 256).
0x0c COINBASE 0 0 20
1. block.coinbase (20)
Pushes block.coinbase.
0x0d BASEFEE 0 0 32
1. block.basefee (32)
Pushes block.basefee.
0x0e PREVRANDAO 0 0 32
1. block.prevrandao (32)
Pushes block.prevrandao.
0x0f CHAINID 0 0 32
1. block.chainid (32)
Pushes block.chainid.
0x10 GASLIMIT 0 0 32
1. block.gaslimit (32)
Pushes block.gaslimit.
0x11 NUMBER 0 0 32
1. block.number (32)
Pushes block.number.
0x12 TIMESTAMP 0 0 32
1. block.timestamp (32)
Pushes block.timestamp.
0x13 GAS 0 0 32
1. gasleft() (32)
Pushes gasleft().
0x14 ADDRESS 0 0 20
1. address(this) (20)
Pushes address(this) (i.e. Multicall4).
0x15 CALLER 0 0 20
1. msg.sender (20)
Pushes msg.sender of the enclosing multicall(...) call.
0x16 CALLVALUE 0 0 32
1. msg.value (32)
Pushes msg.value of the enclosing multicall(...) call.
0x17 GASPRICE 0 0 32
1. tx.gasprice (32)
Pushes tx.gasprice.
0x18 ORIGIN 0 0 20
1. tx.origin (20)
Pushes tx.origin.
0x19 BALANCE 0 20
1. address (20)
32
1. address.balance (32)
Pushes the native balance of address.
0x1a EXTCODESIZE 0 20
1. address (20)
32
1. address.code.length (32)
Pushes the deployed code size of address.
0x1b EXTCODECOPY 0 28
1. address (20)
2. offset (4)
3. length (4)
length
1. address.code[offset : offset + length] (length)
Pushes length bytes of address's deployed code starting at offset. Out-of-range bytes read as zero (EVM semantics).
0x1c EXTCODEHASH 0 20
1. address (20)
32
1. keccak256(address.code) (32)
Pushes keccak256(address.code) (zero if account has no code, per EVM semantics).
0x1d SHA3 0 8
1. offset (4)
2. size (4)
32
1. keccak256(stack[offset : offset + size]) (32)
Computes keccak256 over size bytes of the current stack starting at offset.
0x1e ADD 0 64
1. a (32)
2. b (32)
32
1. a + b (32)
Wrapping unsigned addition.
0x1f SUB 0 64
1. a (32)
2. b (32)
32
1. a - b (32)
Wrapping unsigned subtraction.
0x20 MUL 0 64
1. a (32)
2. b (32)
32
1. a * b (32)
Wrapping unsigned multiplication.
0x21 DIV 0 64
1. a (32)
2. b (32)
32
1. a / b (32)
Unsigned division.
0x22 SDIV 0 64
1. a (32)
2. b (32)
32
1. a / b (32)
Signed division.
0x23 MOD 0 64
1. a (32)
2. b (32)
32
1. a % b (32)
Unsigned modulo.
0x24 SMOD 0 64
1. a (32)
2. b (32)
32
1. a % b (32)
Signed modulo.
0x25 EXP 0 64
1. a (32)
2. b (32)
32
1. a ** b (32)
Wrapping unsigned exponentiation.
0x26 ADDMOD 0 96
1. a (32)
2. b (32)
3. n (32)
32
1. (a + b) % n (32)
(a + b) % n with arbitrary-precision intermediate (unsigned).
0x27 MULMOD 0 96
1. a (32)
2. b (32)
3. n (32)
32
1. (a * b) % n (32)
(a * b) % n with arbitrary-precision intermediate (unsigned).
0x28 EQ 0 64
1. a (32)
2. b (32)
1
1. a == b flag (1)
Equality.
0x29 GT 0 64
1. a (32)
2. b (32)
1
1. a > b flag (1)
Unsigned greater-than.
0x2a SGT 0 64
1. a (32)
2. b (32)
1
1. a > b flag (1)
Signed greater-than.
0x2b LT 0 64
1. a (32)
2. b (32)
1
1. a < b flag (1)
Unsigned less-than.
0x2c SLT 0 64
1. a (32)
2. b (32)
1
1. a < b flag (1)
Signed less-than.
0x2d NOT 0 32
1. value (32)
32
1. ~value (32)
Bitwise negation.
0x2e AND 0 64
1. a (32)
2. b (32)
32
1. a & b (32)
Bitwise AND.
0x2f OR 0 64
1. a (32)
2. b (32)
32
1. a | b (32)
Bitwise OR.
0x30 XOR 0 64
1. a (32)
2. b (32)
32
1. a ^ b (32)
Bitwise XOR.
0x31 SHL 0 64
1. a (32)
2. b (32)
32
1. a << b (32)
Left shift.
0x32 SHR 0 64
1. a (32)
2. b (32)
32
1. a >> b (32)
Logical right shift.
0x33 SAR 0 64
1. a (32)
2. b (32)
32
1. a >> b (32)
Arithmetic right shift.

Design: monolith over plugin architecture

Multicall4 is a monolith: every opcode is implemented inline in one contract. A plugin architecture was considered, where each opcode lives in its own contract and Multicall4 acts as a dispatcher that delegatecalls into the user-supplied opcode contract per instruction. That shape would make new opcodes hot-pluggable: any Solidity version could ship a new EVM instruction (say, a future MCOPY2, TSTORE2, or a curve precompile wrapper) and users could adopt it by pointing at a freshly-deployed opcode contract without waiting for a Multicall4 redeploy.

Ultimately the monolith was chosen for gas. The plugin variant costs additional ~700 warm / ~2600 cold gas per opcode for the delegatecall alone. Programs run tens to hundreds of opcodes per invocation, so the overhead compounds into tens of thousands of gas per multicall. That is a permanent tax paid on every call, in exchange for flexibility.

Versioning strategy

Multicall4 is the family name for the underlying stack-machine architecture. Individual opcode-set revisions are versioned as Multicall4V1, Multicall4V2, and so on. When new revisions land, the current revision is not upgraded in place: a new contract file (Multicall4V2.sol) is deployed at a new deterministic address. Consumers pin whichever revision has the opcodes they need.

  • multicall(bytes) ABI stays byte-compatible across revisions. Opcode set and byte assignments are not guaranteed stable across revisions: opcodes may be added, removed, renumbered, or have their semantics changed. Consumers pin the revision they target and only migrate deliberately.
  • Each revision's opcode set and per-chain address are listed in this repo's README under the revision's git tag.
  • The npm package tracks the same numbering: the major version of @juglipaff/multicall4 matches the revision suffix (1.x.yMulticall4V1, 2.x.yMulticall4V2, …).

This trades a small amount of ecosystem bookkeeping (docs + registry updates on new releases) for permanent gas savings on every call.

Dangers and mitigations

The contract holds no persistent storage. Every invocation starts from a clean slate and the stack lives only for the duration of the call, so the blast radius of any error is bounded by the single transaction that triggers it. This does not make misuse safe though.

Never approve ERC-20 tokens to Multicall4 directly

Multicall4 is permissionless - any address can send it any program. An approve(multicall4, N) from Alice is not scoped to Alice. Any transaction from any address can craft a program that calls token.transferFrom(Alice, attacker, N) and drain her balance. This is the same pitfall as any router-style contract that shares an allowance across users, magnified because Multicall4 executes user-supplied bytecode. Do not approve tokens directly to Multicall4 on any chain.

Intended safe usage is a per-user execution proxy (WIP). The proxy holds the ERC-20 allowances instead of Multicall4, accepts programs only from its owner, validates them, and only then forwards to Multicall4.multicall(...). Under this pattern Multicall4 remains the shared execution engine but never holds an allowance from any user.

Dependencies

Runtime (Solidity):

Development / testing:

  • forge-std - Foundry standard library (Test, console, cheatcodes).
  • foundry - build, test, coverage, formatter. Install via foundryup.

Foundry pulls Solidity dependencies as git submodules under lib/.

Usage

Install

npm:

npm install @juglipaff/multicall4

ABI import

Pre-generated ABIs ship in the abi/ directory of the npm package for off-chain clients (ethers.js, viem, wagmi, web3.js, etc.):

import Multicall4V1 from "@juglipaff/multicall4/abi/Multicall4V1.json";
import IMulticall4V1 from "@juglipaff/multicall4/abi/IMulticall4V1.json";

TypeScript projects can import the JSON directly with resolveJsonModule enabled in tsconfig.json. ABI files are regenerated on every release, so no post-install compilation is required.

Program construction

Programs are built off-chain. A program is a raw bytes blob (a concatenation of opcode bytes and their inline operands) passed to multicall(bytes). The example below uses viem to batch two ERC-20 transferFrom calls into one transaction.

Warning

This example approves Multicall4V1 to spend an ERC-20 balance for illustration only. Do not do this on any live chain. Approving tokens to Multicall4V1 directly is unsafe. See Dangers and mitigations.

import {
  createWalletClient,
  encodeFunctionData,
  encodePacked,
  erc20Abi,
  http,
  parseAbi,
  type Address,
  type Hex,
} from "viem";
import { mainnet } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import Multicall4V1Abi from "@juglipaff/multicall4/abi/Multicall4V1.json";

// Opcode byte values. Order matches the Opcode enum in IMulticall4V1.sol.
const PUSH = 0x00;
const CALL = 0x05;

// Encodes: PUSH <calldata>, PUSH <to>, PUSH <value>, PUSH <length>, CALL.
// CALL pop order (bottom-to-top of stack): calldata, to, value, length.
// PUSH header layout: opcode (1 byte) + length (uint32, 4 bytes) + data (`length` bytes).
function encodeCall(to: Address, value: bigint, data: Hex): Hex {
  const dataLen = (data.length - 2) / 2; // strip 0x, 2 hex chars per byte
  return encodePacked(
    ["uint8", "uint32", "bytes",     // PUSH calldata
     "uint8", "uint32", "address",   // PUSH to (20 bytes)
     "uint8", "uint32", "uint256",   // PUSH value (32 bytes)
     "uint8", "uint32", "uint32",    // PUSH length (4 bytes)
     "uint8"],                       // CALL
    [PUSH, dataLen, data,
     PUSH, 20,      to,
     PUSH, 32,      value,
     PUSH, 4,       dataLen,
     CALL],
  );
}

const MULTICALL4_V1: Address = "0x0000000000000000000000000000000000000000"; // fill in per-chain
const TOKEN:         Address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; // e.g. USDC
const ALICE:         Address = "0x1111111111111111111111111111111111111111";
const BOB:           Address = "0x2222222222222222222222222222222222222222";

const account = privateKeyToAccount(process.env.PRIVATE_KEY as Hex);
const client = createWalletClient({ account, chain: mainnet, transport: http() });

const transferA = encodeFunctionData({
  abi: erc20Abi,
  functionName: "transferFrom",
  args: [account.address, ALICE, 1_000_000n],
});
const transferB = encodeFunctionData({
  abi: erc20Abi,
  functionName: "transferFrom",
  args: [account.address, BOB,   2_000_000n],
});

const program = encodePacked(
  ["bytes", "bytes"],
  [encodeCall(TOKEN, 0n, transferA), encodeCall(TOKEN, 0n, transferB)],
);

await client.writeContract({
  address: MULTICALL4_V1,
  abi: Multicall4V1Abi,
  functionName: "multicall",
  args: [program],
});

The same shape works from ethers.js (ethers.solidityPacked) or web3.js (web3.eth.abi.encodePacked). Only the packing helper changes.

Development

This repo uses Foundry for development and testing and git submodules for dependency management.

git clone https://github.com/Juglipaff/multicall4.git
cd multicall4
forge install

### Make changes

forge test # Test and regenerate gas snapshots
forge coverage # Collect coverage - CI fails if < 100% coverage
scripts/extract-abi.sh src abi # Regenerate abis

Deployment

Multicall4 is designed for deterministic cross-chain deployment via the Arachnid CREATE2 factory at 0x4e59b44847b379578588920cA78FbF26c0B4956C.

forge script scripts/Deploy.s.sol --rpc-url $RPC --broadcast

The salt lives in scripts/Deploy.s.sol. Placeholder is bytes32(0). Replace with a vanity salt before mainnet deployment if wanted.

About

A Turing-complete call batcher for atomic execution of dynamic user workflows on-chain.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages