Architecture

Perk is an Anchor program on Solana with a TypeScript SDK, off-chain crankers, and a Next.js frontend. All trading logic is on-chain — no off-chain matching, no centralized sequencer.


vAMM Engine

The virtual AMM is a constant-product market maker with no real token reserves. It's pure math that determines execution prices.

Core Formula

base_reserve × quote_reserve = k    (invariant)
mark_price = (quote_reserve × peg_multiplier) / base_reserve

Execution

When a trader goes long (buys base):

new_base    = base_reserve - base_size
new_quote   = k / new_base
quote_cost  = new_quote - quote_reserve
exec_price  = (quote_cost × peg_multiplier) / base_size

The price moves with each trade. Larger trades experience more slippage — this is by design. It naturally limits risk without requiring position size caps.

Key Properties

PropertyDetail
No LPs requiredThe vAMM provides virtual liquidity — anyone can trade immediately
Slippage is naturalLarger trades move the price more, like a real AMM
k determines depthHigher k = less slippage = deeper market
Oracle-peggedpeg_multiplier anchors the vAMM to the oracle price
Self-balancingFunding rates incentivize the minority side

Peg Updates

The update_amm() instruction adjusts peg_multiplier to keep the mark price near the oracle price. Called periodically by the cranker or any user. If the mark price drifts more than 0.5% from the oracle, a peg update is triggered.

Peg updates have a cooldown of 100 slots (~40 seconds) to prevent manipulation.


Risk Engine

The risk engine is a full port of Anatoly Yakovenko's Percolator. It handles three problems autonomously: exit fairness, deficit socialization, and market recovery.

H — Haircut Ratio

When a vault is stressed (total liabilities exceed assets), profitable traders can't all withdraw their full profit. The haircut ratio ensures fair exit:

Residual = max(0, vault_balance - total_deposited_capital - insurance)

            min(Residual, total_matured_profit)
H = ────────────────────────────────────────────
              total_matured_profit
  • H = 1: fully backed, all profit is real
  • H < 1: stressed, profit is scaled down proportionally
  • The haircut mechanism is designed to protect deposited capital — only unrealized profit is subject to haircuts when the vault is stressed

H self-heals: as losses settle or new deposits arrive, it recovers toward 1.

A/K — Overhang Clearing

When a liquidation creates a deficit (position loss > collateral), the A/K mechanism socializes it across all positions on the same side:

effective_position(i) = floor(basis_i × A / a_snapshot_i)
pnl_delta(i) = floor(|basis_i| × (K - k_snapshot_i) / (a_snapshot_i × POS_SCALE))

Key properties:

  • No individual is singled out. Everyone on the affected side absorbs an equal per-unit loss.
  • No ADL (auto-deleveraging). Unlike other protocols, Perk doesn't force-close profitable positions.
  • O(1) per account. Settlement is lazy — each account computes its share on next interaction.

Three-Phase Recovery

When cascading liquidations stress one side of the market, recovery is fully autonomous:

  1. DrainOnly — A drops below threshold. No new positions on that side. Existing positions can only close.
  2. ResetPending — Open interest reaches zero. State is snapshotted, A resets, epoch increments.
  3. Normal — Side reopens for trading.

Recovery is fully autonomous — no admin intervention or governance vote required.

Warmup Window

New profit doesn't mature instantly. It enters reservedPnl and converts to matured profit linearly over warmup_period_slots (default: 1000 slots ≈ 400 seconds).

This blocks a specific attack: manipulate the oracle → open position → claim paper profit → dump. With warmup, the profit is locked long enough for the manipulation to be detected or self-correct.


Account Structure

All state lives on-chain as Anchor accounts.

Protocol (singleton PDA)

Seeds: [b"protocol"]

Global configuration: admin, fee parameters, pause state, market count. One per program deployment.

Market (per token per creator PDA)

Seeds: [b"market", token_mint, creator]

Multiple markets can exist for the same token, each created by a different creator with different parameters. Each market has its own vault, vAMM state, and risk engine. All markets for the same token share the same oracle price feed (Pyth or PerkOracle).

Contains all market state:

  • vAMM reserves (base_reserve, quote_reserve, k, peg_multiplier)
  • Risk engine state (long_a/k_index, short_a/k_index, side states, epochs). Note: haircut_numerator/denominator are legacy fields — the haircut ratio is computed dynamically from vault state
  • Market parameters (max_leverage, trading_fee_bps, maintenance_margin_bps)
  • Oracle configuration (oracle_source, oracle_address, fallback oracle)
  • Funding state (last_funding_time). Note: cumulative_long_funding and cumulative_short_funding are legacy fields — funding is applied through per-slot K-coefficient accumulation
  • Aggregate tracking (total_long_position, total_short_position, c_tot, pnl_pos_tot)
  • Fee accumulators (creator_claimable_fees, protocol_claimable_fees)
  • Insurance fund (insurance_fund_balance, epoch caps, floor)

UserPosition (per user per market PDA)

Seeds: [b"position", market, user]

