Skip to main content
The Pendle integration simulates principal token (PT) and yield token (YT) splitting with a full V2 AMM using Notional-style logit curves for interest rate pricing.

Configuration

PendleConfig

pub struct PendleConfig {
    // Tokens
    pub token_underlying: TokenId,       // Accounting asset (e.g., ETH)
    pub token_sy: TokenId,               // Standardized yield share
    pub token_pt: TokenId,               // Principal token
    pub token_yt: TokenId,               // Yield token
    pub token_lp: TokenId,               // LP token
    
    // Yield accrual
    pub yield_rate_ray_per_tick: u128,   // SY exchange rate growth per tick
    
    // Series timing
    pub series_start_ts: Timestamp,      // Series inception
    pub maturity_ts: Timestamp,          // PT maturity
    
    // V2 AMM params
    pub scalar_root: f64,                // Curve steepness at t=period
    pub fee_root: f64,                   // Liquidity fee root
    pub anchor_init: f64,                // Initial anchor (exchange rate baseline)
    pub amm_fee_bps: u32,                // UI parity (not used in V2 math)
}

Initialization

use ank_protocol_pendle::{Pendle, PendleConfig};
use ank_accounting::TokenId;

let pendle = Pendle::new(PendleConfig {
    token_underlying: TokenId(1), // ETH
    token_sy: TokenId(4),
    token_pt: TokenId(5),
    token_yt: TokenId(6),
    token_lp: TokenId(7),
    yield_rate_ray_per_tick: 1_000_000_000_000_000_000_000, // ~0.0001% per tick
    series_start_ts: 1725000000,
    maturity_ts: 1725000000 + 86400 * 90, // 90 days
    scalar_root: 5.0,
    fee_root: 0.001,
    anchor_init: 1.0,
    amm_fee_bps: 30,
});

Token Flow

Underlying (ETH)
    ↓ wrap_sy
SY (standardized yield)
    ↓ mint_from_sy
PT + YT (1:1 minted)
    ↓ AMM trades
PT ↔ SY (price discovery)
YT ↔ SY (flash legs)

Available Actions

SY Wrapper

Description: Wrap underlying into SY shares.Parameters:
  • amount_underlying (u128): Amount to wrap
Example:
{"kind":"wrap_sy","amount_underlying":"1000000000000000000000"}
Mechanics:
sy_shares = amount_underlying / exchange_rate_ray
Effects:
  • Deducts underlying from wallet
  • Credits SY shares to wallet
Description: Unwrap SY shares to underlying.Parameters:
  • shares (u128): SY shares to unwrap
Example:
{"kind":"unwrap_sy","shares":"1000000000000000000000"}
Mechanics:
amount = shares * exchange_rate_ray

PT/YT Lifecycle

Description: Mint PT + YT from underlying (1:1 in shares).Parameters:
  • amount_underlying (u128): Amount of underlying to lock
Example:
{"kind":"mint","amount_underlying":"1000000000000000000000"}
Mechanics:
sy_shares = amount_underlying / eray
pt_minted = sy_shares
yt_minted = sy_shares
Effects:
  • Deducts underlying from wallet
  • Credits PT and YT to wallet
  • Locks SY + principal in vault
  • Sets YT paid_eray checkpoint
Description: Mint PT + YT from SY shares.Parameters:
  • sy_shares (u128): SY shares to lock
Example:
{"kind":"mint_from_sy","sy_shares":"1000000000000000000000"}
Mechanics: Same as mint but uses pre-wrapped SY.
Description: Claim accrued YT yield.Example:
{"kind":"claim"}
Mechanics:
delta_eray = current_eray - user_paid_eray
accrued = yt_shares * delta_eray
payout = min(accrued, treasury_underlying)
Effects:
  • Credits accrued underlying to wallet
  • Updates user YT checkpoint
Description: Burn YT shares and claim final yield.Parameters:
  • shares (u128): YT shares to redeem
Example:
{"kind":"redeem_yt","shares":"500000000000000000000"}
Effects:
  • Burns YT shares
  • Claims remaining accrued yield
  • Does NOT return principal (PT is separate)
Description: Redeem PT for principal after maturity.Parameters:
  • shares (u128): PT shares to redeem
Example:
{"kind":"redeem_pt","shares":"1000000000000000000000"}
Checks:
  • Must be at or past maturity_ts
Mechanics:
principal_out = (shares * locked_principal) / total_pt_shares
Effects:
  • Burns PT shares
  • Credits underlying principal to wallet

AMM Swaps (V2)

Description: Sell PT for SY (user has PT, wants SY).Parameters:
  • amount_in_pt (u128): PT to sell
  • min_out_sy (u128, optional): Slippage protection
