Add PT coupon capacity: pull amount USDC, wrap to SR, strip into PT+YT held by the vault.
Pure capacity — it creates no liability.
Admin-gated. v1's seed was permissionless, which is what turned a capacity limit into
tofix.md #18's denial-of-service: anyone could prepend dust positions until the redeem
walk exceeded the transaction budget and every receipt became unpayable. There is no walk
here, but an open seed still lets a stranger write vault state for the price of a
transaction, and nothing needs it to be open.
fn seed(env: soroban_sdk::Env, from: soroban_sdk::Address, amount: i128) -> i128
fn admin(env: soroban_sdk::Env) -> soroban_sdk::Address
fn pause(env: soroban_sdk::Env)
What amount USDC would earn: (payout, coupon, rate_bps). Read-only.
fn quote(env: soroban_sdk::Env, amount: i128) -> (i128, i128, u32)
fn stats(env: soroban_sdk::Env) -> SrVaultStats
Recover surplus inventory — seed capital and unneeded capacity — to to.
Liability-gated, which is tofix.md #22: v1 had no path at all, so seed capital and any
over-provisioned inventory were one-way. This releases only PT face above every open
payout, so it can never touch a receipt's backing. Admin only.
fn sweep(env: soroban_sdk::Env, to: soroban_sdk::Address, pt_amount: i128) -> i128
Redeem a matured receipt for its full promised payout.
O(1) — and that is the headline difference from v1. v1 walked a list of wrapper
positions, redeeming each until the payout was met, at ~7 MB per position; five positions
exhausted the mainnet transaction budget and a stranger could inflate the list at will
(tofix.md #18). Here PT is fungible bearer, so the vault burns payout PT face in one
redeem_py call. Cost does not depend on history, on how the inventory was assembled, or
on anything an attacker controls.
fn redeem(env: soroban_sdk::Env, receipt_id: u64) -> i128
Lock a fixed rate. Pulls amount USDC, mints matching PT+YT into inventory, and issues a
receipt promising principal + coupon at maturity.
The coupon is backed out of existing capacity: the deposit itself only creates PT equal to the principal, so the coupon must come from spare inventory. That is why the capacity check is the load-bearing line — it is what makes the promise solvent rather than hopeful.
fn deposit(env: soroban_sdk::Env, user: soroban_sdk::Address, amount: i128) -> u64
Claim the vault's YT yield and reinvest it as fresh PT capacity.
No maturity gate and no pruning, which is tofix.md #21. v1's harvest refused to run
after maturity and pruned positions that still held live YT, so yield accrued before
maturity became permanently unclaimable. Here the engine keeps pre-expiry yield claimable
forever, and this simply collects it whenever it is called.
Returns (sr_claimed, py_minted). Reinvestment is skipped past expiry (the engine refuses
mint_py then, correctly) — the claimed SR stays as vault inventory instead of reverting
and throwing the claim away with it.
fn harvest(env: soroban_sdk::Env) -> (i128, i128)
What [Self::sweep_surplus] would release right now, without moving anything.
fn surplus(env: soroban_sdk::Env) -> (i128, i128, i128)
fn unpause(env: soroban_sdk::Env)
fn version(env: soroban_sdk::Env) -> soroban_sdk::String
fn maturity(env: soroban_sdk::Env) -> u64
fn pt_token(env: soroban_sdk::Env) -> soroban_sdk::Address
fn rate_bps(env: soroban_sdk::Env) -> u32
fn set_rate(env: soroban_sdk::Env, rate_bps: u32)
fn sr_token(env: soroban_sdk::Env) -> soroban_sdk::Address
fn timelock(env: soroban_sdk::Env) -> u64
fn code_hash(env: soroban_sdk::Env) -> soroban_sdk::BytesN<32>
fn is_paused(env: soroban_sdk::Env) -> bool
One-shot, admin-gated init.
Takes only the engine address. sr, pt, underlying and maturity are read back from it, so
the tofix.md #24 class of mismatch — a vault wired to an asset its PT does not redeem into
— cannot be constructed here.
fn initialize(
env: soroban_sdk::Env,
yield_contract: soroban_sdk::Address,
rate_bps: u32,
max_rate_bps: u32,
)
fn underlying(env: soroban_sdk::Env) -> soroban_sdk::Address
fn get_receipt(env: soroban_sdk::Env, receipt_id: u64) -> Receipt
fn accept_admin(env: soroban_sdk::Env)
Permissionless TTL keep-alive so a long-dated receipt cannot archive before it matures.
fn bump_receipt(env: soroban_sdk::Env, receipt_id: u64)
fn max_rate_bps(env: soroban_sdk::Env) -> u32
fn set_timelock(env: soroban_sdk::Env, secs: u64)
fn __constructor(env: soroban_sdk::Env, admin: soroban_sdk::Address)
fn apply_upgrade(env: soroban_sdk::Env)
fn pending_admin(env: soroban_sdk::Env) -> Option
fn propose_admin(env: soroban_sdk::Env, new_admin: soroban_sdk::Address)
Recover surplus SR, YT and USDC to to, at or after expiry (tofix.md #22).
sweep handles the PT leg. This handles everything else the vault ends up holding, which
the PT-only sweep left permanently stranded — measured at 248.53 SR on a 20,000 USDC
seed, about 1.2% of it, plus the USDC flooring remainder.
sweep is notPT face is directly comparable to a payout, so surplus PT can be identified — and released — at any time. The other three legs cannot:
assert_solvent cannot see, because that invariant compares PT face
against liability and says nothing about future capacity. A pre-expiry YT sweep would
quietly degrade the vault's ability to meet later payouts while every check still passed.harvest reinvests it in the
same call. It only accumulates after expiry, when mint_py refusfn sweep_surplus(env: soroban_sdk::Env, to: soroban_sdk::Address) -> (i128, i128, i128)
Flooring loss realized by partial redemptions across open receipts (anyfix.md F2).
fn total_residue(env: soroban_sdk::Env) -> i128
fn cancel_upgrade(env: soroban_sdk::Env)
fn yield_contract(env: soroban_sdk::Env) -> soroban_sdk::Address
fn pending_upgrade(env: soroban_sdk::Env) -> Option
fn total_liability(env: soroban_sdk::Env) -> i128
How much more USDC a receipt still needs before it can be paid. 0 = ready (or closed).
fn redeem_remaining(env: soroban_sdk::Env, receipt_id: u64) -> i128
fn schedule_upgrade(env: soroban_sdk::Env, wasm_hash: soroban_sdk::BytesN<32>) -> u64
fn cancel_admin_transfer(env: soroban_sdk::Env)