Ownership Token
✅ Ready to Dev
This contract specification has passed business logic audit, technical review, and cross-program integration check. A developer can implement from this document.
Key Concepts
8 Instructions
Mint, distribute, spend treasury, claim YD for treasury, batch destinations, authority transfer
5 State Accounts
OtConfig, RevenueAccount, RevenueConfig, OtGovernance, OtTreasury
5 PDA Seeds
ot_config, revenue, revenue_config, ot_governance, ot_treasury
Revenue Flow
Revenue Deposit
Asset manager sends USDC to RevenueAccount ATA via standard SPL transfer. No special instruction — anyone can deposit at any time.
Distribution Trigger
When ATA balance ≥ $100 and at least 7 days since last distribution, any crank bot calls
distribute_revenue. Reentrancy guard prevents double execution.Validation: revenue_token_account.amount ≥ min_distribution_amount AND now - last_distribution_ts ≥ 604,800 (7 days in seconds).Protocol Fee
0.25% (25 bps) is deducted first and sent to
areal_fee_destination (static Areal Finance address, set at init).Revenue Split
Remainder distributed proportionally to active destinations:
| Destination | Default BPS | Description |
|---|---|---|
| YD Accumulator | 7,000 (70%) | USDC → YD converts to RWT → merkle streams for OT holders |
| OT Treasury | 2,000 (20%) | Project treasury (multi-token wallet) |
| Liquidity Nexus | 1,000 (10%) | DEX liquidity management (via crank → nexus_deposit) |
Revenue split is configurable per project via
batch_update_destinations. Default: 70% YD holders, 20% Treasury, 10% Liquidity Nexus. Total must always equal 10,000 bps (100%). After initialize_ot, destinations are empty (active_count = 0) — distribute_revenue will fail until batch_update_destinations is called.Instructions
Initialization
initialize_ot
initialize_ot
Register an existing SPL mint as an Ownership Token and create all required PDA accounts. Called once per project. The mint must be created externally beforehand (supports vanity addresses).Pre-requisite: Create the SPL mint externally before calling this instruction. This allows vanity mint addresses (e.g.,
Caller: Deployer (one-time per project)Accounts:
RCPxxxxxx... generated via solana-keygen grind). The mint must have supply == 0 and mint_authority == deployer.Parameters:| Parameter | Type | Description |
|---|---|---|
name | [u8; 32] | Token name (e.g. “Rental Car Pool”), null-padded |
symbol | [u8; 10] | Token symbol (e.g. “RCP”), null-padded |
uri | [u8; 200] | Metadata URI, null-padded |
initial_authority | Pubkey | Initial governance authority |
areal_fee_destination | Pubkey | Where 0.25% protocol fee goes — Areal Finance USDC ATA (static, immutable). Must be a USDC token account, not a wallet address. |
deployer(signer, mut) — current mint_authority of ot_mint, pays for account creationot_mint(mut) — existing SPL mint (vanity address OK)usdc_mint(readonly) — USDC mint address (for creating Revenue ATA)
ot_mint.supply == 0(fresh mint, no tokens issued yet)ot_mint.mint_authority == deployer(deployer controls the mint)ot_mint.freeze_authority == None(no freeze)ot_mint.decimals ≤ MAX_DECIMALS(9) andot_mint.decimals ≥ 1(financial tokens need decimals)nameandsymbolnon-empty
OtConfigPDA — holds token metadata, becomes new mint authority viaset_authorityRevenueAccountPDA — tracks distribution state- Revenue USDC ATA — Associated Token Account for USDC, owned by RevenueAccount PDA. This is where all revenue deposits arrive.
RevenueConfigPDA — stores destination array and protocol fee destinationOtGovernancePDA — stores authority and governance parametersOtTreasuryPDA — project treasury, can hold any tokens (ATAs created externally)
initialize_ot, the instruction calls spl_token::set_authority to transfer mint_authority from deployer → OtConfig PDA. After this, only the OT contract can mint tokens.Vanity address workflow:
solana-keygen grind --starts-with RCP → spl-token create-token RCP_keypair.json --decimals 6 → initialize_ot(ot_mint: RCP_address, ...). Decimals are read from the existing mint — not passed as a parameter.Minting
mint_ot
mint_ot
Mint OT tokens to any recipient wallet.
Caller: Authority (Futarchy PDA)Accounts:
| Parameter | Type | Description |
|---|---|---|
amount | u64 | Number of tokens to mint (in smallest units) |
authority(signer) — must matchot_governance.authorityot_governance— validates authority andis_activeot_config(mut) — incrementstotal_mintedot_mint(mut) — the SPL mintrecipient_token_account(init_if_needed) — recipient’s ATArecipient— any pubkeypayer(signer) — pays for ATA creation if needed
amount > 0
OtMinted event.Revenue
Revenue is deposited by sending USDC directly to the RevenueAccount’s ATA via standard SPL transfer — no special instruction needed. Anyone (asset manager, off-chain service, etc.) can send USDC to this ATA at any time. The ATA is created at init for USDC mint only — it can only hold USDC.distribute_revenue
distribute_revenue
Distribute accumulated USDC revenue to configured destinations. This is a permissionless crank operation — any wallet can trigger it.Caller: Permissionless (crank)Accounts:Logic:
crank(signer) — any walletrevenue_account(mut) — must not be distributing (reentrancy guard)revenue_token_account(mut) — USDC ATA owned by revenue_accountrevenue_config— contains destination array andareal_fee_destinationareal_fee_account(mut) — must matchrevenue_config.areal_fee_destination(validated on-chain)- Remaining accounts: one USDC token account for each of the first
active_countdestinations, in array order. The instruction iterates destinations 0..active_count and matches them 1:1 with remaining accounts — mismatch will fail withDestinationAccountMismatch.
revenue_token_account.amount ≥ min_distribution_amount(default $100, reads ATA balance directly)now - last_distribution_ts ≥ DISTRIBUTION_COOLDOWN(7 days)- Active destination BPS sum = 10,000
is_distributing == false(reentrancy guard)
Solana TX atomicity: If the transaction fails at ANY step, ALL state changes are rolled back — including
is_distributing. Deadlock is impossible on Solana because failed transactions cannot leave partial state. The reentrancy guard protects against same-transaction CPI re-entry only.- Set
is_distributing = true - Read
revenue_token_account.amount(actual USDC balance on ATA) - Calculate protocol fee:
balance × 25 / 10,000(0.25%, ceiling division) - Transfer fee to
areal_fee_account - Distribute remainder proportionally to each active destination
- Last destination gets rounding remainder (dust prevention)
- Update
last_distribution_ts = now, incrementdistribution_count, add tototal_distributed - Set
is_distributing = false - Emit
RevenueDistributedevent
Treasury
The OT Treasury is a multi-token PDA wallet. It can hold USDC, RWT, OT, or any SPL token. Treasury receives USDC automatically fromdistribute_revenue (20% share). Other tokens (RWT, OT, etc.) can be sent by anyone via standard SPL transfer. ATAs are created externally (by the sender) before transferring.
spend_treasury
spend_treasury
Transfer tokens from the treasury to any destination. Governance only.
Caller: Authority (Futarchy PDA)Accounts:
| Parameter | Type | Description |
|---|---|---|
amount | u64 | Token amount to transfer |
authority(signer) — must matchot_governance.authorityot_governance— validates authority andis_activeot_treasury— Treasury PDA (signs the transfer via seeds)treasury_token_account(mut) — source ATA owned by Treasury PDAdestination_token_account(mut) — recipient’s token account (must exist before calling — no init_if_needed)token_mint— mint of the token being transferred
TreasurySpent event.Destination ATA must be created before calling
spend_treasury. If it doesn’t exist, the SPL transfer will fail. The caller (governance proposal executor) is responsible for ensuring the ATA exists.claim_yd_for_treasury
claim_yd_for_treasury
Claim RWT yield from Yield Distribution on behalf of this OT project’s Treasury PDA. Treasury PDA signs the YD claim CPI. Permissionless — any crank can trigger.Primary use case: Non-eligible holders’ (< $100 holdings) yield share from ALL OT projects is allocated to ARL OtTreasury PDA in the merkle tree. The crank calls this instruction on the ARL OT instance to claim that yield as protocol revenue. RWT arrives in ARL Treasury and can be spent by ARL Futarchy governance.Cross-project yield: Any OT Treasury can hold OT tokens of other projects and earn yield from them. If RCP Treasury holds ARL OT worth ≥ $100, RCP Treasury appears as a leaf in the ARL YD merkle tree. Crank calls
Caller: Permissionless (crank)Accounts:
claim_yd_for_treasury on the RCP OT instance, passing ARL YD distributor accounts — RCP OtTreasury PDA signs the claim, and RWT arrives in RCP Treasury. This enables portfolio investments between protocol projects: treasuries earn yield on cross-project OT holdings, governed by each project’s Futarchy.| Parameter | Type | Description |
|---|---|---|
cumulative_amount | u64 | Treasury’s cumulative share (from merkle leaf) |
proof | Vec<[u8; 32]> | Merkle proof path |
crank(signer, mut) — pays for ClaimStatus init if first claimot_treasury— Treasury PDA, signs YD claim CPI via seedstreasury_rwt_ata(mut) — Treasury’s RWT ATA, constraint:owner == ot_treasury.key()ot_mint— for PDA derivation- YD CPI accounts:
yd_distributor,yd_claim_status,yd_reward_vault(all UncheckedAccount) yield_distribution_program— constraint:key == YD_PROGRAM_IDtoken_program,system_program
- CPI →
yield_distribution::claim(cumulative_amount, proof)with OtTreasury PDA as claimant - RWT arrives in treasury_rwt_ata
- Emit
TreasuryYieldClaimed
ARL Treasury = ARL OtTreasury PDA
["ot_treasury", arl_ot_mint]. This is the same PDA created by initialize_ot for the ARL project. Non-eligible yield from all OT projects is protocol revenue that accumulates in ARL Treasury. ARL Futarchy governance controls spending via spend_treasury.Destination Management
batch_update_destinations
batch_update_destinations
Atomically replace the entire revenue destination configuration. Clears all existing destinations and writes new ones. This is the only instruction for managing destinations — no separate add/update/remove.
Each
Caller: Authority (Futarchy PDA)Accounts:
| Parameter | Type | Description |
|---|---|---|
destinations | Vec<BatchDestination> | New complete destination set (1-10 entries) |
BatchDestination:| Field | Type | Description |
|---|---|---|
address | Pubkey | Target USDC token account |
allocation_bps | u16 | Allocation in basis points (1-10,000) |
label | [u8; 32] | Human-readable label |
authority(signer) — must matchot_governance.authorityot_governance— validates authority andis_activerevenue_config(mut) — destination array is overwrittenot_mint— for PDA derivation
- 1 ≤ destinations.len() ≤ 10
- Each allocation_bps ∈ [1, 10,000]
- Each address ≠
Pubkey::default()(no zero address) - Each address ≠
areal_fee_destination(prevent fee destination collision) - No duplicate addresses
- Total BPS = 10,000
active_count and config_version. Emits DestinationConfigUpdated.CPI from Futarchy: When called via
execute_proposal(UpdateDestinations), Futarchy passes destinations through remaining_accounts (Borsh-serialized Vec<BatchDestination>). The OT instruction deserializes remaining_accounts data into Vec<BatchDestination> and applies normally. Futarchy verifies params_hash == sha256(borsh_serialize(destinations)) before CPI to ensure executor passes the exact destinations that were proposed.Authority Transfer
Two-step governance authority handoff. Prevents accidental transfers.propose_authority_transfer
propose_authority_transfer
accept_authority_transfer
accept_authority_transfer
State Accounts
OtConfig
Core token configuration. Acts as mint authority for the OT SPL Mint.| Field | Type | Description |
|---|---|---|
ot_mint | Pubkey | SPL token mint address |
name | [u8; 32] | Token name, null-padded |
symbol | [u8; 10] | Token symbol, null-padded |
decimals | u8 | Token decimals |
total_minted | u64 | Tokens minted so far |
uri | [u8; 200] | Metadata URI, null-padded |
bump | u8 | PDA bump seed |
["ot_config", ot_mint]
RevenueAccount
Owns the USDC ATA where revenue accumulates. Balance is read directly from the ATA — no state tracking of deposits needed.| Field | Type | Description |
|---|---|---|
ot_mint | Pubkey | The OT this belongs to |
total_distributed | u64 | Lifetime USDC distributed |
distribution_count | u64 | Number of distributions executed |
last_distribution_ts | i64 | Unix timestamp of last distribution |
min_distribution_amount | u64 | Minimum ATA balance to trigger distribution (default: 100_000_000 = $100) |
is_distributing | bool | Reentrancy guard flag |
bump | u8 | PDA bump seed |
["revenue", ot_mint]
RevenueConfig
Stores up to 10 revenue destinations and the protocol fee destination.| Field | Type | Description |
|---|---|---|
ot_mint | Pubkey | The OT this belongs to |
destinations | [RevenueDestination; 10] | Fixed-size destination array |
active_count | u8 | Number of active destinations |
config_version | u64 | Incremented on every config change |
areal_fee_destination | Pubkey | Where 0.25% protocol fee is sent |
bump | u8 | PDA bump seed |
| Field | Type | Description |
|---|---|---|
address | Pubkey | Target USDC token account |
allocation_bps | u16 | Allocation in basis points |
label | [u8; 32] | Human-readable label (for UI/off-chain use) |
active_count are active destinations. Remaining slots are zeroed/unused.
PDA Seed: ["revenue_config", ot_mint]
OtGovernance
Governance authority and parameters. Controls minting and destination management.| Field | Type | Description |
|---|---|---|
ot_mint | Pubkey | The OT this belongs to |
authority | Pubkey | Current governance authority (signer for all governance ops) |
pending_authority | Option<Pubkey> | Pending transfer target (set by propose, cleared by accept) |
is_active | bool | Governance active flag (reserved for future use) |
bump | u8 | PDA bump seed |
["ot_governance", ot_mint]
OtTreasury
Project treasury. A multi-token PDA wallet — can hold USDC, RWT, OT, or any SPL token. Token ATAs are created dynamically (init_if_needed) when tokens are deposited for the first time. The PDA itself signs transfers via seeds.| Field | Type | Description |
|---|---|---|
ot_mint | Pubkey | The OT this treasury belongs to |
bump | u8 | PDA bump seed |
["ot_treasury", ot_mint]
Treasury does NOT store ATA addresses in state. ATAs are derived as standard Associated Token Accounts with the Treasury PDA as owner. Any SPL token can be sent to the Treasury by creating an ATA for that mint.
Cross-project investments: Treasury can hold OT tokens of other projects. If holdings ≥ $100, Treasury appears in the other project’s YD merkle tree and earns RWT yield — claimed via
claim_yd_for_treasury. This enables inter-project portfolio investing, where each project’s Futarchy governance decides which OT positions to take and when to sell via spend_treasury.PDA Seeds
| Account | Seeds | Description |
|---|---|---|
| OtConfig | "ot_config", ot_mint | Token config, mint authority for OT |
| RevenueAccount | "revenue", ot_mint | Revenue accumulator, owns USDC ATA |
| RevenueConfig | "revenue_config", ot_mint | Destination allocations + fee destination |
| OtGovernance | "ot_governance", ot_mint | Governance authority |
| OtTreasury | "ot_treasury", ot_mint | Project treasury, multi-token wallet |
Constants
| Constant | Value | Description |
|---|---|---|
BPS_DENOMINATOR | 10,000 | 100% in basis points |
Areal_PROTOCOL_FEE_BPS | 25 | 0.25% protocol fee on distributions |
MAX_DESTINATIONS | 10 | Maximum revenue destinations per OT |
MIN_DISTRIBUTION_AMOUNT | 100,000,000 | $100 USDC (6 decimals) minimum to distribute |
DISTRIBUTION_COOLDOWN | 604,800 | 7 days in seconds — minimum interval between distributions |
MAX_DECIMALS | 9 | Maximum token decimals |
MAX_NAME_LEN | 32 | Token name max bytes |
MAX_SYMBOL_LEN | 10 | Token symbol max bytes |
MAX_URI_LEN | 200 | Metadata URI max bytes |
YD_PROGRAM_ID | hardcoded | Yield Distribution program ID (validated in claim_yd_for_treasury) |
Events
| Event | Fields | When |
|---|---|---|
OtInitialized | ot_mint, authority, decimals, timestamp | Token created |
OtMinted | ot_mint, recipient, amount, new_total_minted, timestamp | Tokens minted |
RevenueDistributed | ot_mint, total_amount, protocol_fee, distribution_count, num_destinations, timestamp | Revenue distributed |
DestinationConfigUpdated | ot_mint, config_version, active_count, timestamp | Destinations changed |
AuthorityTransferProposed | ot_mint, current_authority, pending_authority, timestamp | Transfer proposed |
AuthorityTransferAccepted | ot_mint, old_authority, new_authority, timestamp | Transfer accepted |
TreasurySpent | ot_mint, token_mint, amount, destination, timestamp | Tokens sent from treasury |
TreasuryYieldClaimed | ot_mint, amount, timestamp | Non-eligible yield claimed for treasury |
Error Codes
| Error | Description |
|---|---|
Unauthorized | Signer is not the governance authority |
ZeroAmount | Amount must be > 0 |
InvalidBpsTotal | Destination allocations don’t sum to 10,000 |
InvalidAllocationBps | BPS not in range 1-10,000 |
DuplicateDestination | Same address used twice |
EmptyDestinationList | Batch update with empty list |
TooManyDestinations | More than 10 destinations |
BelowMinDistribution | ATA balance < $100 |
DistributionCooldown | Less than 7 days since last distribution |
DistributionInProgress | Reentrancy — distribution already running |
InsufficientRemainingAccounts | Not enough accounts for all destinations |
DestinationAccountMismatch | Remaining account doesn’t match destination |
MathOverflow | Arithmetic overflow |
InvalidMintSupply | ot_mint.supply ≠ 0 (mint must be fresh) |
InvalidMintAuthority | ot_mint.mint_authority ≠ deployer |
FreezeAuthoritySet | ot_mint.freeze_authority is not None |
InvalidName | Empty name |
InvalidSymbol | Empty symbol |
NoPendingAuthority | No transfer to accept |
InvalidPendingAuthority | Signer ≠ pending_authority |
AuthorityTransferToSelf | Cannot transfer to yourself |
Architecture & Integration Guide
PDA Account Map
5 PDAs are created byinitialize_ot (SPL Mint is created externally beforehand for vanity address support):
OtConfig
Seed:
["ot_config", ot_mint]Mint authority for the SPL Mint. Stores name, symbol, decimals, total_minted, uri.Controlled by: OtGovernance authority (for minting)RevenueAccount
Seed:
["revenue", ot_mint]Owns the USDC ATA where revenue accumulates. Anyone sends USDC here via SPL transfer. Tracks distribution_count, total_distributed. Has reentrancy guard.Used by: distribute_revenue reads ATA balance directlyRevenueConfig
Seed:
["revenue_config", ot_mint]Array of 10 destination slots (address + allocation_bps + label). Stores areal_fee_destination (static). config_version increments on changes.Controlled by: OtGovernance authorityOtGovernance
Seed:
["ot_governance", ot_mint]Stores authority (Futarchy PDA after bootstrap), pending_authority for 2-step transfer. Controls: mint, spend, destinations.Connected to: Futarchy program (authority = Futarchy config PDA)OtTreasury
Seed:
["ot_treasury", ot_mint]Multi-token PDA wallet. No fixed ATAs — holds any SPL token dynamically. Treasury PDA signs transfers via seeds.Receives: 20% of revenue via distribute. Spends: governance only via spend_treasury. All spend history trackable via TreasurySpent events.Cross-Program Integration
→ Yield Distribution (receives 70% revenue)
→ Yield Distribution (receives 70% revenue)
distribute_revenuesends 70% USDC to YD Accumulator ATA- YD contract converts USDC → RWT (swap on DEX up to NAV price + mint remainder at NAV)
- YD creates merkle streams that distribute RWT to eligible OT holders
- Holders claim RWT proportional to their OT balance via merkle proofs
- Non-eligible holders’ share (< $100 holdings) → ARL OtTreasury PDA leaf in merkle tree (protocol revenue)
claim_yd_for_treasuryon ARL OT instance (permissionless crank) claims that share — ARL OtTreasury PDA signs CPI to YD
→ Futarchy Governance (controls authority)
→ Futarchy Governance (controls authority)
→ RWT Engine (holds OT as backing)
→ RWT Engine (holds OT as backing)
- RWT Vault buys/sells OT through DEX via
vault_swap - OT balance in RWT Vault contributes to RWT NAV (Net Asset Value)
- RWT Vault claims yield from YD streams via
claim_yield
→ Native DEX (OT trading)
→ Native DEX (OT trading)
- OT traded in OT/RWT and OT/USDC pools
- Liquidity Nexus manages LP positions using 10% revenue share
- Pool Rebalancer maintains concentrated liquidity around NAV price
Authority Model
🔧 Program Upgrade
Team Multisig (Squads)Can deploy new contract versions. Cannot change on-chain state directly.
🏛️ Governance Authority
OtGovernance.authority = Futarchy PDA
mint_ot— create new tokensspend_treasury— move treasury fundsbatch_update_destinations— change revenue splitpropose_authority_transfer
🌐 Permissionless
Any wallet / Crank bot
- SPL transfer USDC → Revenue ATA
- SPL transfer tokens → Treasury ATA
distribute_revenue(if ≥ $100)claim_yd_for_treasuryaccept_authority_transfer
Two Areal fee addresses across the protocol. OT and RWT Engine send fees in USDC → Areal Finance USDC ATA. YD and DEX send fees in RWT → Areal Finance RWT ATA. These are two different ATAs owned by the same Areal Finance wallet. Developers must use the correct ATA for each contract.
Deployment Checklist
For a new OT project, the deployer must: Prerequisites: Native DEX (for Nexus PDA address) and Yield Distribution (for Accumulator PDA address) must be deployed and initialized first — their addresses are needed for revenue destinations in step 3. Futarchy must be deployed before step 5 (governance transfer).- Create vanity mint externally via
solana-keygen grind+spl-token create-token - Call
initialize_otwith project params → creates 5 PDAs + Revenue USDC ATA, transfers mint authority to OtConfig PDA - Call
batch_update_destinationsto set revenue split:- 70% → YD accumulator USDC ATA
- 20% → OtTreasury USDC ATA
- 10% → Crank USDC ATA (crank routes to Nexus via
nexus_depositfor principal tracking)
- Mint initial tokens via
mint_otto early investors / deployer for distribution - Transfer governance to Futarchy config PDA via
propose_authority_transfer+accept_authority_transfer
Token Flow Summary
| From | To | Mechanism | Who triggers |
|---|---|---|---|
| External USDC | RevenueAccount ATA | SPL transfer | Asset manager |
| RevenueAccount | YD Accumulator (70%) | distribute_revenue | Crank |
| RevenueAccount | Treasury (20%) | distribute_revenue | Crank |
| RevenueAccount | Crank USDC ATA (10%) → Nexus via nexus_deposit | distribute_revenue + nexus_deposit | Crank |
| RevenueAccount | Areal Fee (0.25%) | distribute_revenue | Crank |
| Treasury | Any destination | spend_treasury | Governance |
| OtConfig (mint authority) | Any recipient | mint_ot | Governance |
| YD reward vault | Treasury RWT ATA | claim_yd_for_treasury (CPI to YD) | Crank |