Skip to main content

Futarchy

✅ Ready to Dev

This contract specification has passed business logic audit, cross-program integration check, and authority model verification. A developer can implement from this document.
Per-OT governance contract. Every OT project (RCP, ARL, etc.) gets its own Futarchy instance. OT holders govern three key decisions: minting new tokens, spending treasury funds, and changing revenue distribution. V1: team authority approves proposals. V2: futarchy prediction markets resolve proposals.
Upgradeable contract. Program upgrade authority = Team Multisig (Squads). Separate from governance authority.

Key Concepts

8 Instructions

Initialize, create/approve/cancel/execute proposal, claim OT governance, authority transfer

2 State Accounts

FutarchyConfig, Proposal

2 PDA Seeds

futarchy_config, proposal

What Holders Can Govern

🪙 Mint OT

Create new OT tokens for the project.CPI → ownership_token::mint_otUsed for: raising capital, rewarding contributors, expanding supply.

💰 Spend Treasury

Transfer any token from OT Treasury to any destination.CPI → ownership_token::spend_treasuryUsed for: funding operations, paying expenses, investing.

📊 Update Revenue Split

Change how revenue is distributed between holders, treasury, and Nexus.CPI → ownership_token::batch_update_destinationsUsed for: adjusting protocol economics.
All OT projects are equal. ARL (Areal Finance) uses the same Futarchy contract as RCP (Rental Car Pool) or any other OT project. No special treatment. The only difference is what the OT represents — the governance mechanics are identical.

V1 vs V2

V1: Authority Approval

Current implementation.
  • Authority (team wallet) creates and approves proposals
  • Single signer = instant approval
  • Simple and fast for early stage
  • Authority transferred to Team Multisig after bootstrap

V2: Prediction Markets (future)

Planned upgrade.
  • OT holders create proposals
  • Futarchy prediction market resolves: “Will this proposal increase OT value?”
  • Market-based decision making
  • No single authority — decentralized governance

Instructions

Initialization

Create governance for an OT project. Called once per OT.
ParameterTypeDescription
ot_mintPubkeyThe OT this governance serves
Caller: Deployer (one-time per OT)Accounts:
  • deployer (signer, mut) — pays for creation
  • ot_mint — the OT token mint
  • config (init) — PDA seed: ["futarchy_config", ot_mint]
  • system_program
Creates:
  • FutarchyConfig PDA — governance config for this OT
Initial state:
  • authority = deployer (transferred to Team Multisig after bootstrap)
  • ot_mint = ot_mint
  • next_proposal_id = 0
  • is_active = true

Proposals

Create a new governance proposal. V1: authority creates. V2: any OT holder above threshold.
ParameterTypeDescription
proposal_typeProposalTypeMintOt, SpendTreasury, or UpdateDestinations
amountu64Amount for mint/spend (0 for UpdateDestinations)
destinationPubkeyRecipient for mint/spend, or ignored for UpdateDestinations
token_mintPubkeyToken mint for SpendTreasury (which token to spend)
params_hash[u8; 32]Hash of full params (for UpdateDestinations: hash of new destinations array)
Caller: Authority (V1) / OT holder above threshold (V2)Accounts:
  • proposer (signer, mut) — pays for Proposal account
  • config — validates is_active
  • proposal (init) — PDA seed: ["proposal", config, proposal_id]
  • system_program
Validation:
  • Config must be active
  • V1: proposer == config.authority
  • V2 (future): proposer holds >= threshold OT tokens (threshold TBD — likely configurable in FutarchyConfig, e.g., min_proposal_ot_balance: u64, set by authority)
  • For MintOt / SpendTreasury: amount > 0 (no zero-amount proposals)
  • For SpendTreasury / MintOt: destination ≠ Pubkey::default() (no zero-address recipients)
  • For UpdateDestinations: params_hash ≠ [0; 32] (sanity check against empty hash)
Effect: Creates Proposal with status=Active, increments next_proposal_id. Emits ProposalCreated.
V1 rate limiting: Not needed — only authority can create proposals, providing implicit rate control. V2 rate limiting (future): When any holder can propose, add cooldown per proposer (e.g., 1 proposal per 24h per wallet) and proposal deposit (refundable on execution/cancel) to prevent spam. This should be added during V2 implementation.
Approve a proposal. V1: authority signs. V2: prediction market resolves.Caller: Authority (V1) / Automatic via market resolution (V2)Accounts:
  • authority (signer) — must match config.authority (V1)
  • config
  • proposal (mut) — must be Active status
