UserOperation CallData

The callData field on a UserOperation encodes instructions for carrying out the user's intent during the execution phase.

A User Operation has a bytes field called callData. This data is sent to the Smart Account during the execution phase to carry out the user's intent.

TL;DR: How to encode a callData

// Get account and contract ABIs
const accountABI = ["function execute(address to, uint256 value, bytes data)"];
const contractABI = ["function transfer(address to, uint amount) returns (bool)"];

// Create contract interface
const account = new ethers.utils.Interface(accountABI);
const contract = new ethers.utils.Interface(contractABI);

// Encode UserOperation callData
const callData = account.encodeFunctionData("execute", [
  "0x...",
  ethers.constants.Zero,
  contract.encodeFunctionData("transfer", [accountAddress, amount]),
]);

Related examples


An overview of transaction data

When an EOA transaction is made in an EVM network it will usually consist of a to, value, and optional data field.

Take the simplest transaction of sending 1 ETH from account A to account B. The transaction will have the following information:

  • to: address of account B
  • value: 1 ETH
  • data: null

Now let's say account A wants to send account B 100 USDC (an ERC-20 token). All ERC-20 tokens are just smart contracts that track address balances in it's own state. So the transaction will look like this:

  • to: address of USDC contract
  • value: 0 ETH
  • data: Instructions to transfer 100 USDC from account A to account B

So now we can see that data is how we send instructions to a smart contract. The callData in an ERC-4337 User Operation is no different. It is the instructions given to the Smart Account.

Sending instructions to an ERC-4337 account

The above overview is a high level take of the data field, but what does it look like in practice? In order to start sending generic instructions to a Smart Account, you'll need to implement a function that can make an upstream CALL to a given address with both value and data.

interface ExecutableAccount {
  function execute(
    address to,
    uint256 value,
    bytes calldata data
  ) external;
}

The encoded function data that is required to run execute is what we use as the callData field in the User Operation.

Encoding callData with ethers.js

Fortunately, we have great tools at our disposal to abstract the encoding process using ethers.js. The only thing we require is a reference to the contract's Application Binary Interface (ABI). This gives ethers.js the information needed for encoding and decoding.

Using the account interface above and an ERC-20 token, a human-readable ABI would look like this:

const accountABI = ["function execute(address to, uint256 value, bytes data)"];

// An ABI can be fragments and does not have to include the entire interface.
// Aslong as it includes the parts we want to use.
const partialERC20TokenABI = [
  "function transfer(address to, uint amount) returns (bool)",
];

With these two ABIs we can encode a callData for our UserOperation that sends an amount of USDC to another account's address:

const account = new ethers.utils.Interface(accountABI);
const erc20Token = new ethers.utils.Interface(partialERC20TokenABI);

op.callData = account.encodeFunctionData("execute", [
  usdcToken,
  ethers.constants.Zero,
  erc20Token.encodeFunctionData("transfer", [accountAddress, amount]),
]);

πŸ“˜

Why are there two ABIs?

In the above example our Smart Account is required to interact with the USDC contract. We need to first call execute on our Smart Account which will internally call transfer on the ERC20 token.

If we only wanted to send an amount of ETH to another account's address, the code would look like this:

const account = new ethers.utils.Interface(accountABI);

op.callData = account.encodeFunctionData("execute", [accountAddress, amount, "0x"]);

Just like the simplest EOA transaction, there is no data field. Which means we don't need to encode any data within the userOp's callData.