Programs

Put vault


Copied from the Tender programs' own docs on 2026-09-28: current, plus: init_vault now requires the INIT_AUTHORITY signer (D5, commit e0ebe78), and new vaults default to the put image on pump.fun IPFS (D14).

Built and tested: commits 40b3e77 (M2), 81258bf (sandwich-resistant reads and live-spot clock, from report 25) and c18c8c1 (optional check pool). Described as of c18c8c1.

The product is a daily, European, cash-settled capped put spread (owner decisions 2026-09-28: cash-settled; capped at roughly 3-4x the premium). The engine is not used. The vault holds only USDC and keeps its own position records. This is the one place in tender where a price read settles money; see "Why this is not the engine's rule" below.

Source: review/0dte/SPEC-SOLANA.md section 3 including "As built"; commits 40b3e77, 81258bf, c18c8c1.

Payoff

Per token, at expiry: claim = min(max(K - S, 0), W). K is the strike, W the cap width, S the settlement price. The vault locks W x size of USDC per position at sale, its maximum loss on that position. No tokens move at any point.

Source: SPEC-SOLANA section 3 "Product".

Accounts and seeds

AccountSeedsHolds
PutVault["putvault", underlying_mint]fixed VaultConfig; share and USDC bucket counters; the round's reads; live and settled day
Vault USDC["usdc", vault]all of the vault's USDC
Depositor["depositor", vault, owner]shares and one queued request
Epoch["epoch", vault, index]one settlement's queue result: NAV, shares before, deposits, shares minted, withdrawals
Day["day", vault, expiry]up to 6 series sharing an expiry: strike and width (bps of S and Q64.64), 11-point fair table, sold and locked per series, settlement S and payout per series, claims
Position["position", asset]one purchase: day, series, Core asset, size, premium, collateral, time

USDC is always in exactly one counter bucket: idle, locked (max payout of live positions), reserved (claims set aside), pending_deposits, or withdrawable. The buckets are counters, never balances, so a donation moves nothing.

Source: programs/tender_puts/src/state.rs at c18c8c1.

VaultConfig (fixed at init_vault)

