Contract d5e9038212741f84510ec2ee0b614af4a8b4f8c34632e83f7ec8be4c5e82beb1

โ† Back to Index ๐Ÿ“ฅ Download WASM

Meta

cliver 27.0.0#5a7c5fe76530bf4248477ac812fc757146b98cc4
rssdkver 25.3.1#e50d95af029c83196dd122f0154bac3f1302394b
rsver 1.96.1

Instances

  • CDAQBKP2AAJJPHAHAUDDLNHIWIVTTWAE3VOKPAWLS3LJCIZHHH6D7P7P
  • CDILIYN4IXUL5H7PJ4TW3GLZ2U6LIZYX35SNN4BGYZWPIXFLJZEFMRLP

Interface

fn burn(env: soroban_sdk::Env, from: soroban_sdk::Address, amount: i128)
fn name(env: soroban_sdk::Env) -> soroban_sdk::String
fn admin(env: soroban_sdk::Env) -> soroban_sdk::Address
fn pause(env: soroban_sdk::Env)
fn expiry(env: soroban_sdk::Env) -> u64
fn symbol(env: soroban_sdk::Env) -> soroban_sdk::String
fn approve(
    env: soroban_sdk::Env,
    from: soroban_sdk::Address,
    spender: soroban_sdk::Address,
    amount: i128,
    expiration_ledger: u32,
)
fn balance(env: soroban_sdk::Env, id: soroban_sdk::Address) -> i128