Example:
{
  "kind": "swap_exact_pt_for_sy",
  "amount_in_pt": "1000000000000000000000",
  "min_out_sy": "950000000000000000000"
}
Pricing:
trade_proportion = (reserve_pt + amount_in) / (reserve_pt + reserve_sy)
exchange_rate = (1/scalar(t)) * ln(tp/(1-tp)) + anchor + liquidityFee(t)
sy_out = amount_in_pt / exchange_rate
Effects:
  • Deducts PT from wallet
  • Credits SY to wallet
  • Updates AMM reserves and anchor
Description: Buy PT with SY (user has SY, wants PT).Parameters:
  • amount_in_sy (u128): SY to spend
  • min_out_pt (u128, optional): Slippage protection
Example:
{
  "kind": "swap_exact_sy_for_pt",
  "amount_in_sy": "1000000000000000000000",
  "min_out_pt": "1020000000000000000000"
}
Pricing:
// Buyer gets worse ER (fee subtracted)
exchange_rate = (1/scalar(t)) * ln(tp/(1-tp)) + anchor - liquidityFee(t)
pt_out = amount_in_sy * exchange_rate
Description: Buy YT with SY via flash mint/sell PT.Parameters:
  • amount_in_sy (u128): SY to spend
  • min_out_yt (u128, optional): Slippage protection
Example:
{
  "kind": "swap_exact_sy_for_yt",
  "amount_in_sy": "1000000000000000000000",
  "min_out_yt": "1000000000000000000000"
}
Mechanics:
  1. Flash mint PT+YT from SY
  2. Sell PT for SY in pool
  3. Net SY cost = input - rebate
  4. User receives YT
This is a flash operation; no actual PT/YT mint is recorded in vault state.
Description: Sell YT for SY via flash PT leg.Parameters:
  • amount_in_yt (u128): YT to sell
  • min_out_sy (u128, optional): Slippage protection
Example:
{
  "kind": "swap_exact_yt_for_sy",
  "amount_in_yt": "500000000000000000000",
  "min_out_sy": "480000000000000000000"
}
Mechanics:
  1. Burn user YT
  2. Flash source PT (offset with burned YT)
  3. Sell PT for SY in pool
  4. User receives SY

LP Management

Description: Add liquidity to PT/SY pool.Parameters:
  • pt_in (u128): PT to deposit
  • sy_in (u128): SY to deposit
Example:
{
  "kind": "lp_add",
  "pt_in": "1000000000000000000000",
  "sy_in": "1000000000000000000000"
}
Mechanics:
if total_lp == 0 {
    lp_out = sqrt(pt_in * sy_in)
} else {
    lp_out = min(
        pt_in * total_lp / reserve_pt,
        sy_in * total_lp / reserve_sy
    )
}
Effects:
  • Deducts PT and SY from wallet
  • Credits LP shares to wallet
Description: Remove liquidity from pool.Parameters:
  • lp_shares (u128): LP shares to burn
Example:
{"kind":"lp_remove","lp_shares":"500000000000000000000"}
Mechanics:
pt_out = (reserve_pt * lp_shares) / total_lp
sy_out = (reserve_sy * lp_shares) / total_lp

Admin

Description: Update SY yield rate.Parameters:
  • yield_rate_ray_per_tick (u128)
Example:
{"kind":"set_rate","yield_rate_ray_per_tick":"2000000000000000000000"}
Description: Update maturity timestamp.Parameters:
  • maturity_ts (u128)
Example:
{"kind":"set_maturity","maturity_ts":"1735000000"}
Description: Manually set AMM anchor (admin calibration).Parameters:
  • anchor (f64)
Example:
{"kind":"set_anchor","anchor":1.02}
Description: Update AMM curve parameters.Parameters:
  • scalar_root (f64)
  • fee_root (f64)
Example:
{"kind":"set_roots","scalar_root":6.0,"fee_root":0.002}

V2 AMM Pricing

Logit Curve Formula

// Exchange Rate (PT per SY)
ER(t, p) = (1/scalar(t)) * ln(p/(1-p)) + anchor

where:
  p = proportion = reserve_pt / (reserve_pt + reserve_sy)
  scalar(t) = scalar_root * period / ttm
  ttm = maturity_ts - current_ts
  period = maturity_ts - series_start_ts

Interest Rate

// Annualized implied interest rate
IR_ann(t, p) = (ER(t, p) - 1) * period / ttm

Liquidity Fee

liquidityFee(t) = fee_root * ttm / period

// Seller pays +fee (ER worsens)
// Buyer pays -fee (ER improves)

Anchor Preservation

The anchor updates each tick to preserve the last observed mid IR:
// On tick:
anchor_new = 1 + IR_last * ttm/period - ln(p/(1-p)) / scalar(t)
This prevents discontinuous jumps in price as time passes.