FieldMeaningProgram bound
pool, pool_typemain pool: PumpSwap or Meteora DLMM (Raydium AMM v4 and Orca Whirlpool are declared but return UnsupportedPool)
treasuryfee recipient (its USDC account)
listeronly caller of list_day; the default key makes listing open
expiry_cap, series_capmax payout (sum of W x size) per expiry and per series, USDCabove 0
start_bps, floor_bpsclock multiples of fairfloor at most start, start at most 10x
decay_secs, sale_windowclock decay and sale windowdecay above 0; window at least decay and under 1 day minus 30 min
band_bpsrefuse sales further than this from Sat most 5% (the fair table's range)
min_fair_bpsfair premium at S (fair bps x width bps) must be at least this many bps of S to listabove 0, below 10,000
fee_bpsprotocol feeat most 10%
listing_hourUTC hour (20)below 24
twap_secsDLMM only: read the pool oracle's time-weighted bin over this many seconds (0 = instantaneous)at most 3,600; non-zero only for a DLMM main pool
check_pool, check_twap_secs, max_deviation_bpsoptional DLMM pool of the same token and its TWAP window and allowed deviationnot the main pool; TWAP 1 to 3,600 s; deviation 1 to 5,000 bps
symbol, image_uriNFT naming and imagesymbol 1 to 10 bytes, URI at most 200

Whoever calls init_vault first for a mint sets its config; there is no admin afterwards. The front end lists vaults by known address.

Source: programs/tender_puts/src/state.rs VaultConfig::check at c18c8c1; SPEC-SOLANA section 3 "Open risks".

Instructions

InstructionWhoEffect
init_vault(config)anyone, once per mintcreates the vault, its USDC account and its Core collection
request_deposit(amount)depositorUSDC moves now (measured); shares are minted at the next settle_day
request_withdraw(shares)depositorshares leave the balance now; redeemed at the next settle_day
collectdepositorcredits a processed request, pro rata of that epoch's totals
record_priceanyoneone read per 20-second bucket from listing_hour - 5 min to + 15 min (up to 60). Must be the only instruction in its transaction (compute budget aside) and not a CPI. Cross-checked against the check pool if set
settle_dayanyoneafter the reads close (or all 60 are in): S = median of at least 3 reads; the expiring day's claims are set aside, the rest of its lock freed; the queue is processed at NAV = idle USDC
list_day(strike_bps[], width_bps[], fair_bps[][11])listerwithin 30 min after the reads close, after settle_day, with no live day: refused if the interquartile spread of the reads exceeds 5%; per series checks strike, width at most strike, fair values in range, fair at S at least min_fair_bps; expiry = this round's listing time + 24 h (20:00 UTC the next day)
buy(series, size, max_premium)buyersee below
claimholder (non-zero claim) or anyone (zero claim)after settlement: pays the claim in USDC to the asset's owner, burns the asset, returns rent to the holder

Source: programs/tender_puts/src/lib.rs at c18c8c1.

buy

  1. Inside the sale window of an unsettled day.
  2. Refused if any other instruction in the transaction names the pool.
  3. Reads spot (and the check pool if set). Refused outside the band of S, or at or below K.
  4. Premium = W x size x fair(interpolated at live spot) x clock multiple, where the clock falls linearly from start to floor over decay_secs from listing.
  5. Refused if premium exceeds max_premium, or if premium x 4.5 is less than W x size (max payout at most 4.5x the premium).
  6. Refused unless series and expiry caps have room and idle USDC plus the net premium covers W x size.
  7. Premium in, measured: fee to the treasury, the rest to the vault.
  8. Mints a Core asset in the vault's collection with the terms (see Option NFTs).

Measured compute (verification run of tests-svm/tests/puts.rs at 6108c98, programs unchanged since c18c8c1): buy 145,094 CU without a check pool and 167,761 with one (commit c18c8c1's message says about 171k); record_price 16,724 (PumpSwap), 49,353 (DLMM TWAP, 163 observations) and 34,582 (PumpSwap plus DLMM check pool TWAP); claim about 153k.

Source: programs/tender_puts/src/lib.rs buy at c18c8c1; commit messages 81258bf and c18c8c1; verification run (docs/protocol/verification.md).

Price reads

  • PumpSwap: (quote token account + the pool's BOOST virtual quote reserve at byte 245) / base token account, quoted in USDC or wSOL, the same pricing the pool and the launch vault use. NEET's pool has 0 virtual reserve. Fixed in a386675 (earlier builds left the reserve out).
  • Meteora DLMM: the active bin, or with twap_secs the pool oracle's time-weighted bin. The oracle updates before each swap, so a same-slot move carries no weight. ANSEM, ZCAT and PAID have initialized oracles (163, 100 and 100 observations); NEET's PumpSwap pool has none.
  • SOL-quoted reads are converted to USD with a Pyth SOL/USD PriceUpdateV2 passed in: Full-verified, at most 60 s old, confidence at most 1% (the engine's checks).
  • Check pool: record_price and buy refuse with CheckPoolDeviation when the main pool is more than max_deviation_bps from the check pool's oracle TWAP, both in USD from the same Pyth update. NEET's vault uses PumpSwap 5wNu... as main and Meteora DLMM BkocTz... as check at 200 bps (2%). The commit message calls 200 the default; there is no on-chain default, the value is passed at init_vault.

Source: SPEC-SOLANA section 3 "As built"; tests-svm/fixtures/puts/README.md; programs/tender_puts/src/price.rs (pumpswap) at c18c8c1.

SettingValue
Strike0.98 x S
Widthsolved so W = 3.5 x the floor premium (median 12.0% of S NEET, 13.9% ANSEM, 28.8% ZCAT and 45.6% PAID on proxy paths)
Fairmax(30-day fair, 72-hour-regime fair), passed as 11 points at -5% to +5% of S
Clockstart 3.0x, floor 1.2x, 15 min decay, 30 min window
Band+/-5%
Caps, max payout per expiryNEET $1.3k, ANSEM $1.8k, ZCAT $3k (pilot), PAID $4k (pilot)

Report 25's verdict: no strike, width or floor made the originally specified clock (fixed USD price, floor 0.8) depositor-positive against disciplined buyers. Re-pricing at live spot off the higher of the two fairs with floor 1.2 makes the vault "positive or idle": disciplined and regime-aware buyers never buy; the vault sells only to buyers paying at least 1.2x fair, and every profit comes from them.

Source: 25-first-list.md TL;DR, findings 1-3, section 5; addendum.

Invariants

  • Every live position is backed by W x size of USDC in the locked bucket.
  • Sold max payout never exceeds series_cap per series or expiry_cap per expiry.
  • The buckets always sum to the vault's accounted USDC; donations are ignored.
  • The queue is processed only at settle_day, when the vault's value is all USDC, so shares never need a price.
  • A claim is paid once, to the Core asset's owner at the time of claim.
  • No sale outside the band, in the money, beside another instruction naming the pool, or under W / 4.5.

Source: programs/tender_puts/src/state.rs and lib.rs at c18c8c1; 19 LiteSVM tests in tests-svm/tests/puts.rs.

Why this is not the engine's rule

The engine settles by delivery so that no price is ever read for a payout. These spreads settle on a price. What bounds the damage is the cap: the vault can lose at most W x size per position and expiry_cap per day, and report 25 sized the caps so that displacing the median costs more than it can win, for a sandwich of each read (model A) and for a read that must survive arbitrage (model B).

Open risks recorded in the spec:

  • a multi-transaction bundle can still sandwich reads (the median and TWAP only raise the cost);
  • buy on a PumpSwap pool reads instantaneous spot (pump, buy, dump in one bundle gets a cheaper premium, bounded by the band, floor, W/4.5 and caps, not modelled); the check pool closes this for NEET;
  • the first caller of init_vault sets a mint's config;
  • unclaimed claims stay reserved.

Source: SPEC-SOLANA section 3 "As built" (open risks) and "Manipulation"; 25-first-list.md section 4.

Permissionless cranks

record_price, settle_day, collect and zero-value claim need no key. list_day needs the lister key, which is a trusted role (owner decision pending).

Source: programs/tender_puts/src/lib.rs at c18c8c1; SPEC-SOLANA section 3 "As built".