Strip. Pull sr_in SR from from, mint sr_in ร— index of PT and YT to receiver. Returns the PY face minted. Refused at/after expiry (Pendle's notExpired).

fn mint_py(
    env: soroban_sdk::Env,
    from: soroban_sdk::Address,
    receiver: soroban_sdk::Address,
    sr_in: i128,
) -> i128

SR held beyond PT cover. All of this is owed to YT holders โ€” see [Self::sweep_surplus]. Exposed for dashboards, never as a claim on the protocol's behalf.

fn surplus(env: soroban_sdk::Env) -> i128
fn unpause(env: soroban_sdk::Env)

Human-readable semver of the source build (informational; for verifiable identity use [Self::code_hash]).

fn version(env: soroban_sdk::Env) -> soroban_sdk::String
fn decimals(env: soroban_sdk::Env) -> u32
fn pt_token(env: soroban_sdk::Env) -> soroban_sdk::Address

Public read of the index (no writes) โ€” the number every quote above should use.

fn py_index(env: soroban_sdk::Env) -> i128

(sr_held, sr_required, surplus) โ€” the solvency dashboard for this series.

fn solvency(env: soroban_sdk::Env) -> (i128, i128, i128)
fn sr_token(env: soroban_sdk::Env) -> soroban_sdk::Address

The current upgrade delay, seconds. Bounded on chain to [1h, 30d].

fn timelock(env: soroban_sdk::Env) -> u64
fn total_py(env: soroban_sdk::Env) -> i128

Move YT. Settles both parties first โ€” this is the line that makes YT tradeable at all.

fn transfer(
    env: soroban_sdk::Env,
    from: soroban_sdk::Address,
    to: soroban_sdk::Address,
    amount: i128,
)
fn treasury(env: soroban_sdk::Env) -> soroban_sdk::Address
fn allowance(
    env: soroban_sdk::Env,
    from: soroban_sdk::Address,
    spender: soroban_sdk::Address,
) -> i128

Burn on a spender's authority, consuming their allowance.

This must NOT route through [Self::burn]: that calls from.require_auth(), so delegating to it would demand the OWNER's signature as well and make every allowance unusable. Both entry points share [Self::burn_checked], which does the work and no auth, so each caller establishes authority in its own correct way.

fn burn_from(
    env: soroban_sdk::Env,
    spender: soroban_sdk::Address,
    from: soroban_sdk::Address,
    amount: i128,
)

The live deployed WASM hash (32-byte SHA-256) โ€” reflects the running code across upgrades, so anyone can verify what is actually deployed rather than trusting a version string.

fn code_hash(env: soroban_sdk::Env) -> soroban_sdk::BytesN<32>
fn is_paused(env: soroban_sdk::Env) -> bool

Recombine. Burn py_amount of face from from and send the released SR to receiver.

  • Before expiry โ€” burns py_amount PT and py_amount YT.
  • At/after expiry โ€” burns PT only. Matching Pendle's if (!isExpired()) _burn(...): a matured YT carries no principal claim, so demanding it would strand PT holders who sold their YT.

Returns the SR paid out.

fn redeem_py(
    env: soroban_sdk::Env,
    from: soroban_sdk::Address,
    receiver: soroban_sdk::Address,
    py_amount: i128,
) -> i128

The provably-unowed residue: surplus, minus credited claims, minus a conservative upper bound on every claim that could still be unsettled.

fn sweepable(env: soroban_sdk::Env) -> i128

Settle user without paying โ€” Pendle's "accrue, don't pay". Useful before an off-protocol transfer, and as the explicit version of what every balance change does implicitly.

fn checkpoint(env: soroban_sdk::Env, user: soroban_sdk::Address) -> i128

One-shot, admin-gated init.

  • sr โ€” the SR token this series strips. Everything below Blend is its problem, not ours.
  • pt โ€” the PT SAC, which must already be admined by this contract.
  • expiry โ€” unix seconds.
  • yield_fee_bps โ€” protocol share of YT interest, โ‰ค [MAX_YIELD_FEE_BPS].
fn initialize(
    env: soroban_sdk::Env,
    sr: soroban_sdk::Address,
    pt: soroban_sdk::Address,
    expiry: u64,
    yield_fee_bps: u32,
)
fn is_expired(env: soroban_sdk::Env) -> bool

Permissionless TTL keep-alive for a holder's entries.

Covers both the Interest record and the YT balance entry. It used to bump only the former (tofix.md #30): a dormant YT holder's balance is a separate persistent entry with its own TTL, refreshed only when the balance is written, so keeping the interest record alive while letting the balance archive kept exactly the wrong half.

fn bump_holder(env: soroban_sdk::Env, user: soroban_sdk::Address)

A holder's full interest record.

fn interest_of(env: soroban_sdk::Env, user: soroban_sdk::Address) -> UserInterest

Accept a pending admin proposal (step 2 of 2). Callable only by the proposed address.

fn accept_admin(env: soroban_sdk::Env)
fn expiry_index(env: soroban_sdk::Env) -> Option
fn set_timelock(env: soroban_sdk::Env, secs: u64)
fn set_treasury(env: soroban_sdk::Env, treasury: soroban_sdk::Address)
fn total_supply(env: soroban_sdk::Env) -> i128

Bind admin + treasury atomically at deploy.

fn __constructor(
    env: soroban_sdk::Env,
    admin: soroban_sdk::Address,
    treasury: soroban_sdk::Address,
)

Apply a scheduled upgrade. Reverts until eta has passed.

fn apply_upgrade(env: soroban_sdk::Env)

The live PY index โ€” max(SR.exchange_rate(), stored) before expiry, frozen after. Mutating: ratchets the stored index. The swap/mint paths use this.

fn index_current(env: soroban_sdk::Env) -> i128
fn pending_admin(env: soroban_sdk::Env) -> Option

Propose a new admin (step 1 of 2). The proposed address must then call [Self::accept_admin]; until it does, nothing changes.

fn propose_admin(env: soroban_sdk::Env, new_admin: soroban_sdk::Address)
fn set_yield_fee(env: soroban_sdk::Env, bps: u32)

Sweep genuinely-unowed SR to the treasury.

Read this before assuming it is a revenue line โ€” it usually is not

In a share-based design the surplus is not free money. The conservation identity is exact: minting sr_in at index i0 creates face = sr_in ร— i0 and leaves held โˆ’ pt_cover = sr_in โˆ’ sr_in ร— i0/i, which is precisely what YT holders are owed. Every stroop above PT cover belongs to some YT holder โ€” settled or not.

So this can only ever sweep what is provably owed to nobody:

  • rounding remainder from the one-directional floors in the interest math, and
  • claims abandoned by burning YT without withdrawing.

The unsettled part of holders' claims cannot be enumerated on chain, so it is bounded from above: no holder's settlement index can be below [storage::init_index], so the worst-case unsettled total is what the entire YT supply would have earned from that floor. That bound is deliberately loose โ€” a healthy series sweeps โ‰ˆ0, which is the correct answer, not a bug.

fn sweep_surplus(env: soroban_sdk::Env) -> i128
fn total_accrued(env: soroban_sdk::Env) -> i128
fn transfer_from(
    env: soroban_sdk::Env,
    spender: soroban_sdk::Address,
    from: soroban_sdk::Address,
    to: soroban_sdk::Address,
    amount: i128,
)
fn yield_fee_bps(env: soroban_sdk::Env) -> u32
fn cancel_upgrade(env: soroban_sdk::Env)
fn pending_upgrade(env: soroban_sdk::Env) -> Option

The mutating index โ€” the number every value-MOVING path must price on.

[Self::py_index] is a pure view and may lag SR by one sync. That is harmless for a quote and wrong for a trade: srmarket used to price on the view while the mint_py/redeem_py it then called internally used this one, so a single transaction ran on two different indices (FINAL_CHECK.md V2-01). Anything that moves value calls this instead.

Mutating: refreshes SR from the strategy and ratchets the stored index. sr::sync_rate ALWAYS writes, so the footprint stays a function of the call graph alone and cannot depend on timing โ€” the property that the 2026-08-24 testnet footprint failure taught us to keep.

Calling this and then mint_py/redeem_py in the same transaction is consistent, not doubled: the ledger timestamp is fixed for the transaction, so the second sync observes the same b_rate and returns the same index.

fn py_index_current(env: soroban_sdk::Env) -> i128

Schedule an upgrade to wasm_hash, applyable once the timelock elapses. Returns the eta. The pending hash is publicly readable via [Self::pending_upgrade] for the whole window.

fn schedule_upgrade(env: soroban_sdk::Env, wasm_hash: soroban_sdk::BytesN<32>) -> u64

SR user could withdraw right now, gross of the yield fee. Panic-free view; never writes.

fn claimable_interest(env: soroban_sdk::Env, user: soroban_sdk::Address) -> i128

Freeze the index at expiry. Permissionless โ€” anyone may pin it, and the earlier it is pinned the tighter the ceiling. Write-once: a later call can never raise it.

Blend exposes only the current rate with no historical lookup, so the expiry index cannot be reconstructed after the fact โ€” it has to be observed on chain. This is futureamm.md's "Rule A", made explicit and callable rather than left to whoever happens to touch the contract first.

fn stamp_expiry_index(env: soroban_sdk::Env) -> i128

Settle user and pay out their accrued SR, minus the protocol yield fee.

Returns (paid_to_user, fee_to_treasury), both in SR. Callable by anyone on anyone's behalf โ€” it only ever moves value to the holder, so there is nothing to gate. That also makes it safe for a keeper to sweep dust claims.

fn redeem_due_interest(
    env: soroban_sdk::Env,
    user: soroban_sdk::Address,
) -> (i128, i128)

Withdraw a pending proposal. Current admin only.

fn cancel_admin_transfer(env: soroban_sdk::Env)

redeem_due_interest, but paying the holder's SR to receiver instead of to themselves.

This exists so a router can claim on the user's behalf and unwrap the proceeds to USDC in the same transaction. Without it the router would have to claim (paying the user), then pull the SR back โ€” and the pull amount is only known on chain, which is exactly the simulate-vs-execute authorization drift that bit us on testnet (AUDITPREP.md ยง4, item 1).

Why the auth split is what it is

Paying a holder their own yield is safe for anyone to trigger โ€” it only ever moves value to them. Redirecting that payment is not: it moves their value to a third party. So the permissionless case stays permissionless, and only the redirect requires the holder's signature. Note this deliberately checks receiver != user rather than trusting the caller.

fn redeem_due_interest_to(
    env: soroban_sdk::Env,
    user: soroban_sdk::Address,
    receiver: soroban_sdk::Address,
) -> (i128, i128)

Imports

WebAssembly Text (WAT) โ–ถ