Smart Contracts

MainnetAudit pending

What this page is for: the canonical reference for the rFlow Anchor program — program IDs, PDA seeds, every instruction with its accounts and parameters, and every error code.

Overview#

The rFlow program lives in one Anchor crate programs/payflow and exposes two deal flavors with a shared lifecycle:

  • Yield Deals — fungible receipt tokens (LSTs and lending receipts) that appreciate via an exchange rate
  • Meteora LP Deals — Meteora DAMM v2 Position NFTs whose pool fees can be claimed during the deal

Program information#

Mainnet program2woLsnG7zvKdyd7geH9GAFgKSt6NLrnLDDMmFBUdDjFU
Devnet programFeMxQmzURASxcLu6eEArxiiAGoLuqi7MRaA9Rmff25QL
FrameworkAnchor 0.32.1
LanguageRust 2021
OraclePyth (pyth-solana-receiver-sdk)
Meteora CPIcpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG
SourcerFlowdapp/rflow-sdk·GitHub org
Audit status
The rFlow program is live on mainnet but pending third-party security audit. Treat large deposits with caution until the audit completes.

PDA Seeds#

rFlow uses five PDAs. All seeds are stable across versions and can be derived from the program ID alone:

PDASeedsNotes
protocol_config["protocol_config"]Singleton; holds fee, penalties, mint whitelist, pause flag
yield_deal["yield_deal", deal_id.to_le_bytes()]One per yield deal; deal_id comes from config.deal_counter
vault["vault", deal.key()]Token account holding the locked receipt tokens
meteora_lp_deal["meteora_lp_deal", deal_id.to_le_bytes()]One per Meteora LP deal
meteora_nft_vault["meteora_nft_vault", deal.key()]Token account holding the locked Position NFT

Core Instructions#

Eight instructions cover the receipt-token deal lifecycle. Lock -> fill -> settle, with cancel / buyback / permissionless crank as escape valves.

initialize

Initialize the singleton ProtocolConfig. Sets defaults from constants.rs. Authority-only; runs once.

Accounts
authority (signer, mut)config (PDA, init)treasury (system account)system_program
create_deal

Lock receipt tokens in a vault PDA and initialize a YieldDeal. On mainnet, supported LSTs require a Pyth price_update account; the program validates principal_value_at_lock against the oracle ±10%.

Accounts
seller (signer, mut)config (PDA)deal (PDA, init)seller_token_account (mut)vault (PDA, init)receipt_token_mintpayment_mintprice_update (optional · required for mainnet LSTs)token_programsystem_programrent
Parameters
receipt_tokens_amount:u64
principal_value_at_lock:u64
expected_yield:u64
selling_price:u64
duration_days:u16
source_protocol:SourceProtocol
exchange_rate_at_lock:u64 (scaled 1e6)
buy_deal

Pay the selling price in USDC; protocol withholds the fee (default 2%). Status flips from Created to Active. Stamps purchased_at and computes ends_at.

Accounts
buyer (signer, mut)config (PDA)deal (mut)buyer_payment_account (mut)seller_payment_account (mut)treasury_account (mut)token_program
cancel_deal

Seller-only escape hatch while the deal is still Created. Returns receipt tokens to the seller, closes the vault, refunds rent.

Accounts
seller (signer, mut)deal (mut, close = seller)vault (mut)seller_token_account (mut)token_program
settle_deal

Permissionless after ends_at. Reads current value (Pyth on mainnet LSTs, caller param bounded by ±10% otherwise). Splits receipt tokens — yield to buyer, principal to seller — and closes the vault.

Accounts
payer (signer, mut)seller (mut · receives deal rent)deal (mut, close = seller)vault (mut)buyer_token_account (mut)seller_token_account (mut)config (PDA)receipt_token_mintprice_update (optional · required for mainnet LSTs)token_program
Parameters
current_token_value:u64
buyback_deal

Seller-only early exit while Active. Refunds buyer (selling_price + accrued yield + decaying penalty). Penalty goes from base_penalty_bps at purchase to min_penalty_bps at expiry.

Accounts
seller (signer, mut)config (PDA)deal (mut, close = seller)vault (mut)seller_payment_account (mut)seller_receipt_account (mut)buyer_payment_account (mut)receipt_token_mintprice_update (optional · required for mainnet LSTs)token_program
Parameters
current_token_value:u64
crank_settle

Alias path for permissionless settlement of expired deals — used by keeper bots. Same effect as settle_deal.

Accounts
payer (signer, mut)deal (mut)vault (mut)buyer_token_account (mut)seller_token_account (mut)token_program
Parameters
current_token_value:u64

Meteora LP Instructions#

Seven instructions for DAMM v2 LP deals. The Position NFT is the collateral; fees are the yield.

create_meteora_lp_deal

Lock a Meteora DAMM v2 Position NFT and snapshot accrued fees. Validates that the position belongs to the specified pool.