Validation:
  • proposal.status == Active
Effect: Sets proposal.status = Approved. Emits ProposalApproved.
Cancel an active proposal. Only the authority can cancel. Cannot cancel already approved or executed proposals.Caller: Authority (V1)Accounts:
  • authority (signer) — must match config.authority
  • config
  • proposal (mut) — must be Active status
Validation:
  • proposal.status == Active
Effect: Sets proposal.status = Cancelled. Emits ProposalCancelled.
Execute an approved proposal. Permissionless — anyone can trigger execution once approved. CPIs to OT contract based on proposal_type.Caller: PermissionlessAccounts:
  • executor (signer, mut) — pays for CPI fees
  • config
  • proposal (mut) — must be Approved status
  • OT contract accounts (varies by proposal_type — order must match OT instruction layout):
    • MintOt: ot_governance (OT governance PDA ["ot_governance", ot_mint]), ot_config (mut, ["ot_config", ot_mint]), ot_mint (mut), recipient_token_account (mut, ATA of proposal.destination for OT mint), recipient (proposal.destination), payer (signer, pays ATA init if needed — can be executor)
    • SpendTreasury: ot_governance (["ot_governance", ot_mint]), ot_treasury (["ot_treasury", ot_mint]), treasury_token_account (mut, Treasury ATA for proposal.token_mint), destination_token_account (mut, ATA at proposal.destination — must exist), token_mint (proposal.token_mint)
    • UpdateDestinations: ot_governance (["ot_governance", ot_mint]), revenue_config (mut, ["revenue_config", ot_mint]), ot_mint, plus remaining_accounts[0] (serialized Vec<BatchDestination>)
  • ot_program — constraint: key == OT_PROGRAM_ID
  • token_program, system_program
Executor guide: All OT PDA addresses are derivable from config.ot_mint. Executor reads proposal fields (amount, destination, token_mint, proposal_type) and derives required accounts. For UpdateDestinations, executor must also fetch serialized destinations data matching proposal.params_hash (from off-chain source published by proposer).
Logic by ProposalType:MintOt:
  1. CPI → ownership_token::mint_ot(proposal.amount)
  2. Futarchy config PDA signs as OT governance authority
  3. New OT tokens minted to proposal.destination
SpendTreasury:
  1. CPI → ownership_token::spend_treasury(proposal.amount)
  2. Futarchy config PDA signs as OT governance authority
  3. Tokens transferred from OT Treasury to proposal.destination
  4. proposal.token_mint determines which token to spend
UpdateDestinations:
  1. Executor passes full destinations array via remaining_accounts
  2. Deserialization:
    // remaining_accounts[0].data contains Borsh-serialized Vec<BatchDestination>
    let data = remaining_accounts[0].try_borrow_data()?;
    let destinations: Vec<BatchDestination> = Vec::<BatchDestination>::deserialize(&mut &data[..])?;
    
  3. Hash verification: sha256(remaining_accounts[0].data) == proposal.params_hash — ensures executor passes the exact destinations that were proposed
  4. CPI → ownership_token::batch_update_destinations(destinations) with destinations as instruction parameter
  5. Futarchy config PDA signs as OT governance authority via seeds [b"futarchy_config", config.ot_mint.as_ref(), &[config.bump]]
BatchDestination struct (defined in OT contract, used here for serialization):
FieldTypeDescription
addressPubkeyTarget USDC token account
allocation_bpsu16Allocation in basis points (1-10,000)
label[u8; 32]Human-readable label
Hash construction: params_hash = sha256(borsh_serialize(destinations_vec)) where destinations_vec: Vec<BatchDestination>. Proposer computes hash off-chain when creating proposal; executor must pass identical serialized data in remaining_accounts[0].
Executor workflow: Proposer publishes params_hash and full destinations data off-chain (e.g., IPFS or API). Executor downloads data, passes as remaining_accounts[0], contract verifies hash match before CPI. If hash mismatch → revert.
Validation:
  • proposal.status == Approved
  • Proposal not already executed
