Skip to main content

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

1

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.
2

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.
3

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.
4

Multiple pools run in parallel

If the OT DAO sends funds again next quarter, another pool is created. All pools run independently — each at its own stage of DCA deployment, yield amplification, or reward accrual. They never merge.
This isolation model ensures that each distribution is self-contained and auditable. A 500KdistributioninJanuaryanda500K distribution in January and 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 calling init_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

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)
Accounts:
  - engine_authority:    Signer (AREAL DAO team member)
  - distribution_stream: PDA (init)
  - ot_mint:             OT mint to register
  - distribution_config: PDA
  - system_program

Args:
  - lp_period_days: u32              LP deployment period (e.g., 365)
  - protocol_fee_bps: Option<u16>    Override protocol fee (null = global default)
  - dca_interval_secs: u32           Interval between DCA buys in seconds (e.g., 60 = every minute)
  - dca_deployment_days: u32         Duration over which to deploy funds into LP (e.g., 30 days)
  - max_single_trade_bps: u16        Max single DCA trade as bps of total amount (e.g., 100 = 1%)
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)
Accounts:
  - engine_authority:    Signer (AREAL DAO team member)
  - distribution_stream: PDA (mut)

Args:
  - lp_period_days: Option<u32>
  - dca_interval_secs: Option<u32>
  - dca_deployment_days: Option<u32>
  - max_single_trade_bps: Option<u16>
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)
Accounts:
  - engine_authority:    Signer (AREAL DAO team member)
  - distribution_stream: PDA (mut, close)
  - ot_mint:             OT mint to deregister
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)
Accounts:
  - ot_program:          Calling OT program
  - ot_config:           OT config PDA (verification)
  - distribution_stream: PDA (verification + config source)
  - distribution_pool:   PDA (init)
  - distribution_config: PDA
  - rwt_mint
  - usdy_mint
  - token_program

Args:
  - amount: u64          Total distribution amount (after AREAL fee)
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
The step size is capped by max_single_trade_bps. Deployment continues until all funds are deployed or dca_deployment_days has elapsed.Authority: Permissionless (crank, runs in Ephemeral Rollup)
Accounts:
  - crank:               Signer
  - distribution_pool:   PDA (mut)
  - distribution_stream: PDA (config source)
  - rwt_vault:           PDA (NAV Book Value source)
  - rwt_mint:            RWT mint (for minting path)
  - master_pool:         RWT/USDY pool on native DEX (mut, CPI)
  - lp_position:         LP position account (mut)
  - oracle_price_feed:   MagicBlock oracle (market price)
  - dex_program
  - token_program
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)
Accounts:
  - crank:               Signer
  - distribution_pool:   PDA (mut)
  - holder_registry:     PDA (delegated to ER)
  - claim_accounts:      Active ClaimAccount PDAs only (mut)
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)
Accounts:
  - crank:               Signer
  - distribution_pool:   PDA (mut)
  - master_pool:         RWT/USDY pool (mut, CPI)
  - lp_position:         LP position account (mut, CPI)
  - dex_program
  - token_program
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)
Accounts:
  - ot_mint:             OT mint (verification)
  - holder_registry:     PDA (mut, delegated to ER)
  - from_wallet:         Sender address
  - to_wallet:           Receiver address

Args:
  - from_prev_balance: u64
  - to_prev_balance: u64
  - amount: u64
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)
Accounts:
  - holder:              Signer
  - claim_account:       PDA (mut)
  - holder_rwt_ata:      Holder's RWT token account (mut)
  - rwt_reserve:         Distribution RWT reserve (mut)
  - token_program
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)
Accounts:
  - holder:              Signer (pays SOL rent, wallet or multisig)
  - claim_account:       PDA (init)
  - system_program
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)
Accounts:
  - engine_authority:    Signer (current engine authority)
  - distribution_config: PDA (mut)

Args:
  - default_period_days: Option<u32>   LP deployment period in days (default 365)
  - protocol_fee_bps: Option<u16>      AREAL protocol fee (default 25 = 0.25%)
  - min_distribution: Option<u64>      Minimum distribution amount in USDC

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.

Holder claims

Holders call claim_rewards at any time to move all accumulated RWT from their ClaimAccount to their wallet. One transaction, all rewards, clean history.

Stream Execution Strategy

Each DistributionStream 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.
1

Funds received

create_distribution receives the revenue amount and stores it in the DistributionPool. No market activity yet.
2

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.
3

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.
4

Yield amplification phase

The LP position now earns swap fees and token appreciation for the remainder of the lp_period_days (e.g., 12 months total minus 30 days deployment = 11 months of pure yield).
Each DCA step doesn’t blindly buy — it compares the RWT market price to the NAV Book Value and chooses the optimal acquisition path:
RWT is trading at or above its fair value. Minting is cheaper than buying from the market.
  • 50% of the step amount → mint RWT at NAV Book Value price via the RWT contract
  • 50%buy USDY
  • Deploy both into the master pool LP position
This is the standard 50/50 split. Minting at NAV ensures the contract never overpays.
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

ParameterDescriptionExample
dca_interval_secsTime between each DCA step60 (every minute)
dca_deployment_daysTotal duration of the deployment phase30 days
max_single_trade_bpsMax size of a single DCA trade as bps of total100 (1% per trade)
lp_period_daysTotal 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

AccountWhy delegated
HolderRegistryBalance-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
DistributionPoolRemaining 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

The HolderRegistry 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_seconds
Every 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.
The Yield Distribution contract is currently in development. This documentation describes the target architecture. Contract code has not yet been audited.