Skip to main content

Curve Voting Library

The curve-voting-lib is a Python toolkit for creating and simulating Curve DAO governance votes. Built on titanoboa (a Vyper/EVM execution framework), it replaces the deprecated curve-dao package.

GitHub

Source code for curve-voting-lib can be found on GitHub.


Overview

The library uses a context manager pattern (vote()) that automatically captures contract interactions and constructs Aragon EVM scripts. Every vote is simulation-first: before going live, the library creates the vote using the Convex voter proxy (the largest veCRV holder), votes yes, time-travels past the voting period, and executes — verifying the entire lifecycle on a forked mainnet.

Key features:

  • Context manager votingvote() monkey-patches titanoboa's prepare_calldata to capture contract calls
  • Automatic simulation — every vote is simulated before going live
  • IPFS integration — vote descriptions are pinned via Pinata with local caching
  • Cross-chain supportxvote() context manager for cross-chain governance across 20+ chains

Installation

git clone https://github.com/curvefi/curve-voting-lib.git
cd curve-voting-lib
uv sync
uv run python -m pip install -e .

Environment Variables

VariableDescription
RPC_URLEthereum RPC endpoint URL
PINATA_JWTPinata API JWT token for IPFS pinning

DAO Types

Curve has two DAO types with different quorum and support requirements:

DAO TypeQuorumSupportUse Case
OWNERSHIP30%51%Critical changes (ownership transfers, etc.)
PARAMETER15%60%Parameter adjustments (pool fees, etc.)

Each DAO type is a DAOParameters dataclass containing the agent, voting contract, token, and quorum:

from voting import OWNERSHIP, PARAMETER

# OWNERSHIP
# agent: 0x40907540d8a6C65c637785e8f8B742ae6b0b9968
# voting: 0xE478de485ad2fe566d49342Cbd03E49ed7DB3356
# token: 0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2
# quorum: 30

# PARAMETER
# agent: 0x4eeb3ba4f221ca16ed4a0cc7254e2e32df948c5f
# voting: 0xbcff8b0b9419b9a88c44546519b1e909cf330399
# token: 0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2
# quorum: 15

Core API

vote() Context Manager

The primary interface. Any mutable contract call made inside the with vote(...) block is automatically captured and bundled into an Aragon EVM script.

from voting import vote, OWNERSHIP

with vote(
OWNERSHIP,
"Description of what this vote does",
live_env=None, # None = simulation only, BrowserEnv() or CustomEnv() for live
):
# All contract calls here are captured as vote actions
contract.do_something(value)
assert contract.something() == value # assertions work too

The context manager also acts as a prank (the caller is set to the DAO agent) and an anchor (state changes are reverted after the block). On exit, it:

  1. Prepares the EVM script from captured actions
  2. Creates the vote using the Convex voter proxy (0x989AEB4D175E16225E39E87D0D97A3360524AD80)
  3. Votes yes, time-travels past the voting period, and executes
  4. If live_env is set, pins the description to IPFS and creates the vote on-chain

vote_test() Context Manager

Use vote_test() inside a vote() block to run assertions without capturing them as vote actions:

with vote(OWNERSHIP, "Set pool implementation"):
factory.set_pool_implementation(new_impl, impl_hash)

with vote_test():
# These calls are NOT captured as vote actions
assert factory.pool_implementations(impl_hash) == new_impl

Live Voting Environments

For creating actual on-chain votes (not just simulations):

from voting import vote, OWNERSHIP, BrowserEnv, CustomEnv

# Using a browser wallet (Rabby, MetaMask, etc.)
with vote(OWNERSHIP, "Description", live_env=BrowserEnv()):
contract.do_something()

# Using a custom account
from eth_account import Account
env = CustomEnv(rpc="https://eth.llamarpc.com", account=Account.from_key("0x..."))
with vote(OWNERSHIP, "Description", live_env=env):
contract.do_something()

Usage Example

This example sets a new pool implementation on the TwoCrypto-NG factory:

import os
import boa
from voting import vote, abi, OWNERSHIP, BrowserEnv
from eth_utils import keccak

RPC_URL = os.getenv("RPC_URL")
boa.fork(RPC_URL)
factory = abi.twocrypto_ng_mainnet_factory.at("0x98EE851a00abeE0d95D08cF4CA2BdCE32aeaAF7F")

with vote(
OWNERSHIP,
"[twocrypto] Add implementations for donations-enabled pools",
live=BrowserEnv(),
):
factory.set_pool_implementation(
donations_pool := "0xbab4CA419DF4e9ED96435823990C64deAD976a9F",
donations_hash := int.from_bytes(keccak(b"donations"), "big"),
)

assert factory.pool_implementations(donations_hash) == donations_pool

Scripts can be run using:

uv run scripts/gauges/add_gauge.py
uv run scripts/twocrypto-ng/set_implementation.py

Cross-chain Governance (xvote())

The xvote() context manager nests inside vote() to create cross-chain governance proposals. It forks the target chain, captures messages, and broadcasts them through the appropriate broadcaster contract.

from voting import vote, xvote, OWNERSHIP
from voting.xgov.chains import FRAXTAL

with vote(OWNERSHIP, description="[Fraxtal] Set things"):
with xvote(FRAXTAL, "https://rpc.frax.com"):
things.set()

Supported Chains

The library supports 20+ chains including Arbitrum, Optimism, Base, Fraxtal, Mantle, Polygon, Gnosis, Avalanche, BSC, Fantom, Sonic, Kava, Celo, Aurora, Moonbeam, X Layer, Taiko, Ink, Etherlink, XDC, Corn, Plume, Hyperliquid, and TAC.

Each chain uses one of several broadcaster types:

Broadcaster TypeChains
ArbitrumArbitrum
OptimismOptimism, Fraxtal, Base, Mantle
Polygon zkEVMX Layer
TaikoTaiko
Storage ProofsAll other chains (Gnosis, Polygon, BSC, Avalanche, Fantom, Sonic, Kava, Celo, etc.)

For more details on the cross-chain governance infrastructure, see the Cross-chain Governance section.


Pre-built ABIs

The abi module provides pre-configured contract interfaces for common Curve contracts:

from voting import abi

# Available ABIs include:
abi.aragon_agent # Aragon Agent contract
abi.voting # Aragon Voting contract
abi.twocrypto_ng_mainnet_factory # TwoCrypto-NG Factory
# ... and more

These are loaded using boa.loads_abi() and can be attached to deployed addresses with .at().