Accounts
seller (signer, mut)config (PDA)deal (PDA, init)seller_nft_account (mut)nft_vault (PDA, init)position_nft_mintmeteora_positionmeteora_pooltoken_a_minttoken_b_mintpayment_mintnft_token_programsystem_programrent
Parameters
position_account:Pubkey
pool:Pubkey
fee_a_at_lock:u64
fee_b_at_lock:u64
expected_fee_a:u64
expected_fee_b:u64
expected_fee_value_usdc:u64
selling_price:u64
duration_days:u16
buy_meteora_lp_deal

Identical pricing flow to buy_deal. Buyer pays in USDC, protocol fee withheld, deal becomes Active.

Accounts
buyer (signer, mut)config (PDA)deal (mut)buyer_payment_account (mut)seller_payment_account (mut)treasury_account (mut)token_program
claim_meteora_fees

Buyer-only. CPI into Meteora to harvest fees accumulated on the locked position. Fees flow directly to the buyer's token A and token B ATAs.

Accounts
buyer (signer, mut)deal (mut)nft_vaultbuyer_token_a_account (mut)buyer_token_b_account (mut)meteora_programmeteora_positionmeteora_poolpool_token_a_vaultpool_token_b_vaulttoken_a_minttoken_b_minttoken_program
settle_meteora_lp_deal

Permissionless after ends_at. Returns the Position NFT to the seller and closes the deal account.

Accounts
payer (signer, mut)seller (mut · receives deal rent)deal (mut, close = seller)nft_vault (mut)seller_nft_account (mut)position_nft_mintnft_token_program
cancel_meteora_lp_deal

Seller-only while Created. Returns the Position NFT and closes the deal.

Accounts
seller (signer, mut)deal (mut, close = seller)nft_vault (mut)seller_nft_account (mut)position_nft_mintnft_token_program
withdraw_meteora_liquidity

After the deal is settled, the seller can unwind their Meteora position. Forwards token A and token B from the pool to the seller via Meteora CPI.

Accounts
seller (signer, mut)dealseller_nft_accountseller_token_a_account (mut)seller_token_b_account (mut)meteora_programmeteora_positionmeteora_poolpool_token_a_vaultpool_token_b_vaulttoken_a_minttoken_b_mintpool_authorityevent_authoritytoken_a_programtoken_b_program
Parameters
token_a_amount_threshold:u64
token_b_amount_threshold:u64
split_meteora_position

Owner-only helper. Splits an existing Meteora position into two so you can list a portion as an rFlow deal while keeping the rest yourself.

Accounts
owner (signer, mut)meteora_poolsource_positionsource_nft_account (mut)target_positiontarget_nft_account (mut)target_nft_mintmeteora_programevent_authority
Parameters
unlocked_liquidity_percentage:u8
permanent_locked_liquidity_percentage:u8
fee_a_percentage:u8
fee_b_percentage:u8
reward_0_percentage:u8
reward_1_percentage:u8

Admin Instructions#

update_config

Authority-only. Adjust fee, penalty parameters, treasury, allowed mints, and the pause flag. All parameters are Option<T> — pass only what you want to change.

Accounts
authority (signer)config (mut)
Parameters
fee_bps:Option<u16>
base_penalty_bps:Option<u16>
min_penalty_bps:Option<u16>
is_paused:Option<bool>
use_oracle:Option<bool>
new_treasury:Option<Pubkey>
new_authority:Option<Pubkey>
add_mint:Option<Pubkey>
remove_mint:Option<Pubkey>
add_payment_mint:Option<Pubkey>
remove_payment_mint:Option<Pubkey>

State Accounts#

ProtocolConfig

Singleton, seeds: [b"protocol_config"].

state/protocol_config.rs
1pub struct ProtocolConfig {
2 pub authority: Pubkey, // Admin
3 pub treasury: Pubkey, // Fee recipient
4 pub fee_bps: u16, // Default 200 (2%), cap 1000 (10%)
5 pub min_duration_days: u16, // Default 30
6 pub max_duration_days: u16, // Default 365
7 pub base_penalty_bps: u16, // Default 300 (3%), cap 3000 (30%)
8 pub min_penalty_bps: u16, // Default 100 (1%), cap 1500 (15%)
9 pub is_paused: bool, // Emergency pause
10 pub use_oracle: bool, // True on mainnet for LSTs
11 pub deal_counter: u64, // Auto-increment ID
12 pub allowed_mints: Vec<Pubkey>, // Receipt mint whitelist (max 10)
13 pub allowed_payment_mints: Vec<Pubkey>, // Payment mint whitelist (max 10)
14 pub bump: u8,
15}

YieldDeal

One per receipt-token deal. Seeds: [b"yield_deal", deal_id.to_le_bytes()].