Effect: Sets proposal.status = Executed, executed_ts = now. Emits ProposalExecuted.

OT Governance Claim

Accept OT governance authority on behalf of Futarchy config PDA. Called during deployment after OT proposes authority transfer to this Futarchy instance. Permissionless — anyone can trigger once OT has proposed.Caller: PermissionlessAccounts:
  • executor (signer, mut)
  • config — Futarchy config PDA (signs CPI as new OT authority)
  • ot_governance (mut) — OT governance account (pending_authority must == config PDA)
  • ot_mint — for PDA derivation
  • ot_program — constraint: key == OT_PROGRAM_ID
Validation:
  • ot_governance.pending_authority == config.key() (OT must have proposed this Futarchy)
Effect: CPI → ownership_token::accept_authority_transfer. Futarchy config PDA signs as new_authority. OT governance authority now = Futarchy config PDA. Emits OtGovernanceClaimed.

Authority Transfer

Two-step governance authority handoff. Standard pattern across all contracts.
Step 1: Current authority proposes a new authority.
ParameterTypeDescription
new_authorityPubkeyProposed new authority
Caller: Current authorityAccounts:
  • authority (signer) — must match config.authority
  • config (mut)
Validation: new_authority ≠ current authority (no self-transfer)Effect: Sets config.pending_authority = Some(new_authority). Emits AuthorityTransferProposed.
Calling again overwrites any existing pending_authority. The previous proposed authority loses their ability to accept.
Step 2: Proposed authority accepts.Caller: The proposed new authority (must sign)Accounts:
  • new_authority (signer) — must match config.pending_authority
  • config (mut)
Validation: signer == config.pending_authorityEffect: Sets authority = new_authority, clears pending_authority. Emits AuthorityTransferAccepted.
This changes who controls the Futarchy instance — NOT the OT governance authority. OT governance authority remains = Futarchy config PDA regardless of who controls Futarchy.

State Accounts

FutarchyConfig

Per-OT governance configuration. One per OT project.
FieldTypeDescription
ot_mintPubkeyThe OT this governance serves
authorityPubkeyCurrent governance authority (team wallet V1, market PDA V2)
pending_authorityOption<Pubkey>Pending authority transfer target
next_proposal_idu64Auto-incrementing proposal counter (also = total proposals created)
is_activeboolGovernance active flag
bumpu8PDA bump seed
PDA Seed: ["futarchy_config", ot_mint]
FutarchyConfig PDA is set as ot_governance.authority in the OT contract. This is how Futarchy controls OT operations — by signing as the OT governance authority via CPI.

Proposal

Individual governance proposal. Created per action.
FieldTypeDescription
proposal_idu64Unique ID within this Futarchy instance
ot_mintPubkeyThe OT project
proposerPubkeyWho created this proposal
proposal_typeProposalTypeMintOt, SpendTreasury, or UpdateDestinations
amountu64Amount for mint/spend (context-dependent)
destinationPubkeyRecipient for mint/spend
token_mintPubkeyToken mint for SpendTreasury
params_hash[u8; 32]Hash of serialized params (for UpdateDestinations)
statusProposalStatusActive, Approved, Executed, Cancelled
created_tsi64Creation timestamp
executed_tsi64Execution timestamp (0 if not executed)
bumpu8PDA bump seed
PDA Seed: ["proposal", futarchy_config, proposal_id] ProposalType enum:
  • MintOt — mint new OT tokens
  • SpendTreasury — spend from OT Treasury
  • UpdateDestinations — change revenue split
ProposalStatus enum:
  • Active — created, awaiting approval
  • Approved — approved, ready for execution
  • Executed — executed successfully
  • Cancelled — cancelled by authority via cancel_proposal (V1), or market resolves against (V2)

PDA Seeds

AccountSeedsDescription
FutarchyConfig"futarchy_config", ot_mintPer-OT governance config
Proposal"proposal", futarchy_config, proposal_idIndividual proposal

Constants

ConstantValueDescription
OT_PROGRAM_IDhardcodedOwnership Token program ID (validated in execute_proposal)

Events

