Skip to main content
The OpenZeppelin Governor client enables interaction with governance systems built on OpenZeppelin’s Governor contracts. It supports both v4 and v5 of the OpenZeppelin Governor standard, with gasless voting through signatures or direct transaction-based voting.

Installation

npm install @snapshot-labs/sx

Overview

OpenZeppelin Governor is a flexible and modular on-chain governance protocol used by many DAOs. The client supports both major versions:
  • Governor v4: Original version with basic ballot signing
  • Governor v5: Enhanced version with nonce-based signatures and additional parameters
The OpenZeppelin Governor client currently supports voting only. Proposal creation and execution must be done through direct contract interaction or other tools.

Client Types

The OpenZeppelin Governor client comes in two implementations:
  • OpenZeppelinEthereumSig: Signature-based voting (gasless)
  • OpenZeppelinEthereumTx: Transaction-based voting (direct)

OpenZeppelinEthereumSig (Signature-based)

The signature-based client allows users to vote using EIP-712 signatures, which can be submitted by relayers for gasless voting.

Initialization

import { clients } from '@snapshot-labs/sx';

const client = new clients.OpenZeppelinEthereumSig({
  chainId: 1 // Ethereum mainnet
});

Configuration

chainId
number
required
Chain ID for the network (1 for Ethereum mainnet, 11155111 for Sepolia)

Methods

vote()

Create a signed vote message that can be submitted to the OpenZeppelin Governor contract.
import { Wallet } from '@ethersproject/wallet';

const signer = new Wallet(privateKey);

const envelope = await client.vote({
  signer,
  authenticatorType: 'OpenZeppelinAuthenticatorSignatureV5',
  data: {
    spaceId: '0x...', // Governor contract address
    proposalId: '42', // Proposal ID (string for v5)
    choice: 1         // 1 = For
  }
});

// The envelope contains signature data that can be submitted
// to the Governor contract's castVoteBySig function
console.log('Signature:', envelope.signatureData.signature);
signer
Signer & TypedDataSigner
required
Ethers.js signer with EIP-712 signing capabilities
authenticatorType
OpenZeppelinAuthenticator
required
Version-specific authenticator type:
  • 'OpenZeppelinAuthenticatorSignatureV4' for Governor v4
  • 'OpenZeppelinAuthenticatorSignatureV5' for Governor v5
data
Vote
required
Vote data object
envelope
Envelope<Vote>
Returns an envelope containing the signature data and vote data

Version Differences

Ballot Structure
type EIP712BallotV4 = {
  proposalId: string;
  support: number;
};
Features
  • Simple ballot with proposalId and support
  • No nonce or voter address in signature
  • No built-in reason support in ballot

Submitting Signed Votes

After creating a signed vote, it must be submitted to the Governor contract:
import { Contract } from '@ethersproject/contracts';

const governorAbi = [
  'function castVoteBySig(uint256 proposalId, uint8 support, address voter, bytes signature)'
];

const governor = new Contract(
  envelope.data.spaceId,
  governorAbi,
  provider
);

const tx = await governor.castVoteBySig(
  envelope.data.proposalId,
  envelope.data.choice,
  envelope.signatureData.address,
  envelope.signatureData.signature
);

await tx.wait();
console.log('Vote submitted:', tx.hash);

OpenZeppelinEthereumTx (Transaction-based)

The transaction-based client allows direct voting through contract transactions.

Initialization

import { clients } from '@snapshot-labs/sx';
import { JsonRpcProvider } from '@ethersproject/providers';

const provider = new JsonRpcProvider('https://eth.llamarpc.com');

const client = new clients.OpenZeppelinEthereumTx({
  provider
});

Methods

vote()

Submit a vote transaction directly to the Governor contract.
import { Wallet } from '@ethersproject/wallet';

const signer = new Wallet(privateKey, provider);

const tx = await client.vote({
  signer,
  data: {
    spaceId: '0x...', // Governor contract address
    proposalId: '42',
    choice: 1 // 1 = For
  }
});

