Executors define how approved proposals are executed onchain. They handle transaction encoding, multi-call execution, timelock integration, and cross-chain relaying.
Overview
The SDK provides the getExecutionData function to prepare execution data for different executor types:
import { getExecutionData } from '@snapshot-labs/sx' ;
const { executor , executionParams } = getExecutionData (
executorType ,
executorAddress ,
executionInput
);
Executor Types
The SDK supports several executor types:
type ExecutorType =
| 'SimpleQuorumVanilla'
| 'SimpleQuorumAvatar'
| 'SimpleQuorumTimelock'
| 'EthRelayer'
| 'Axiom'
| 'Isokratia' ;
Vanilla Executor
Simple execution without transaction payloads:
import { getExecutionData } from '@snapshot-labs/sx' ;
const data = getExecutionData (
'SimpleQuorumVanilla' ,
'0xExecutorAddress...'
);
// Returns:
// {
// executor: '0xExecutorAddress...',
// executionParams: []
// }
Use case : Signaling proposals that don’t require onchain execution.
Avatar Executor
Executes transactions through a Zodiac Avatar (Safe, Timelock, etc.):
import { getExecutionData } from '@snapshot-labs/sx' ;
import { MetaTransaction } from '@snapshot-labs/sx/types' ;
const transactions : MetaTransaction [] = [
{
to: '0xTargetContract...' ,
value: 0 ,
data: '0x...' , // encoded function call
operation: 0 , // 0 = Call, 1 = DelegateCall
salt: 0 n
}
];
const data = getExecutionData (
'SimpleQuorumAvatar' ,
'0xExecutorAddress...' ,
{ transactions }
);
Use case : Execute proposal transactions through a Gnosis Safe or similar avatar.
The Avatar executor encodes transactions using ABI encoding compatible with Zodiac modules.
Operation Types
0 (Call): Regular contract call
1 (DelegateCall): Delegate call (executes code in the context of the calling contract)
Timelock Executor
Executes transactions through a timelock contract:
import { getExecutionData } from '@snapshot-labs/sx' ;
const data = getExecutionData (
'SimpleQuorumTimelock' ,
'0xTimelockAddress...' ,
{ transactions }
);
Use case : Add a time delay between proposal approval and execution for security.
Timelock executors use the same encoding as Avatar executors but require waiting for the timelock period before execution.
ETH Relayer Executor
Relays execution from one chain to another (typically L2 → L1):
import { getExecutionData } from '@snapshot-labs/sx' ;
const data = getExecutionData (
'EthRelayer' ,
'0xRelayerAddress...' ,
{
transactions ,
destination: '0xL1TargetAddress...' // L1 destination address
}
);
Use case : Starknet governance controlling Ethereum contracts.
The ETH Relayer uses Starknet’s L2→L1 messaging to execute transactions on Ethereum mainnet.
Axiom Executor
Executes transactions using Axiom’s ZK proof verification:
import { getExecutionData } from '@snapshot-labs/sx' ;
const data = getExecutionData (
'Axiom' ,
'0xAxiomExecutorAddress...' ,
{ transactions }
);
Use case : Execute proposals with ZK-verified voting results.
Isokratia Executor
Custom executor for Isokratia governance:
import { getExecutionData } from '@snapshot-labs/sx' ;
const data = getExecutionData (
'Isokratia' ,
'0xIsokratiaExecutorAddress...' ,
{ transactions }
);
Use case : Isokratia-specific governance execution.
Most executors require an execution input object:
interface ExecutionInput {
transactions ?: MetaTransaction [];
destination ?: string ; // Required for EthRelayer
}
interface MetaTransaction {
to : string ; // Target contract address
value : string | number ; // ETH value to send
data : string ; // Encoded function call
operation : number ; // 0 = Call, 1 = DelegateCall
salt : bigint ; // Nonce for transaction uniqueness
}
Creating Execution Transactions
Single Transaction
import { ethers } from 'ethers' ;
const iface = new ethers . Interface ([
'function transfer(address to, uint256 amount)'
]);
const transaction : MetaTransaction = {
to: '0xTokenAddress...' ,
value: 0 ,
data: iface . encodeFunctionData ( 'transfer' , [
'0xRecipient...' ,
ethers . parseEther ( '100' )
]),
operation: 0 ,
salt: 0 n
};
Multiple Transactions
const transactions : MetaTransaction [] = [
{
to: '0xToken...' ,
value: 0 ,
data: iface . encodeFunctionData ( 'approve' , [ '0xSpender...' , amount ]),
operation: 0 ,
salt: 0 n
},
{
to: '0xSpender...' ,
value: 0 ,
data: spenderIface . encodeFunctionData ( 'execute' , [ params ]),
operation: 0 ,
salt: 1 n
}
];
Use unique salt values for each transaction to ensure they can be distinguished on-chain.
Execution Hashes
For Avatar executors, you can compute execution hashes:
import { encoding } from '@snapshot-labs/sx' ;
const { executionHash , txHashes } = encoding . createExecutionHash (
transactions ,
executorAddress ,
chainId
);
// executionHash: overall hash of all transactions
// txHashes: array of individual transaction hashes
Use case : Verify execution data before submitting or track execution status.
Error Handling
The getExecutionData function throws errors for invalid configurations:
try {
const data = getExecutionData ( type , address , input );
} catch ( error ) {
if ( error . message . includes ( 'Not enough data' )) {
// Missing required transactions or destination
}
}
Always provide transactions for executors that require them (Avatar, Timelock, Axiom, Isokratia). Only Vanilla executor works without transactions.
Common Patterns
Safe Execution
import { getExecutionData } from '@snapshot-labs/sx' ;
// 1. Prepare transactions
const transactions = [
{
to: targetAddress ,
value: 0 ,
data: encodedCall ,
operation: 0 ,
salt: 0 n
}
];
// 2. Get execution data
const { executor , executionParams } = getExecutionData (
'SimpleQuorumAvatar' ,
safeAddress ,
{ transactions }
);
// 3. Include in proposal
const proposal = {
// ... other proposal fields
executionStrategy: {
address: executor ,
params: executionParams
}
};
Cross-Chain Execution
// L2 proposal executing on L1
const { executor , executionParams } = getExecutionData (
'EthRelayer' ,
l2RelayerAddress ,
{
transactions: l1Transactions ,
destination: l1TargetAddress
}
);
Timelock with Delay
// Execute through timelock
const { executor , executionParams } = getExecutionData (
'SimpleQuorumTimelock' ,
timelockAddress ,
{ transactions }
);
// After proposal passes:
// 1. Queue transactions (starts timelock)
// 2. Wait for timelock period
// 3. Execute transactions
Executor Implementations
Each executor type has its own implementation:
// Avatar Executor (packages/sx.js/src/executors/avatar.ts)
function createAvatarExecutor () {
return {
type: 'avatar' ,
getExecutionData ( executorAddress : string , transactions : MetaTransaction []) {
const abiCoder = new AbiCoder ();
const executionParams = abiCoder . encode (
[ 'tuple(address to, uint256 value, bytes data, uint8 operation, uint256 salt)[]' ],
[ transactions ]
);
return {
executor: executorAddress ,
executionParams: [ executionParams ]
};
}
};
}
Best Practices
Validate Transactions Test transaction encoding before submitting proposals
Use Unique Salts Ensure each transaction has a unique salt value
Consider Gas Limits Multiple transactions may hit gas limits on execution
Test Execution Simulate execution locally before creating proposals
Security Considerations
Use Call (0) for most operations. Only use DelegateCall (1) when you specifically need to execute code in the caller’s context, as it can be dangerous.
For cross-chain executors, ensure the destination address is correct. Mistakes can lead to loss of funds.
Transactions execute in the order provided. Ensure dependencies are ordered correctly (e.g., approve before transfer).
Timelocks provide a safety window for detecting malicious proposals before execution.
Utils Execution hash utilities
Authenticators Authentication methods
Strategies Voting strategies