The safety model
The property, and where it stops#
The claim has two halves and they are not equally strong. The first is a property of this program and is unchanged. The second belongs to whichever venue the money reaches, and is weaker. Both are stated here, because only the pair of them is true.
The property holds above the line because the program enforces it. Below the line it holds only as far as each venue's own key model: on Hyperliquid that is a vendor statement nobody has tested, and on Derive the key model is the right one but no key can be minted per token today.
Every outbound path#
These are all of them. Each row is an instruction that can reduce a policy-owned balance.
| Instruction | Who signs | Where it can send | Can it reach a chosen address? |
|---|---|---|---|
route | anyone | the associated token account of Config.protocol_fee_dest, the bankroll, and lp_quote | No. The destination wallet is protocol config, not a caller argument. |
compound_lp | anyone | the pinned AMM program, via a passthrough signed only by lp_auth | No. lp_auth owns lp_quote and lp_base and nothing else, so that pair is the whole set the forwarded instruction can debit. |
passive_deploy | anyone | only the registered venue account whose asset_mint equals target_asset | No. |
agent_fund_venue | only Policy.agent | any registered venue account | No. Registration is an authority action. |
sweep_profit | anyone | lp_quote and holder_rewards | No. Both are program-owned and neither can pay a wallet. |
buyback_and_burn | anyone | the pinned AMM program, via a passthrough signed only by holder_auth, then the token program's burn | No. holder_auth owns only holder_rewards, and the bought tokens are burned in the same instruction rather than left anywhere. |
burn_base_leg | anyone | nowhere. It only burns. | No. |
bankroll ─┬─▶ registered venue account (agent_fund_venue / passive_deploy)
├─▶ lp_quote ──▶ LP (sweep_profit ──▶ compound_lp)
└─▶ holder_rewards ──▶ burn (sweep_profit ──▶ buyback_and_burn)
no arrow from this diagram ends at an address a caller supplies.The venue boundary#
Three instructions forward an instruction supplied by the caller to an external program: claim_fees to the pinned LaunchLab program, and compound_lp and buyback_and_burn to the pinned AMM program. Handing an outside program one of this program's signatures is the single most dangerous thing it does, and on 2026-09-23 two defects in exactly that were found, fixed and deployed to mainnet. The shape below is the fixed shape.
Two shapes of containment, because the constraints differ#
There are only two ways to contain a forwarded instruction: withhold the signature, or bound which instruction it can be. Each of the two call sites uses the one available to it.
| Instruction | How it is contained | What the forwarded instruction can reach |
|---|---|---|
compound_lp | Withholds the signature. It signs as lp_auth, a dedicated program address at ["compound_auth", policy], and nothing else signs. | lp_quote and lp_base, the two accounts lp_auth owns. The escrows are the policy's, the bankroll is bankroll_auth's and the holder rewards are holder_auth's, and none of those four signs here, so no instruction sent to the AMM can debit them however it is shaped. |
buyback_and_burn | Withholds the signature the same way, as holder_auth. This call site was always built this way, and it is the pattern the fix copied. | holder_rewards only. The bought tokens land in base_escrow, which is a destination and never has to sign, and they are burned in the same instruction. |
claim_fees | Cannot withhold it, so it bounds which instruction: the forwarded data must equal LaunchLab's claim_creator_fee discriminator exactly. | Only that one instruction, which takes no arguments, so there is no argument surface left to steer. Both escrows are still measured, and a forwarded call that made either one fall is refused with VenueSpentEscrow. |
require!(
data.as_slice() == LAUNCHLAB_CLAIM_CREATOR_FEE_DISCRIMINATOR,
BankrollError::NotTheClaimInstruction
);The eight bytes are [26, 97, 138, 203, 132, 171, 141, 252], read from LaunchLab's own IDL and independently equal to Anchor's derivation, sha256("global:claim_creator_fee")[..8]. The check is equality and deliberately not a prefix match, because a prefix match would accept appended bytes, and the point of the check is that the caller chooses nothing at all.
Three rules hold at every one of the three call sites:
| Rule | What it prevents |
|---|---|
| The CPI target must equal the one program pinned in Config for that instruction | Our signer seeds can never reach an arbitrary program. Each call site pins a single target, never either-of-two and never a caller-supplied id. |
| Every account the passthrough could touch is measured before and after, and only the measured delta is credited | A claim that moves nothing credits nothing, and a compound counts what actually left. The venue's instruction layout does not have to be trusted for the accounting to be right. |
| Exactly one account is marked as a signer by the forwarder, and it is ours | Every other account keeps the flags it arrived with, so the call cannot escalate someone else's privileges, and which of our addresses signs decides what is reachable. |
The measured-delta rule matters even now the venues are known, because a transfer-fee mint delivers less than was sent. On Token-2022 a measured delta is the only honest credit.
require_keys_eq!(*target.key, *venue_program, BankrollError::VenueNotWired);
let ix = Instruction { program_id: *venue_program, accounts: accounts.to_vec(), data };
invoke_signed(&ix, account_infos, seeds)What the venue boundary does not do#
The specification's own plan narrows this further: concrete instruction builders for LaunchLab and for Meteora belong in the program, at src/venue/launchlab.rs and src/venue/meteora.rs, so callers stop constructing venue instructions by hand. Those files are not written yet, and this is listed on what is not built yet.
The three keys#
| Key | Set by | May | May not |
|---|---|---|---|
| authority | init_policy, from the signer | change an unlocked policy, lock it, rotate the agent, register and revoke venue accounts, pause and unpause | withdraw anything, change the mode, unlock a locked policy |
| agent | init_policy, rotatable by set_agent | call agent_fund_venue to registered venues, call note_credit | choose a destination, withdraw, change any policy field, run at all in Passive mode |
| guardian | init_policy, and never changed afterwards | pause the policy | unpause it, or anything else at all |
A fourth key lives on Config: the reporter, which may attest Remote venue equity and should not be the agent. See the attestation page. The key the agent uses at Hyperliquid is not in this table at all: it is approved at the venue, and what it may do is the venue's rule rather than this program's.
The guardian can stop and cannot start#
set_paused(true) ──▶ signer must be the guardian OR the authority set_paused(false) ──▶ signer must be the authority
The asymmetry is deliberate. A guardian is a circuit breaker you can hand to someone you trust to stop things, without handing them the ability to restart the money moving. Only the authority restarts.
A paused policy fails every money instruction with PolicyPaused: claim_fees, route, compound_lp, passive_deploy, agent_fund_venue, sweep_profit, buyback_and_burn, burn_base_leg and note_credit. The protocol-wide Config.paused does the same thing to every policy at once, with ProtocolPaused.
The automatic pause#
A policy can also pause itself. report_remote_equity sets Policy.paused = true when an attestation moves further from the previous one than Config.max_deviation_bps allows. The report is still recorded, so a real loss is never hidden, but the money stops moving until a human looks at it. Only the authority can restart it. See remote equity and attestation.
Leverage, capped three times#
MAX_LEVERAGE = 5, and one place would not be enough.
| Where | What it checks | Enforced by |
|---|---|---|
| The policy | target_leverage_x <= 5 wherever it is written | on chain, init_policy and set_policy, error LeverageTooHigh |
| The attestation | a report carrying notional > 5 * equity is rejected outright | on chain, report_remote_equity, error NotionalTooHigh |
| The venue | the per-asset leverage setting on the sub-account, set to at most 5 when it is created | off chain, and therefore trusted |
The 5x multiplier in the attestation check is a literal in the instruction, not a config field, so no admin can widen it.
The risk slider is a fourth, different constraint: it caps how much of the vault may be deployed at all, which bounds the size of the position rather than its leverage. See risk, origins and asset classes.
For options, the 5x equivalent is two rules rather than one: defined risk only, no naked short options at any risk setting, and delta-adjusted notional at most 5x. Those are described on venues, they are enforced off chain by the agent runner rather than by the program, and no Stags options vault exists today to enforce them against.
The properties, and their status#
| Property | Status |
|---|---|
| The program can never pay an address the agent chooses | Enforced on chain. No instruction lets the agent name a destination. |
| The agent cannot withdraw at the venue either | Not claimed. On Hyperliquid an agent key has been approved and has traded, and whether it can withdraw has never been probed. On Derive no Stags vault exists to test. |
| A locked policy's splits and risk can never change | Enforced on chain. lock_policy is one way, set_policy refuses afterwards. |
| The guardian can stop the fund but not restart it | Enforced on chain, in set_paused. |
| Fee claims credit only the measured balance delta, and never debit an escrow | Enforced on chain. A forwarded call that made either escrow fall is refused with VenueSpentEscrow. |
| A forwarded venue instruction can only spend the accounts its one signer owns | Enforced on chain since 2026-09-23. compound_lp signs as lp_auth, buyback_and_burn as holder_auth, and claim_fees may only be claim_creator_fee. |
| Leverage is capped at 5x in three independent places | Two on chain, one at the venue and therefore trusted. |
| Remote equity is bounded by staleness, deviation and the pause, and is independently checkable against public APIs | Bounds enforced on chain; the number itself is an attestation, not a proof. See the attestation page. |