Skip to main content
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: 0n
  }
];

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.

Execution Input

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: 0n
};

Multiple Transactions

const transactions: MetaTransaction[] = [
  {
    to: '0xToken...',
    value: 0,
    data: iface.encodeFunctionData('approve', ['0xSpender...', amount]),
    operation: 0,
    salt: 0n
  },
  {
    to: '0xSpender...',
    value: 0,
    data: spenderIface.encodeFunctionData('execute', [params]),
    operation: 0,
    salt: 1n
  }
];
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: 0n
  }
];

// 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

Build docs developers (and LLMs) love