Smart Contracts
MainnetAudit pendingWhat 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 program | 2woLsnG7zvKdyd7geH9GAFgKSt6NLrnLDDMmFBUdDjFU |
| Devnet program | FeMxQmzURASxcLu6eEArxiiAGoLuqi7MRaA9Rmff25QL |
| Framework | Anchor 0.32.1 |
| Language | Rust 2021 |
| Oracle | Pyth (pyth-solana-receiver-sdk) |
| Meteora CPI | cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG |
| Source | rFlowdapp/rflow-sdk·GitHub org |
Audit status
PDA Seeds#
rFlow uses five PDAs. All seeds are stable across versions and can be derived from the program ID alone:
| PDA | Seeds | Notes |
|---|---|---|
| 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.
initializeInitialize the singleton ProtocolConfig. Sets defaults from constants.rs. Authority-only; runs once.
Accounts
authority (signer, mut)config (PDA, init)treasury (system account)system_programcreate_dealLock 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_programrentParameters
receipt_tokens_amount:u64principal_value_at_lock:u64expected_yield:u64selling_price:u64duration_days:u16source_protocol:SourceProtocolexchange_rate_at_lock:u64 (scaled 1e6)buy_dealPay 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_programcancel_dealSeller-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_programsettle_dealPermissionless 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_programParameters
current_token_value:u64buyback_dealSeller-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_programParameters
current_token_value:u64crank_settleAlias 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_programParameters
current_token_value:u64Meteora LP Instructions#
Seven instructions for DAMM v2 LP deals. The Position NFT is the collateral; fees are the yield.
create_meteora_lp_dealLock 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_programrentParameters
position_account:Pubkeypool:Pubkeyfee_a_at_lock:u64fee_b_at_lock:u64expected_fee_a:u64expected_fee_b:u64expected_fee_value_usdc:u64selling_price:u64duration_days:u16buy_meteora_lp_dealIdentical 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_programclaim_meteora_feesBuyer-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_programsettle_meteora_lp_dealPermissionless 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_programcancel_meteora_lp_dealSeller-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_programwithdraw_meteora_liquidityAfter 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_programParameters
token_a_amount_threshold:u64token_b_amount_threshold:u64split_meteora_positionOwner-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_authorityParameters
unlocked_liquidity_percentage:u8permanent_locked_liquidity_percentage:u8fee_a_percentage:u8fee_b_percentage:u8reward_0_percentage:u8reward_1_percentage:u8Admin Instructions#
update_configAuthority-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"].
1pub struct ProtocolConfig {2 pub authority: Pubkey, // Admin3 pub treasury: Pubkey, // Fee recipient4 pub fee_bps: u16, // Default 200 (2%), cap 1000 (10%)5 pub min_duration_days: u16, // Default 306 pub max_duration_days: u16, // Default 3657 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 pause10 pub use_oracle: bool, // True on mainnet for LSTs11 pub deal_counter: u64, // Auto-increment ID12 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()].
1pub struct YieldDeal {2 pub deal_id: u64,3 pub bump: u8,4 // Parties5 pub seller: Pubkey,6 pub buyer: Pubkey, // Pubkey::default() until filled7 // Locked collateral8 pub receipt_token_mint: Pubkey,9 pub receipt_token_vault: Pubkey,10 pub receipt_tokens_amount: u64,11 // Pricing snapshot12 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 anchor17 // Timing18 pub duration_days: u16,19 pub created_at: i64,20 pub purchased_at: i64, // 0 until filled21 pub ends_at: i64, // 0 until filled22 // State23 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()].
1pub struct MeteoraLpDeal {2 pub deal_id: u64,3 pub bump: u8,4 pub seller: Pubkey,5 pub buyer: Pubkey,6 // Position7 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 lock14 pub fee_a_at_lock: u64,15 pub fee_b_at_lock: u64,16 // Projections17 pub expected_fee_a: u64,18 pub expected_fee_b: u64,19 pub expected_fee_value_usdc: u64,20 // Pricing21 pub selling_price: u64,22 pub payment_mint: Pubkey,23 // Timing24 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
1pub enum DealStatus {2 Created, // Listed, waiting for a buyer3 Active, // Bought, accruing yield4 Settled, // Expired and distributed5 Cancelled, // Cancelled by seller before purchase6 BoughtBack, // Seller exited early via buyback_deal7}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.
1pub enum SourceProtocol {2 // Phase 1 — lending3 Kamino, Solend, Save,4 // Phase 1 — liquid staking5 Marinade, Jito, Blaze, Sanctum,6 // Phase 2 — LP positions7 RaydiumLp, MeteoraLp, OrcaLp,8 // Phase 3 — fee streams9 FeeStream,10}Error Codes#
| Code | Name | Description |
|---|---|---|
| 6000 | ProtocolPaused | Protocol is paused |
| 6001 | InvalidDuration | Duration outside [min_duration_days, max_duration_days] |
| 6002 | InvalidPrice | selling_price must be greater than zero |
| 6003 | InvalidAmount | receipt_tokens_amount must be greater than zero |
| 6004 | DealNotAvailable | Deal is not in Created state |
| 6005 | DealNotActive | Deal is not in Active state |
| 6006 | DealNotEnded | Deal has not yet reached ends_at |
| 6007 | DealEnded | Deal has already expired |
| 6008 | DealAlreadyActive | Deal already has a buyer |
| 6009 | NotSeller | Caller is not the deal seller |
| 6010 | NotBuyer | Caller is not the deal buyer |
| 6011 | InsufficientFunds | Seller does not have enough USDC for buyback |
| 6012 | Unauthorized | Caller is not the protocol authority |
| 6013 | MathOverflow | Arithmetic overflow |
| 6014 | InvalidMint | Receipt mint not in allowed_mints |
| 6015 | WhitelistFull | Cannot add more than 10 mints |
| 6016 | MintAlreadyWhitelisted | Mint already present in whitelist |
| 6017 | MintNotInWhitelist | Mint not found in whitelist |
| 6018 | InvalidMeteoraPosition | Meteora position account invalid |
| 6019 | PositionPoolMismatch | Position does not belong to the specified pool |
| 6020 | InvalidPositionNft | Position NFT supply must be exactly 1 |
| 6021 | NotDealBuyer | Caller is not the active deal buyer |
| 6022 | DealNotSettled | Deal has not been settled |
| 6023 | InvalidSplitPercentage | Split percentage must be 0–100 |
| 6024 | FeeTooHigh | fee_bps exceeds MAX_FEE_BPS (1000) |
| 6025 | PenaltyTooHigh | Penalty exceeds configured maximum |
| 6026 | InvalidPenaltyRange | base_penalty_bps must be ≥ min_penalty_bps |
| 6027 | InvalidTokenValue | current_token_value outside ±10% tolerance |
| 6028 | OracleRequired | Mainnet LST requires a Pyth price_update account |
| 6029 | UnsupportedPriceFeed | No Pyth feed registered for this mint |
| 6030 | StalePriceData | Pyth price update older than 60s |
| 6031 | InvalidOraclePrice | Oracle returned a non-positive price |
| 6032 | PriceMismatch | principal_value_at_lock outside ±10% of oracle value |
| 6033 | InvalidPaymentMint | Payment mint not in allowed_payment_mints |
| 6034 | InvalidDealDuration | Computed total_duration invalid |
| 6035 | InvalidTreasury | Treasury account must hold lamports |
Error codes are best-effort
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).