The Biconomy Modular Execution Environment stack enables developers to asynchronously orchestrate a series of "calls" across multiple blockchains. This enables them to easily achieve things like:
- Multichain DeFi Automation
- Chain Abstraction
- Unified Multichain Balances
- Transaction Scheduling
- Repeat Transactions
In order to achieve the goals set out by Modular Execution Environments (such as the decentralized MEE - Biconomy Network), there was a requirement for a new data structure to contain all of the instructions required for successful orchestration.
The Supertransaction
We call that data structure the Supertransaction. It contains all of the instructions for all of the calls across all of the chains within a single object and, more importantly, represents all of those instructions with a single hash.
By signing that single hash, the user is giving approval to execute all of the instructions on all of the chains. This is the engine which enables the single-signature flows within Modular Execution Environments.

Encoding the Data
The Supertransaction uses a Merkle Tree data structure to represent all of the instructions contained with a single hash. Let's take a look at how we would encode one very simple Supertransaction.
This Supertransaction will encode:
- Approve Across Spoke Contract to spend USDC
- Call Across Spoke Contract
- When the funds arrive on the destination chain, we'll send them to two addresses
Essentially, this will be four function calls:
approve
call on USDC contract on Optimismdeposit
call on the Across Spoke Pool contract on Optimism- Two
transfer
calls on the USDC contract on Base
1. Encode all of the function calls
All of the function calls are encoded mostly normally, just like you would encode a function call to a vanilla EVM contract. The slight difference is that, instead of just encoding the call itself, you encode a proxy call to an execute
or batchExecute
functions on a contract which enables validation and orchestration of instuctions within the Supertransaction. To ensure maximum compatibility with wallets and accounts - the contract which orchestrates the transactions has been made compatible with ERC-4337, ERC-7579, ERC-7710 and EIP-7702 standards.
An example of how to encode an action.
To learn how to actually use and encode function calls, check out our docs.
const spokePoolAddress = '0x...'
const smartAccountAddress = '0x...'
const amountUsed = parseUnits('100', 6) // USDC has 6 decimals
const usdcAddressOnOptimism '0x...'
const usdcAddressOnBase = '0x...'
// Encoding an approval call to the Across Spoke pool contract.
const approveSpokePoolCallData = encodeFunctionData({
abi: erc20Abi,
functionName: 'approve',
args: [
spokePoolAddress,
amountUsed
]
})
// Encoding a call to the Across contract
const depositSpokePoolData = encodeFunctionData({
abi: DEPOSIT_ABI,
functionName: 'deposit',
args: [
amountUsed, // amount being bridged
smartAccountAddress, // recipientOfBridging
base.id, // chainId of the destination chain
usdcAddressOnOptimism, // origin token being bridged
... // you would place some Across specific responses here
// we'll skip those for now as they don't help explain the concepts
]
});
// ABI of the orchestration smart account
const erc7579Abi = parseAbi([
'function batchExecute(tuple(address target, uint256 value, bytes data)[] calldata operations) external returns (bytes[] memory)',
]);
// Encode the function call data objects into objects taken by the
// orchestration smart account.
const calls = [
{
target: usdcAddressOnOptimism,
data: approveSpokePoolCallData,
value: 0n
},
{
target: spokePoolAddress,
data: depositSpokePoolData,
value: 0n
}
]
// Encode approve + bridge into a function call on source
const executeCallsOptimism = encodeFunctionData({
abi: erc7579Abi,
functionName: 'batchExecute',
args: calls,
});
// Encode ERC20 Transfer data calls
const [transfer1, transfer2] = [encodeFunctionData({..}), ...]
const transferCallsBase = encodeFunctionData({
abi: erc7579Abi,
functionName: 'batchExecute',
args: [transfer1, transfer2],
});
Encoding an action within a Supertransaction
This call to the batchExecute
function is a single "instruction" within a Supertransaction as it's a single call to blockchain. This has been inherited from the Account Abstraction UserOp
model.
The core innovation of the Supertransaction model is that it can contain an arbitrary amount of these batchExecute
or execute
calls which don't have to be on the same chain. Additionally, when these calls are sent to the Nodes, they use intelligent orchestration to make sure that they're executed in the correct order (in our case, this would mean that the two transfer
functions on destination will not be executed until the funds are successfully bridged).
2. Encode Payment Info
Every Supertransaction has an associated PaymentInfo object which sets the conditions on how the node will charge for the entire execution flow. This is usually a transfer of some ERC-20 token or native coin from the user account to the Node account. The Node will claim those funds and then execute all the instructions contained within the Supertransaction.
An example of payment info might be:
const paymentInfo = {
to: '0xUSDC_ADDRESS_OPTIMISM',
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'transfer',
args: [
'0x_EXECUTING_NODE_ADDRESS', // Recipient
parseUnit('0.2', 6) // Amount paid to node
]
})
}
Payment info is filled out by the Node before the user accepts the terms. As will be explained in later articles, Modular Execution Environments follow a quote/commit model of execution.
3. Setting the Execution Time Constraints and Metadata
Every instruction in the Supertransaction has a minimum execution time and a maximum execution time. This constrains the execution to a specific period of time. The variables controlling this are called lowerBoundTimestamp
and upperBoundTimestamp
!
By setting these variables, you're telling the Node exactly which time span to execute the transaction in.
These variables are also encoded in the Supertransaction along with other metadata information required by the Node. Other metadata includes things like:
- Gas limits defined in the ERC-4337 standard.
- Account init code which is used to lazy deploy an orchestrator account contract if it's not deployed.
4. Calculate the Supertransaction Hash
The Supertransaction Hash is constructed by taking the PaymentInfo object and all of the instructions and constructing a Merkle Tree. Somewhat similar to this pseudocode (note that the hashed calls include also the Metadata and execution time constraints mentioned in the previous section).
const paymentHash = hash(paymentCall)
const executeOptimismHash = hash(executeCallsOptimism)
const executeBaseHash = hash(executeCallsBase)
const filler = hash(zeroAddress) // Must always have an even number of leafs
const hashA = hash(paymentHash, executeOptimismHash)
const hashB = hash(executeBaseHash, filler)
const supertransactionHash = hash(hashA, hashB)
Conclusion
The Supertransaction provides a very powerful new data primitive for executing multiple calls across multiple chains. Combined with the PaymentInfo object it allows for effortless cross-chain gas abstraction. Additionally, combined with the Modular Execution Environment and its orchestration capabilities - it enables asynchronous orchestration of all of the contained instructions.
To learn more about all of those features, continue reading our Learn series: