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
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 Bvalue
: 1 ETHdata
: 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 contractvalue
: 0 ETHdata
: 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
callData
with ethers.jsFortunately, 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 calltransfer
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
.
Updated about 1 year ago