Protocol Mechanics

What this page is for: the on-chain mechanics behind every rFlow deal — the lifecycle, the settlement math, the oracle, the penalty curve, and how fees flow.

Overview#

rFlow is a yield discounting protocol. It separates a yield-bearing position into two pieces — a principal you still own and a yield stream you can sell — and gives you a trustless market to trade the second one.

A deal is one Anchor PDA. It records who locked which tokens, how much USDC was paid for the future yield, when the contract expires, and (critically) the exchange rate at lock so the program can settle fairly. There is no AMM, no yield-token IOU, and no need for the counterparties to interact again — the program settles permissionlessly once ends_at passes.

Custody

Receipt tokens (or LP NFTs) lock in a PDA vault. The program is the only signer.

Pricing

Sellers set the price in USDC. Buyers either fill or skip — no curve, no slippage.

Settlement

Pyth feeds (mainnet LSTs) anchor settlement values. Anti-manipulation tolerances are enforced by the program.

How It Works#

A single deal moves through four moments. Each one is one Anchor instruction:

1. Lock

Seller calls create_deal. Receipt tokens move into a vault PDA; the deal account is initialized with the price and duration.

2. Fill

Buyer calls buy_deal. USDC flows to the seller (minus a 2% protocol fee) and the deal becomes Active.

3. Accrue

The position keeps earning yield in the vault for duration_days. Nothing on-chain needs to happen.

4. Settle

After ends_at, anyone calls settle_deal. Yield tokens go to the buyer, principal tokens go to the seller.

Concrete example
Alice locks 100 mSOL when the mSOL/SOL exchange rate is 1.180 — a principal value of 118 SOL. She lists the next 90 days of yield for $80 USDC. Bob buys. 90 days later, the rate is 1.193 and Pyth reports the oracle value as 119.3 SOL. The program splits the 100 mSOL: ~1.09 mSOL to Bob (the appreciation), ~98.91 mSOL back to Alice (her principal).

Deal Lifecycle#

A deal account moves through one of three terminal states. The status is stored on the deal PDA and updated by the program:

StatusTriggerOutcome
Createdcreate_dealTokens locked, deal open to any buyer
Activebuy_dealBuyer paid; purchased_at & ends_at stamped
Settledsettle_deal (permissionless after ends_at)Yield to buyer, principal to seller, vault closed
Cancelledcancel_deal (seller only, while Created)Tokens returned to seller, deal closed
BoughtBackbuyback_deal (seller only, while Active)Seller refunds buyer + accrued yield + penalty
Why exchangeRateAtLock is stored
The deal account snapshots exchange_rate_at_lock at creation (principal_value_at_lock / receipt_tokens_amount, scaled by 1e6). Settlement uses this to bound the accepted current value within ±10% — a defense against feeding a manipulated value into settle_deal on chains where the oracle is bypassed.

Settlement Math#

At settlement, the program needs to know the current value of the locked tokens. For mainnet LSTs that's a Pyth feed; for devnet or unsupported tokens it's a caller-supplied value bounded by exchange_rate_at_lock.

settle_deal.rs (simplified)
1// 1. Get the current value (oracle or param)
2let current_token_value = if use_oracle {
3 let price_data = price_update.get_price_no_older_than(...)?;
4 calculate_oracle_value(receipt_tokens_amount, price_data.price, ...)
5} else {
6 current_token_value_param // bounded ±10% by exchange_rate_at_lock
7};
8
9// 2. Yield is the appreciation since lock
10let actual_yield = current_token_value
11 .saturating_sub(deal.principal_value_at_lock);
12
13// 3. Split the receipt tokens proportionally
14let yield_tokens = receipt_tokens_amount as u128
15 * actual_yield as u128
16 / current_token_value as u128;
17let principal_tokens = receipt_tokens_amount.saturating_sub(yield_tokens);
18
19// 4. Distribute
20token::transfer(vault -> buyer, yield_tokens)?; // buyer earns the yield
21token::transfer(vault -> seller, principal_tokens)?; // seller gets principal back