EventFieldsWhen
FutarchyInitializedot_mint, authority, timestampGovernance created
ProposalCreatedproposal_id, ot_mint, proposer, proposal_type, amount, destination, timestampProposal created
ProposalApprovedproposal_id, approver, timestampProposal approved
ProposalCancelledproposal_id, authority, timestampProposal cancelled
ProposalExecutedproposal_id, proposal_type, executor, timestampProposal executed via CPI
OtGovernanceClaimedot_mint, futarchy_config, timestampOT governance authority accepted
AuthorityTransferProposedcurrent_authority, pending_authority, timestampTransfer proposed
AuthorityTransferAcceptedold_authority, new_authority, timestampTransfer accepted

Error Codes

ErrorDescription
UnauthorizedSigner is not the authority
GovernancePausedConfig is_active = false
ProposalNotActiveProposal status ≠ Active (for approve)
ProposalNotApprovedProposal status ≠ Approved (for execute)
AlreadyExecutedProposal already executed
ProposalCancelledProposal was cancelled (for execute)
InvalidProposalTypeUnknown proposal type
MathOverflowArithmetic overflow
SelfTransferCannot transfer authority to yourself
NoPendingAuthorityNo pending transfer
InvalidPendingAuthoritySigner ≠ pending_authority

Architecture & Integration Guide

Cross-Program Integration

  • Futarchy config PDA = ot_governance.authority in OT contract
  • execute_proposal CPIs to OT: mint_ot, spend_treasury, batch_update_destinations
  • Config PDA signs as OT governance authority
  • OT_PROGRAM_ID hardcoded and validated

How Futarchy Connects to OT

FutarchyConfig PDA ["futarchy_config", ot_mint]

  │ is set as authority in:

OtGovernance PDA ["ot_governance", ot_mint]

  │ controls:

OT Contract: mint_ot, spend_treasury, batch_update_destinations
The Futarchy config PDA IS the OT governance authority. When execute_proposal CPIs to OT, the Futarchy config PDA signs via seeds — OT contract validates signer == ot_governance.authority. ✓

Authority Model

🔧 Program Upgrade

Team Multisig (Squads)Can deploy new contract versions.

🏛️ Governance Authority

Team wallet (V1) / Prediction market (V2)
  • create_proposal
  • approve_proposal
  • propose_authority_transfer
In V1, team creates + approves. In V2, holders create, markets resolve.
execute_proposal is permissionless — once approved, anyone can trigger execution. This ensures proposals cannot be blocked after approval.

What Team Controls Directly (NOT via Futarchy)

These protocol operations are managed by team wallets directly, NOT through Futarchy proposals:
  • DEX: config, pool creators, rebalancer — via dex_config.authority (Team Multisig)
  • RWT Engine: manager, distribution config, admin mint, capital adjustments — via vault.authority (Team Multisig)
  • Yield Distribution: config, publish authority, distributors — via config.authority (Team Multisig)
  • Liquidity Nexus: managed directly by team (Nexus manager wallet)
  • Pool Rebalancer: config — via config.authority (Team Multisig)
  • Emergency pause: all contracts — via pause_authority (Team Multisig)
Futarchy governs per-project OT decisions (mint, spend, split). Protocol-level infrastructure is managed by the team until decentralized governance (V2) is mature enough to handle it.

Deployment Checklist

  1. Deploy OT contract and initialize_ot for the project
  2. Call initialize_futarchy for the OT
  3. Transfer OT governance to Futarchy config PDA:
    • OT: propose_authority_transfer(futarchy_config_pda) (deployer signs)
    • Futarchy: claim_ot_governance (permissionless — Futarchy config PDA accepts via CPI)
  4. Transfer Futarchy authority to Team Multisig via propose_authority_transfer + accept_authority_transfer
Step 3 must complete before deployer loses access. After claim_ot_governance, only Futarchy proposals can mint/spend/update destinations.

Token Flow Summary

FromToMechanismWho triggers
OT mint → recipientexecute_proposal (MintOt CPI)Anyone (permissionless)
OT Treasurydestinationexecute_proposal (SpendTreasury CPI)Anyone (permissionless)
OT RevenueConfigexecute_proposal (UpdateDestinations CPI)Anyone (permissionless)