View Methods

view_user

{
  "pt_shares": "1000000000000000000000",
  "yt_shares": "1000000000000000000000",
  "yt_accrued_underlying": "5000000000000000000",
  "yt_paid_eray": "1005000000000000000000000000",
  "lp_shares": "500000000000000000000"
}

view_market

{
  "eray": "1010000000000000000000000000",
  "yield_rate_ray_per_tick": "1000000000000000000000",
  "series_start_ts": 1725000000,
  "maturity_ts": 1732780800,
  "locked_sy_shares": "10000000000000000000000",
  "locked_principal_underlying": "10000000000000000000000",
  "treasury_underlying": "50000000000000000000",
  "amm": {
    "reserve_sy": "5000000000000000000000",
    "reserve_pt": "5000000000000000000000",
    "total_lp": "5000000000000000000000",
    "anchor": "1.015000000",
    "mid_exchange_rate": "1.025000000",
    "mid_ir_ann": "0.105000000",
    "scalar_root": "5.000000000",
    "fee_root": "0.001000000",
    "period_secs": "7776000"
  }
}

Usage Examples

Mint PT/YT and LP

use ank_protocol::{Action, Protocol};
use serde_json::json;

// 1. Wrap underlying → SY
pendle.execute(ts, user, Action::Custom(json!({
    "kind": "wrap_sy",
    "amount_underlying": "10000000000000000000000"
})))?;

// 2. Mint PT+YT from half the SY
pendle.execute(ts, user, Action::Custom(json!({
    "kind": "mint_from_sy",
    "sy_shares": "5000000000000000000000"
})))?;

// 3. Add liquidity with PT and remaining SY
pendle.execute(ts, user, Action::Custom(json!({
    "kind": "lp_add",
    "pt_in": "5000000000000000000000",
    "sy_in": "5000000000000000000000"
})))?;

PT Discount Arbitrage

// Check PT price (ER < 1.0 means PT trades at discount)
let market = pendle.view_market();
let er: f64 = market["amm"]["mid_exchange_rate"].as_str().unwrap().parse()?;

if er < 1.0 {
    // PT is cheap → buy PT with SY
    pendle.execute(ts, user, Action::Custom(json!({
        "kind": "swap_exact_sy_for_pt",
        "amount_in_sy": "1000000000000000000000",
        "min_out_pt": "1020000000000000000000" // expect > 1:1 ratio
    })))?;
    
    // Hold PT until maturity → redeem at par (1:1)
}

YT Farming

// Buy YT when implied yield is high
pendle.execute(ts, user, Action::Custom(json!({
    "kind": "swap_exact_sy_for_yt",
    "amount_in_sy": "1000000000000000000000",
    "min_out_yt": "980000000000000000000"
})))?;

// Claim accrued yield periodically
for tick in 0..100 {
    pendle.on_tick(ts + tick)?;
    
    if tick % 10 == 0 {
        pendle.execute(ts + tick, user, Action::Custom(json!({
            "kind": "claim"
        })))?;
    }
}

// Redeem YT at end
pendle.execute(maturity_ts, user, Action::Custom(json!({
    "kind": "redeem_yt",
    "shares": "1000000000000000000000"
})))?;

Protocol-Specific Behavior

YT Yield Accrual

YT holders earn yield as SY exchange rate grows:
// On tick:
eray_new = eray_old + yield_rate_ray_per_tick
delta = eray_new - user_paid_eray
yt_accrued += yt_shares * delta

// On claim:
paid_out = min(yt_accrued, treasury_underlying)
treasury_underlying -= paid_out
user_paid_eray = eray_new
Treasury is funded by yield on locked SY shares.

PT Maturity Redemption

After maturity, PT redeems for pro-rata principal:
principal_per_pt = locked_principal_underlying / total_pt_shares
user_receives = user_pt_shares * principal_per_pt
No yield is paid on PT redemption (yield goes to YT holders).

AMM Anchor Dynamics

Anchor drifts to preserve mid IR as ttm decreases:
t=0:   anchor ≈ 1.00, IR ≈ 5%
t=T/2: anchor ≈ 1.025, IR ≈ 5% (preserved)
t=T:   anchor → infinity, IR → 0 (converges to spot)

Limitations

The Pendle simulation does not model:
  • Multiple maturity series (single series only)
  • Multiple SY sources (e.g., stETH, sDAI)
  • Router aggregation (multi-pool routing)
  • vePENDLE governance (voting, boosted yields)
  • Flash swaps
  • Limit orders
  • Liquidity mining incentives
  • Oracle-based circuit breakers
Use this model to explore PT/YT splits and interest rate curves, not for exact Pendle APY predictions.

See Also

Build docs developers (and LLMs) love