Contract Architecture
The Yield Distribution contract handles per-second reward accrual for Ownership Token holders. It implements a no-staking reward model — holders earn rewards simply by holding OTs in their wallets. The contract runs on for real-time, per-second reward calculation and crediting. Each holder gets a dedicated ClaimAccount (sub-account) where rewards accumulate — keeping the holder’s main wallet history clean.How It All Fits Together
AREAL DAO registers the OT project
The AREAL team calls
register_ot_stream, creating a DistributionStream PDA with execution strategy config (DCA parameters, LP period, fees). This is the one-time gatekeeper — without it, the OT cannot distribute.Revenue deposited → automatic trigger
When the project DAO deposits revenue into its
RevenueAccount (via deposit_revenue), the OT contract’s distribute_revenue fires automatically — splitting funds by the pre-configured destinations. For the Holders share, it automatically calls create_distribution on this contract via CPI. No per-distribution governance vote — the DAO only voted once to set up the config.A new isolated DistributionPool is created
create_distribution creates a brand new DistributionPool with a unique cycle_id. This pool is completely independent — its own capital, its own DCA deployment, its own LP position, its own reward timeline. It inherits the stream’s config snapshot at creation time.This isolation model ensures that each distribution is self-contained and auditable. A 300K distribution in April each have their own LP position, their own DCA history, and their own reward schedule — completely traceable from deposit to claim.
On-Chain Accounts
DistributionPool
Represents a single, isolated distribution cycle. Every time an OT project DAO sends funds, a new
DistributionPool is created with its own DCA deployment, LP position, and reward accrual — completely independent from previous cycles. Multiple pools can run in parallel for the same OT. Seeds: ["dist_pool", ot_mint, cycle_id].HolderRegistry
Tracks OT holder balances and holding duration for per-second reward calculation. Updated on every OT transfer event via transfer hook. Stores cumulative balance-seconds for each holder. Delegated to MagicBlock ER. Seeds:
["holder_registry", ot_mint].ClaimAccount
Per-holder sub-account — a single PDA per holder that accumulates rewards from all OT projects. Created once via
init_claim_account with SOL rent payment, then used for every Ownership Token the holder owns. Only holders with an active ClaimAccount receive rewards — no account, no accrual. Seeds: ["claim", holder].DistributionStream
Per-OT configuration PDA — binds an Ownership Token to the distribution system with its own execution strategy config. Created manually by the AREAL DAO team via
register_ot_stream. Stores the OT mint reference, LP deployment period, protocol fee override, and the full execution strategy (DCA interval, max trade size, deployment duration) that controls how funds enter the master pool. Seeds: ["stream", ot_mint].DistributionConfig
Global configuration managed by the AREAL DAO. Stores the default LP deployment period (initially 12 months, adjustable by DAO), AREAL protocol fee (0.25%), master pool reference (RWT/USDY), and minimum distribution amount. All parameters can be updated via
update_distribution_config. Seeds: ["dist_config"].ClaimAccount — the holder’s sub-account
To start receiving rewards, a holder must explicitly create their ClaimAccount once by callinginit_claim_account and paying the SOL rent fee. This typically happens when the user connects their wallet on areal.finance during registration.
One ClaimAccount per holder — for all OT projects. The same sub-account receives rewards from every Ownership Token the holder owns. No need to create separate accounts for each project.
No ClaimAccount = no rewards. The accrue_rewards instruction only processes holders who have an active ClaimAccount. OT holders without one are simply skipped — their share is not lost but not accrued until the account is created.
This model works for both regular wallets and multisig wallets — any Solana address can create a ClaimAccount.
One account, all projects
A single ClaimAccount accumulates rewards from every OT the holder owns. Buy 5 different OTs — all rewards flow into one place. Create once, use forever.
Clean wallet history
Per-second credits happen inside the
ClaimAccount — the holder’s main wallet sees zero transactions until they explicitly claim.Multisig support
Multisig wallets can create ClaimAccounts and claim rewards just like regular wallets. The ClaimAccount PDA is derived from the multisig address.
The SOL rent for a ClaimAccount is the standard Solana account rent (~0.002 SOL). This is a one-time cost — one ClaimAccount covers all OT projects and all distribution cycles, forever.
Core Instructions
register_ot_stream
register_ot_stream
Registers an Ownership Token in the distribution system by creating a
DistributionStream PDA with a full execution strategy configuration. Only the AREAL DAO team can call this — this is the gatekeeper that controls which OT projects can distribute yield through the protocol.Until a stream is registered, the OT contract’s distribute_revenue will fail to forward funds to this contract — there is no self-service onboarding. The AREAL team manually verifies the project, sets the execution strategy parameters, and activates the stream.Authority: Engine Authority (AREAL DAO team)update_stream_config
update_stream_config
Updates the execution strategy configuration for an existing
DistributionStream. The AREAL DAO team can adjust DCA parameters, deployment duration, or LP period as market conditions change. Changes apply to new distribution cycles only — active cycles continue with the config they were created with.Authority: Engine Authority (AREAL DAO team)deregister_ot_stream
deregister_ot_stream
Removes an Ownership Token from the distribution system. After deregistration, the OT contract can no longer create new distribution cycles. Existing active distributions continue until completion — only new ones are blocked.Authority: Engine Authority (AREAL DAO team)
create_distribution
create_distribution
Called automatically by the OT contract’s
distribute_revenue instruction via CPI when a Holders destination is processed. No separate governance vote is needed on the Yield Distribution side — the OT DAO votes once on distribute_revenue, and this instruction fires automatically for the holders’ share.Each call creates a new, isolated DistributionPool — a completely independent distribution cycle with its own DCA deployment, its own LP position, and its own reward accrual timeline. If the OT DAO distributes revenue 3 times — 3 separate pools are created, each running in parallel.Requires a registered DistributionStream — if the OT has not been registered by the AREAL team, this instruction fails.Authority: OT contract (automatic CPI call from distribute_revenue)execute_dca_step
execute_dca_step
Executes a single NAV-aware DCA step. Before each step, the contract reads the current RWT market price from the native DEX and the NAV Book Value from the RWT Vault, then decides the optimal acquisition strategy:
- Market ≥ NAV (at or above peg): mint RWT at NAV Book Value price (cheaper than market) + buy USDY with the other half → deploy to LP
- Market < NAV (below peg): buy RWT from the native DEX market (cheaper than minting, plus provides price support toward NAV). The further below NAV, the larger the RWT share — up to nearly 100% at deep discounts
max_single_trade_bps. Deployment continues until all funds are deployed or dca_deployment_days has elapsed.Authority: Permissionless (crank, runs in Ephemeral Rollup)accrue_rewards
accrue_rewards
The core per-second accrual instruction, executed inside the MagicBlock Ephemeral Rollup by a crank every second. For each active
DistributionPool, it calculates the reward slice for the current second and credits each holder’s ClaimAccount proportionally based on their balance-seconds in the HolderRegistry.Only holders with an active ClaimAccount are included. Holders without a sub-account are skipped — their OT balance still counts in the HolderRegistry, but rewards are not accrued until they create their ClaimAccount via init_claim_account.Authority: Permissionless (crank, runs in Ephemeral Rollup)settle_to_l1
settle_to_l1
Periodically withdraws the accumulated reward portion from the LP position on the native DEX, converts USDY to RWT, and commits the latest
ClaimAccount balances to Solana L1. This bridges the ER accrual state to L1 so holders can claim.Authority: Permissionless (crank)update_holder_balance
update_holder_balance
Called on every OT transfer to update the
HolderRegistry. Records the balance change and updates the cumulative balance-seconds for both sender and receiver. Executes inside the Ephemeral Rollup for per-second accuracy.Authority: OT token program (transfer hook)claim_rewards
claim_rewards
Transfers all accumulated unclaimed RWT rewards from the holder’s
ClaimAccount to their main wallet. Can be called at any time — no lock-up, no minimum claim amount. This is the only instruction that touches the holder’s wallet — all prior accrual happens silently in the sub-account.Authority: Holder (must sign)init_claim_account
init_claim_account
Creates a single
ClaimAccount PDA for a holder that will receive rewards from all OT projects. Must be called by the holder themselves (or their multisig) — this is the one-time opt-in step that activates reward accrual across the entire AREAL ecosystem.Typically triggered during user registration on areal.finance when connecting a wallet. After creation, the holder starts receiving per-second rewards from every OT they hold.Works with both regular wallets and multisig wallets — the ClaimAccount PDA is derived from the holder’s address regardless of wallet type.Authority: Holder (must sign — wallet or multisig)update_distribution_config
update_distribution_config
Updates the global distribution parameters. The AREAL DAO can adjust the default LP deployment period (e.g., change from 12 months to 6 months or 24 months), the protocol fee percentage, the master pool reference, or the minimum distribution amount.The deployment period determines how long distribution funds remain as an LP position in the RWT/USDY master pool — earning swap fees and yield amplification before being distributed to holders.Authority: Engine Authority (AREAL DAO governance)
Distribution Flow
Revenue deposit triggers automatically
When revenue lands in the OT’s
RevenueAccount, distribute_revenue fires automatically. For the Holders share, the OT contract calls create_distribution via CPI — a new isolated DistributionPool is created. No governance vote needed per distribution — the DAO configured the split once. Each deposit = a new independent cycle.DCA deployment phase
Funds are not deployed instantly. The crank executes
execute_dca_step at the stream’s configured interval (e.g., every 60 seconds), gradually buying RWT and USDY on the native DEX and building the LP position over the deployment period (e.g., 30 days). This prevents price shocks on RWT.Yield amplification phase
Once deployment is complete, the full LP position earns additional yield from three sources: swap fees, USDY appreciation, and RWT NAV Book Value growth. This means holders receive more than the original amount.
Holder creates ClaimAccount
The OT holder creates their
ClaimAccount via init_claim_account (typically during registration on areal.finance), paying SOL rent. This activates reward accrual for their address. Works for regular wallets and multisigs.Per-second accrual in ER
Every second,
accrue_rewards runs inside the MagicBlock Ephemeral Rollup. It processes only holders with active ClaimAccounts, calculates each holder’s share based on their balance-seconds, and credits their ClaimAccount — silently, without touching their main wallet.Periodic L1 settlement
settle_to_l1 periodically withdraws from the LP position, converts USDY to RWT, and commits the latest ClaimAccount balances to Solana L1.Stream Execution Strategy
EachDistributionStream has its own execution strategy that controls how funds enter the master pool. This prevents price shocks — if a large project deposits $10M in revenue, buying all the RWT at once would spike the price and disrupt the market.
The problem
How DCA deployment works
Instead of deploying funds into the LP pool immediately, the contract executes a DCA (Dollar-Cost Averaging) strategy — buying RWT and USDY in small portions over a configured deployment period.Funds received
create_distribution receives the revenue amount and stores it in the DistributionPool. No market activity yet.DCA deployment phase
The crank calls
execute_dca_step at the configured interval (e.g., every 60 seconds). Each step buys a small portion of RWT and USDY from the native DEX and adds them to the LP position. Step size is capped by max_single_trade_bps.Deployment complete
After
dca_deployment_days has elapsed (e.g., 30 days), all funds are deployed as an LP position in the RWT/USDY master pool.NAV-Aware Acquisition
Each DCA step doesn’t blindly buy — it compares the RWT market price to the NAV Book Value and chooses the optimal acquisition path:This mechanism creates an automatic stabilizer: when RWT trades below NAV, distribution flows shift toward buying RWT from the market — providing organic buy pressure that pushes the price back toward fair value. The deeper the discount, the stronger the support.
Stream config parameters
| Parameter | Description | Example |
|---|---|---|
dca_interval_secs | Time between each DCA step | 60 (every minute) |
dca_deployment_days | Total duration of the deployment phase | 30 days |
max_single_trade_bps | Max size of a single DCA trade as bps of total | 100 (1% per trade) |
lp_period_days | Total LP deployment period (including DCA phase) | 365 days |
The AREAL DAO team configures these parameters per-stream based on the project size and market conditions. A small project might deploy over 7 days with larger steps, while a large project might spread over 60 days with tiny per-second steps. The config can be updated via
update_stream_config for future cycles.MagicBlock Integration
The Yield Distribution contract runs its core accrual logic on for per-second granularity.Delegated Accounts
| Account | Why delegated |
|---|---|
HolderRegistry | Balance-seconds must update on every OT transfer in real time — requires ER write access |
ClaimAccount (all) | Rewards are credited every second — must be writable in ER for continuous accrual |
DistributionPool | Remaining balance is decremented every second — tracks real-time distribution progress |
Execution Model
Per-second accrual
accrue_rewards runs every second inside the ER. Each holder’s ClaimAccount is credited with their proportional share of the current second’s reward — based on their balance-seconds.Transfer hooks in ER
OT transfers trigger
update_holder_balance inside the ER, ensuring balance-second snapshots are captured at the exact second of transfer.L1 settlement
settle_to_l1 commits ClaimAccount balances to L1, withdraws from LP position, and converts USDY → RWT. After settlement, holders can claim on L1.claim_rewards executes on Solana L1 — it reads the latest committed ClaimAccount balance and transfers RWT to the holder’s wallet. The holder never needs to interact with the Ephemeral Rollup directly.Per-Second Accrual
TheHolderRegistry tracks each holder’s cumulative balance-seconds — the integral of their OT balance over time. This ensures that rewards are distributed proportionally to both the amount held and the duration of holding.
holder_share = holder_balance_seconds / total_balance_secondsEvery OT transfer triggers
update_holder_balance, which snapshots the current balance-seconds before updating. Every second, accrue_rewards reads these snapshots and credits each ClaimAccount proportionally.
This model means a holder who buys 1,000 OT and holds for 30 days earns exactly twice as much as someone who holds 1,000 OT for 15 days — precise, fair, and continuous.
Security Considerations
Gated onboarding
Only OTs registered by the AREAL DAO team (via
register_ot_stream) can create distributions. No self-service — the team verifies each project before enabling yield flows.CPI-only creation
Distribution pools can only be created via CPI from a verified OT contract with a registered
DistributionStream. No external caller can create fraudulent distribution cycles.Proportional fairness
Balance-second tracking ensures late buyers receive proportionally less than long-term holders. No flash-loan or last-minute timing exploits.
Sub-account isolation
Each holder’s
ClaimAccount is a separate PDA. One holder’s claim cannot affect another’s balance. The holder’s main wallet is never written to until they explicitly claim.Permissionless crank
accrue_rewards and settle_to_l1 are permissionless — anyone can run the crank. Distribution continues regardless of who triggers it.ER → L1 consistency
ClaimAccount balances only increase (append-only). The L1 settlement commits a monotonically growing balance — no risk of double-claiming or balance rollback.Clean wallet history
Per-second accrual happens in
ClaimAccount PDAs, not in the holder’s wallet. Zero transaction spam — the holder’s wallet history remains clean until they choose to claim.Opt-in accrual
Only holders who explicitly created a ClaimAccount receive rewards. This filters out dead wallets, reduces crank overhead, and ensures holders consciously opt into the system.