await tx.wait();
console.log('Vote cast:', tx.hash);
signer
Signer
required
Ethers.js signer connected to a provider
data
Vote
required
Vote data object (same structure as signature-based client)
transaction
TransactionResponse
Returns an ethers.js transaction response that can be awaited

Type Definitions

Vote

type Vote = {
  spaceId: string;      // OpenZeppelin Governor contract address
  proposalId: string;   // Proposal ID
  choice: 0 | 1 | 2;    // 0=Against, 1=For, 2=Abstain
  reason?: string;      // Optional vote reason (v5 only)
};

EIP712 Ballot Types

// Governor v4
type EIP712BallotV4 = {
  proposalId: string;
  support: number;
};

// Governor v5
type EIP712BallotV5 = {
  voter: string;
  proposalId: string;
  support: number;
  nonce: number;
};

// Governor v5 Extended (with reason)
type EIP712ExtendedBallotV5 = {
  voter: string;
  proposalId: string;
  support: number;
  nonce: number;
  reason: string;
  params: string;
};

SignatureData

type SignatureData = {
  authenticatorType: OpenZeppelinAuthenticator;
  address: string;
  signature: string;
  domain: TypedDataDomain;
  types: Record<string, TypedDataField[]>;
  message: Record<string, any>;
};

type OpenZeppelinAuthenticator = 
  | 'OpenZeppelinAuthenticatorSignatureV4'
  | 'OpenZeppelinAuthenticatorSignatureV5';

Governor Contract Detection

The client automatically fetches governor metadata for EIP-712 signing:
// The client queries both name() and version() from the contract
const [name, version] = await Promise.all([
  governorContract.name(),
  governorContract.version()
]);
// e.g., name: "MyDAO Governor", version: "1"

// Used in EIP-712 domain:
const domain = {
  name: name,
  version: version,
  chainId: 1,
  verifyingContract: '0x...'
};

Complete Example

Here’s a full example of voting on an OpenZeppelin Governor v5 proposal:
import { clients } from '@snapshot-labs/sx';
import { Wallet } from '@ethersproject/wallet';
import { JsonRpcProvider } from '@ethersproject/providers';
import { Contract } from '@ethersproject/contracts';

// 1. Initialize client
const client = new clients.OpenZeppelinEthereumSig({
  chainId: 1
});

// 2. Create signer
const provider = new JsonRpcProvider('https://eth.llamarpc.com');
const signer = new Wallet(process.env.PRIVATE_KEY, provider);

// 3. Generate signed vote
const envelope = await client.vote({
  signer,
  authenticatorType: 'OpenZeppelinAuthenticatorSignatureV5',
  data: {
    spaceId: '0x...', // Governor address
    proposalId: '42',
    choice: 1,
    reason: 'Strong support for this initiative'
  }
});

// 4. Submit vote to Governor contract
const governorAbi = [
  'function castVoteWithReasonAndParamsBySig(uint256 proposalId, uint8 support, address voter, string reason, bytes params, bytes signature)'
];

const governor = new Contract(
  envelope.data.spaceId,
  governorAbi,
  provider
);

const tx = await governor.castVoteWithReasonAndParamsBySig(
  envelope.data.proposalId,
  envelope.data.choice,
  envelope.signatureData.address,
  envelope.data.reason || '',
  '0x', // params
  envelope.signatureData.signature
);

await tx.wait();
console.log('Vote successfully cast:', tx.hash);

Migration from v4 to v5

If you’re upgrading from Governor v4 to v5:
1

Update Authenticator Type

Change from 'OpenZeppelinAuthenticatorSignatureV4' to 'OpenZeppelinAuthenticatorSignatureV5'
2

Update Contract Calls

Use the new v5 signature format (voter address instead of v,r,s)
3

Add Reason Support

Take advantage of built-in reason parameter in v5 ballots
4

Handle Nonces

The client automatically fetches nonces - no changes needed

Source Code

View the complete implementation:

Build docs developers (and LLMs) love