Per-user position state:

  • Collateral and margin (deposited_collateral)
  • Position (base_size, quote_entry_amount)
  • Risk engine snapshots (basis, a_snapshot, k_snapshot, epoch_snapshot)
  • PnL state (pnl, reserved_pnl, warmup fields)
  • Funding tracking (last_cumulative_funding)
  • Trigger order count (open_trigger_orders, next_order_id)

TriggerOrder (per order PDA)

Seeds: [b"trigger", market, user, order_id]

Order details: type, side, size, trigger price, leverage, reduce_only flag, expiry.

PerkOraclePrice (per token PDA)

Seeds: [b"perk_oracle", token_mint]

Oracle state: price, confidence, timestamp, EMA, source count, staleness config, freeze flag. See PerkOracle.


On-Chain Program

Built with Anchor 0.32.1. Program structure:

programs/perk-protocol/src/
├── lib.rs                    # Instruction dispatch
├── constants.rs              # All protocol constants
├── errors.rs                 # Error codes
├── state/                    # Account definitions
│   ├── protocol.rs
│   ├── market.rs
│   ├── user_position.rs
│   ├── trigger_order.rs
│   └── perk_oracle.rs
├── instructions/             # Instruction handlers
│   ├── initialize_protocol.rs
│   ├── create_market.rs
│   ├── deposit.rs / withdraw.rs
│   ├── open_position.rs / close_position.rs
│   ├── place_trigger_order.rs / cancel_trigger_order.rs / execute_trigger_order.rs
│   ├── liquidate.rs
│   ├── crank_funding.rs
│   ├── update_amm.rs
│   └── admin_*.rs
└── engine/                   # Core math
    ├── vamm.rs               # vAMM constant-product math
    ├── risk.rs               # H, A/K, three-phase recovery
    ├── funding.rs            # Funding rate calculation
    ├── margin.rs             # Margin requirements
    ├── liquidation.rs        # Liquidation logic
    ├── oracle.rs             # Oracle abstraction + fallback
    ├── warmup.rs             # PnL warmup window
    └── wide_math.rs          # u256/i128 arithmetic

Key Instructions

InstructionPermissionlessDescription
create_marketCreate a market for any SPL token
deposit / withdrawMove collateral in/out of vault
open_positionOpen long or short via vAMM
close_positionClose full or partial position
place_trigger_orderPlace limit/stop/TP order
execute_trigger_orderExecute triggered order (earns fee)
liquidateLiquidate underwater position (earns fee)
crank_fundingUpdate funding rate indices
update_ammRe-peg vAMM to oracle
admin_pause❌ AdminEmergency global pause

Cranker System

Crankers are off-chain bots that call permissionless instructions. Anyone can run them. Perk runs the initial set, but the system is designed for competition.

Cranker Loops

LoopFrequencyWhat It DoesIncentive
FundingEach funding periodCalls crank_funding() on each marketNone (protocol health)
LiquidationFrequentlyScans positions, liquidates underwater ones50% of liquidation fee
Trigger ExecutorNear real-timeChecks trigger conditions, executes orders0.01% of notional
Peg UpdaterPeriodicallyRe-pegs vAMM when mark drifts >0.5% from oracleNone (protocol health)
Oracle (PerkOracle)Near real-timePosts aggregated prices from Jupiter + BirdeyeNone (required for trading)

Architecture

Each cranker is a Node.js process using perk-protocol. The PerkCranker and PerkOracleCranker classes handle the loops:

import { PerkCranker, PerkOracleCranker } from "perk-protocol";

const cranker = new PerkCranker({ connection, wallet, /* ... */ });
const oracleCranker = new PerkOracleCranker({ connection, wallet, /* ... */ });

Crankers hold minimal SOL for transaction fees (~0.5–1 SOL). They have no admin privileges and can't access vault funds. If a cranker goes offline, anyone else can step in — all instructions are permissionless.


Price Scaling

All prices on-chain use a scale of 1e6 (6 decimal places, matching USDC precision).

$150.32 on-chain → 150_320_000
$0.00001832 on-chain → 18

Position sizes use token-native decimals. The SDK provides priceToNumber() and numberToPrice() helpers.


Constants

PRICE_SCALE               = 1_000_000      // 6 decimal price precision
POS_SCALE                 = 1_000_000      // Position scaling
K_SCALE                   = 1e12           // vAMM k precision
CREATOR_FEE_SHARE_BPS     = 1000           // 10%
MIN_TRADING_FEE_BPS       = 3              // 0.03%
MAX_TRADING_FEE_BPS       = 100            // 1%
LIQUIDATION_FEE_BPS       = 100            // 1%
MAINTENANCE_MARGIN_BPS    = 500            // 5%
DEFAULT_FUNDING_PERIOD     = 3600          // 1 hour
FUNDING_RATE_CAP_BPS      = 10             // 0.1% per period
WARMUP_PERIOD_SLOTS       = 1000           // ~400 seconds
MAX_TRIGGER_ORDERS        = 8              // Per user per market
ORACLE_STALENESS_SECONDS  = 15             // Max age for oracle prices
INSURANCE_EPOCH_CAP_BPS   = 3000           // 30% max epoch payout

See constants.rs (on-chain) and constants.ts (SDK) for the full list.