The buyer receives receipt tokens denominated in the same mint the seller locked — not USDC. They can keep holding (the position keeps earning) or redeem at the source protocol.

Meteora LP deals settle differently
Meteora deals lock a Position NFT, not a fungible token. The buyer claims fees during the active deal via claim_meteora_fees; at ends_at the NFT returns to the seller via settle_meteora_lp_deal. See the forward marketplace page.

Oracle Role#

rFlow has Pyth integration via pyth-solana-receiver-sdk, but the current mainnet alpha has use_oracle = false. For now, regular LST deals settle the expected-yield model: the caller passes the current value parameter and the program enforces a tolerance band against the stored exchange_rate_at_lock.

Future oracle path

  • Pyth feed must be no older than 60 seconds
  • Price must be > 0
  • Value computed with feed exponent & mint decimals
  • No caller value accepted for supported LSTs

Devnet / non-LST path

  • Caller supplies current_token_value
  • Must fall within ±10% of expected value
  • Expected value derived from exchange_rate_at_lock
  • Rejects with InvalidTokenValue otherwise

The Pyth feed IDs for currently-supported LSTs are listed on the Receipt Tokens page.

Early Buyback Penalty#

A seller can exit an active deal early by calling buyback_deal. They refund the buyer the original selling price, plus the yield accumulated so far, plus a penalty. The penalty decays linearly from base_penalty_bps at purchased_at down to min_penalty_bps at ends_at.

buyback_deal.rs (penalty curve)
1// Defaults from constants.rs (admin-tunable up to MAX_BASE_PENALTY_BPS)
2// DEFAULT_BASE_PENALTY_BPS = 300 // 3%
3// DEFAULT_MIN_PENALTY_BPS = 100 // 1%
4
5let progress_bps =
6 (time_elapsed * BPS_DENOMINATOR / total_duration) as u16; // 0..10_000
7
8let penalty_range = base_penalty_bps - min_penalty_bps;
9let penalty_reduction = penalty_range * progress_bps / BPS_DENOMINATOR;
10let current_penalty_bps = base_penalty_bps - penalty_reduction;
11
12let penalty_amount = selling_price * current_penalty_bps / BPS_DENOMINATOR;
13let total_buyback = selling_price + actual_yield + penalty_amount;
The penalty protects the buyer
The selling price plus accrued yield is what the buyer would have earned. The penalty is the extra premium for breaking the contract early. At the very start of the deal, that's the full base_penalty_bps. As ends_at approaches, the penalty trends toward min_penalty_bps.

Participants#

Yield Sellers

Anyone holding a whitelisted receipt token or a Meteora LP NFT.

  • Instant USDC for future yield
  • Principal stays staked or lent
  • No unstaking queues, no LP unwind
  • Optional early exit via buyback

Yield Buyers

Anyone with USDC who wants a known payout at a known date.

  • Pay a fixed USDC price upfront
  • Receive yield in receipt tokens at settlement
  • No active management, no liquidations
  • Permissionless settlement after expiry

Supported Yields#

rFlow supports three categories of yield, with more on the roadmap. The exact mints and feed IDs are on the Receipt Tokens page.

TierSourceMechanismStatus
Liquid stakingmSOL, jitoSOL, bSOLExchange rate appreciationLive (Pyth oracle)
LendingkUSDC, cUSDCExchange rate appreciationWhitelist-gated
LP feesMeteora DAMM v2Fee accumulation on Position NFTLive

Fee Structure#

A single 2% protocol fee is charged on the selling price at fill time — paid by the buyer, withheld from the seller's USDC. Both fee_bps and the penalty parameters live in ProtocolConfig and are admin-tunable (capped at 10% fee / 30% base penalty / 15% min penalty by the program).

typescript
1// At buy_deal time
2const FEE_BPS = 200; // default: 2%
3const fee = sellingPrice * FEE_BPS / 10_000;
4const seller = sellingPrice - fee;
5
6token::transfer(buyer -> seller, seller); // seller receives
7token::transfer(buyer -> treasury, fee); // protocol treasury

See Tokenomics for the full fee distribution and the public landing page tokenomics for $rFLOW token mechanics.