state/deal.rs
1pub struct YieldDeal {
2 pub deal_id: u64,
3 pub bump: u8,
4 // Parties
5 pub seller: Pubkey,
6 pub buyer: Pubkey, // Pubkey::default() until filled
7 // Locked collateral
8 pub receipt_token_mint: Pubkey,
9 pub receipt_token_vault: Pubkey,
10 pub receipt_tokens_amount: u64,
11 // Pricing snapshot
12 pub principal_value_at_lock: u64,
13 pub expected_yield: u64,
14 pub selling_price: u64,
15 pub payment_mint: Pubkey,
16 pub exchange_rate_at_lock: u64, // scaled 1e6, anti-manip anchor
17 // Timing
18 pub duration_days: u16,
19 pub created_at: i64,
20 pub purchased_at: i64, // 0 until filled
21 pub ends_at: i64, // 0 until filled
22 // State
23 pub status: DealStatus,
24 pub source_protocol: SourceProtocol,
25}

MeteoraLpDeal

One per Meteora LP deal. Seeds: [b"meteora_lp_deal", deal_id.to_le_bytes()].

state/meteora_lp_deal.rs
1pub struct MeteoraLpDeal {
2 pub deal_id: u64,
3 pub bump: u8,
4 pub seller: Pubkey,
5 pub buyer: Pubkey,
6 // Position
7 pub position_nft_mint: Pubkey,
8 pub position_account: Pubkey,
9 pub position_nft_vault: Pubkey,
10 pub pool: Pubkey,
11 pub token_a_mint: Pubkey,
12 pub token_b_mint: Pubkey,
13 // Snapshots at lock
14 pub fee_a_at_lock: u64,
15 pub fee_b_at_lock: u64,
16 // Projections
17 pub expected_fee_a: u64,
18 pub expected_fee_b: u64,
19 pub expected_fee_value_usdc: u64,
20 // Pricing
21 pub selling_price: u64,
22 pub payment_mint: Pubkey,
23 // Timing
24 pub duration_days: u16,
25 pub created_at: i64,
26 pub purchased_at: i64,
27 pub ends_at: i64,
28 pub status: DealStatus,
29}

Enums#

DealStatus

rust
1pub enum DealStatus {
2 Created, // Listed, waiting for a buyer
3 Active, // Bought, accruing yield
4 Settled, // Expired and distributed
5 Cancelled, // Cancelled by seller before purchase
6 BoughtBack, // Seller exited early via buyback_deal
7}

SourceProtocol

Used by the seller to tag a deal with its yield source. Purely informational — the program does not enforce a specific mint per variant.

rust
1pub enum SourceProtocol {
2 // Phase 1 — lending
3 Kamino, Solend, Save,
4 // Phase 1 — liquid staking
5 Marinade, Jito, Blaze, Sanctum,
6 // Phase 2 — LP positions
7 RaydiumLp, MeteoraLp, OrcaLp,
8 // Phase 3 — fee streams
9 FeeStream,
10}

Error Codes#

CodeNameDescription
6000ProtocolPausedProtocol is paused
6001InvalidDurationDuration outside [min_duration_days, max_duration_days]
6002InvalidPriceselling_price must be greater than zero
6003InvalidAmountreceipt_tokens_amount must be greater than zero
6004DealNotAvailableDeal is not in Created state
6005DealNotActiveDeal is not in Active state
6006DealNotEndedDeal has not yet reached ends_at
6007DealEndedDeal has already expired
6008DealAlreadyActiveDeal already has a buyer
6009NotSellerCaller is not the deal seller
6010NotBuyerCaller is not the deal buyer
6011InsufficientFundsSeller does not have enough USDC for buyback
6012UnauthorizedCaller is not the protocol authority
6013MathOverflowArithmetic overflow
6014InvalidMintReceipt mint not in allowed_mints
6015WhitelistFullCannot add more than 10 mints
6016MintAlreadyWhitelistedMint already present in whitelist
6017MintNotInWhitelistMint not found in whitelist
6018InvalidMeteoraPositionMeteora position account invalid
6019PositionPoolMismatchPosition does not belong to the specified pool
6020InvalidPositionNftPosition NFT supply must be exactly 1
6021NotDealBuyerCaller is not the active deal buyer
6022DealNotSettledDeal has not been settled
6023InvalidSplitPercentageSplit percentage must be 0–100
6024FeeTooHighfee_bps exceeds MAX_FEE_BPS (1000)
6025PenaltyTooHighPenalty exceeds configured maximum
6026InvalidPenaltyRangebase_penalty_bps must be ≥ min_penalty_bps
6027InvalidTokenValuecurrent_token_value outside ±10% tolerance
6028OracleRequiredMainnet LST requires a Pyth price_update account
6029UnsupportedPriceFeedNo Pyth feed registered for this mint
6030StalePriceDataPyth price update older than 60s
6031InvalidOraclePriceOracle returned a non-positive price
6032PriceMismatchprincipal_value_at_lock outside ±10% of oracle value
6033InvalidPaymentMintPayment mint not in allowed_payment_mints
6034InvalidDealDurationComputed total_duration invalid
6035InvalidTreasuryTreasury account must hold lamports
Error codes are best-effort
Codes above are the canonical mapping from programs/payflow/src/errors.rs. If your client sees a higher code, it likely originated in Anchor (AnchorError) or a CPI program (Meteora, Token, Pyth).