Documentation Index Fetch the complete documentation index at: https://mintlify.com/snapshot-labs/sx-monorepo/llms.txt
Use this file to discover all available pages before exploring further.
Executors (also called execution strategies) define how passed proposals are executed onchain. They determine what transactions can be executed, who can trigger execution, and what conditions must be met.
What are Executors?
An executor is a contract that:
Receives execution requests from the space contract
Validates execution conditions (quorum, timelock, etc.)
Executes transactions on behalf of the governance system
Each proposal specifies an execution strategy when created, determining how it will be executed if it passes.
Executor Types
Snapshot X supports multiple execution patterns:
type ExecutorType =
| 'SimpleQuorumVanilla'
| 'SimpleQuorumAvatar'
| 'SimpleQuorumTimelock'
| 'EthRelayer'
| 'Axiom'
| 'Isokratia' ;
Core Executors
Vanilla Executor
The simplest executor - no actual execution, just marks the proposal as passed.
function createVanillaExecutor () {
return {
type: 'vanilla' ,
getExecutionData ( executorAddress : string ) {
return {
executor: executorAddress ,
executionParams: []
};
}
};
}
Use case : Signaling proposals that don’t require onchain execution.
Deployment : No deployment needed, uses a singleton contract.
Avatar Executor (Safe/Zodiac)
Executes transactions through a Gnosis Safe or any Avatar-compatible contract.
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 ]
};
}
};
}
Transaction format :
type MetaTransaction = {
to : string ; // Target contract address
value : string ; // ETH value to send
data : string ; // Encoded function call
operation : 0 | 1 ; // 0 = Call, 1 = DelegateCall
salt : string ; // Unique identifier
};
Deployment :
const { address , txId } = await client . deployAvatarExecution ({
signer ,
params: {
controller: '0x...' , // Address that can update settings
target: '0x...' , // Safe/Avatar address
spaces: [ '0x...' ], // Authorized space addresses
quorum: 1000000 n // Minimum voting power required (in wei)
},
saltNonce: '0x...'
});
Use case : Execute arbitrary transactions through a Safe multi-sig after governance approval.
Timelock Executor
Adds a time delay between proposal passing and execution, with veto capability.
// Uses same getExecutionData as Avatar executor
Deployment :
const { address , txId } = await client . deployTimelockExecution ({
signer ,
params: {
controller: '0x...' , // Address that can update settings
vetoGuardian: '0x...' , // Address that can veto executions
spaces: [ '0x...' ], // Authorized space addresses
timelockDelay: 172800 n , // Delay in seconds (e.g., 2 days)
quorum: 1000000 n // Minimum voting power required
},
saltNonce: '0x...'
});
Execution flow :
Proposal Passes
Once voting ends and quorum is met, proposal is queued
Timelock Delay
Wait for the configured delay period
Execute or Veto
Either the veto guardian vetoes, or anyone executes the queued proposal
Execute queued proposal :
await client . executeQueuedProposal ({
signer ,
executionStrategy: '0x...' ,
executionParams: '0x...' // Encoded transactions
});
Veto execution :
await client . vetoExecution ({
signer ,
executionStrategy: '0x...' ,
executionHash: '0x...' // Hash of the execution to veto
});
Use case : Add security delay for high-stakes governance decisions, allowing time for review or emergency veto.
Cross-Chain Executors
EthRelayer Executor
Relays execution from Starknet to Ethereum L1.
function createEthRelayerExecutor ({ destination } : { destination : string }) {
return {
type: 'ethRelayer' ,
getExecutionData ( executorAddress : string , transactions : MetaTransaction []) {
const abiCoder = new AbiCoder ();
const executionParams = abiCoder . encode (
[ 'tuple(address to, uint256 value, bytes data, uint8 operation)[]' ],
[ transactions ]
);
// Hash the execution params for cross-chain verification
const executionHash = uint256 . bnToUint256 (
BigInt ( keccak256 ( executionParams ))
);
return {
executor: executorAddress ,
executionParams: [
destination ,
`0x ${ executionHash . low . toString ( 16 ) } ` ,
`0x ${ executionHash . high . toString ( 16 ) } `
]
};
}
};
}
Configuration :
destination: L1 executor contract that will receive the execution
transactions: Array of transactions to execute on L1
Use case : Starknet governance controlling Ethereum contracts.
ZK-Proof Executors
Axiom Executor
Uses Axiom’s ZK proofs to verify historical blockchain data for execution.
const { address , txId } = await client . deployAxiomExecution ({
signer ,
params: {
controller: '0x...' ,
quorum: 1000000 n ,
contractAddress: '0x...' , // Contract to read data from
slotIndex: 0 n , // Storage slot to read
space: '0x...' , // Space contract address
querySchema: '0x...' // Axiom query schema hash
},
saltNonce: '0x...'
});
Use case : Execute based on proven historical token balances or other onchain state.
Isokratia Executor
Executes based on ZK proofs with a proving time allowance.
const { address , txId } = await client . deployIsokratiaExecution ({
signer ,
params: {
provingTimeAllowance: 86400 , // 24 hours to generate proof
quorum: 1000000 n ,
queryAddress: '0x...' , // Query contract address
contractAddress: '0x...' , // Contract to read data from
slotIndex: 0 n // Storage slot to read
},
saltNonce: '0x...'
});
Use case : Execute transactions with cryptographic guarantees about voter eligibility.
Execution Data
The getExecutionData function prepares execution parameters for a proposal:
function getExecutionData (
type : ExecutorType ,
executorAddress : string ,
input ?: ExecutionInput
) {
if ( type === 'SimpleQuorumVanilla' ) {
return createVanillaExecutor (). getExecutionData ( executorAddress );
}
if (
[ 'SimpleQuorumAvatar' , 'SimpleQuorumTimelock' ]. includes ( type ) &&
input ?. transactions
) {
return createAvatarExecutor (). getExecutionData (
executorAddress ,
input . transactions
);
}
if ( type === 'EthRelayer' && input ?. transactions && input . destination ) {
return createEthRelayerExecutor ({
destination: input . destination
}). getExecutionData ( executorAddress , input . transactions );
}
if ( type === 'Axiom' && input ?. transactions ) {
return createAxiomExecutor (). getExecutionData (
executorAddress ,
input . transactions
);
}
if ( type === 'Isokratia' && input ?. transactions ) {
return createIsokratiaExecutor (). getExecutionData (
executorAddress ,
input . transactions
);
}
throw new Error (
`Not enough data to create execution for executor ${ executorAddress } `
);
}
Using Executors
1. Deploy Executor
First, deploy your chosen executor contract:
const { address : executorAddress , txId } = await client . deployAvatarExecution ({
signer ,
params: {
controller: await signer . getAddress (),
target: safeAddress ,
spaces: [], // Will add space after deployment
quorum: ethers . parseEther ( '1000' ) // 1000 tokens
}
});
2. Create Space with Executor
Reference the executor when creating proposals:
await client . propose ({
signer ,
envelope: {
data: {
space: '0x...' ,
authenticator: '0x...' ,
strategies: [ ... ],
executionStrategy: {
addr: executorAddress ,
params: '0x...' // Executor-specific params
},
metadataUri: 'ipfs://...'
}
}
});
3. Execute Passed Proposal
After voting ends and the proposal passes:
// For immediate execution (Avatar/Vanilla)
await client . execute ({
signer ,
space: '0x...' ,
proposal: 1 ,
executionParams: '0x...' // Encoded transactions
});
// For timelock execution
// Wait for timelock delay, then:
await client . executeQueuedProposal ({
signer ,
executionStrategy: executorAddress ,
executionParams: '0x...'
});
Transaction Building
When creating proposals with transactions, build the transaction array:
import { AbiCoder , Interface } from '@ethersproject/abi' ;
const targetInterface = new Interface ([
'function transfer(address to, uint256 amount)'
]);
const transactions : MetaTransaction [] = [
{
to: tokenAddress ,
value: '0' ,
data: targetInterface . encodeFunctionData ( 'transfer' , [
recipientAddress ,
ethers . parseEther ( '100' )
]),
operation: 0 , // Call
salt: '0x0000000000000000000000000000000000000000000000000000000000000001'
},
// Add more transactions as needed
];
const { executor , executionParams } = createAvatarExecutor (). getExecutionData (
executorAddress ,
transactions
);
Executor Configuration
Each network configuration specifies available executor implementations:
type EvmNetworkConfig = {
// ...
executionStrategiesImplementations : {
SimpleQuorumVanilla ?: string ;
SimpleQuorumAvatar ?: string ;
SimpleQuorumTimelock ?: string ;
EthRelayer ?: string ;
Axiom ?: string ;
Isokratia ?: string ;
};
};
These are used by the proxy factory to deploy new executor instances.
Execution Flow
Spaces Learn about governance spaces
Strategies Understand voting power
Creating Proposals Create executable proposals
Safe Integration Integrate with Gnosis Safe