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.
py_amount PT and py_amount YT.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.
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:
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).
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)