Security and trust model
Who can do what with a vault's funds: the program's guarantees, the keeper's limited powers, the admin's broad ones, what pausing does, and the audit status.
On this page
The short version#
- The program is non-custodial. Only a share holder can redeem shares, and withdrawals are never blocked by the pause switch.
- The keeper is boxed in. It can move a vault's USDC between the idle account and whitelisted strategies, within each strategy's on-chain cap, and nothing else. It cannot withdraw or send funds anywhere.
- The admin is trusted. One key is both the program's admin and its upgrade authority. An upgrade can change any rule, so today you are trusting that key completely.
- Nothing has been independently audited. This is devnet software with test tokens.
Roles#
- User
- Who
- Any wallet
- Powers
- Deposit, withdraw its own shares, mint test USDC from the faucet
- Keeper
- Who
- One key stored in the program config
- Powers
- Allocate and deallocate between idle and strategies; set devnet reserve APYs
- Admin
- Who
- One key stored in the program config, also the upgrade authority
- Powers
- Settings, vaults, strategies, pause, keeper and treasury rotation, program upgrades
- Treasury
- Who
- One account stored in the program config
- Powers
- Receives fee shares and can redeem them like any holder
- Anyone
- Who
- Any wallet
- Powers
- Sweep fees to the treasury, accrue a devnet reserve
What the keeper can and cannot do#
The keeper signs three instructions. The program checks the signer against the keeper key in its config on each of them:
allocateindex: u8amount: u64Moves idle USDC into a strategy. Rejected if the position would exceed the strategy's cap against total assets, if the strategy is disabled, or while paused. Cross-chain strategies burn a simulated 1 bp bridge fee.
deallocateindex: u8amount: u64Moves USDC from a strategy back to idle. `u64::MAX` exits the whole position. Allowed while paused so the keeper can always de-risk.
set_reserve_apyapy_bps: u32Devnet only. Accrues a simulated reserve at its old rate, then sets the new APY (at most 10,000 bps).
| The keeper can | The keeper cannot |
|---|---|
| Move USDC from idle into any enabled strategy, up to its cap | Withdraw, transfer or burn anyone's shares |
| Move USDC from any strategy back to idle, including while paused | Send USDC to any account other than the vault's idle account and its strategies' reserves |
| Set a devnet reserve's APY between 0% and 100% | Change caps, fees, the whitelist, the treasury or the pause flag |
| Choose not to act, or ignore its own sentinel | Mint shares, or move funds between vaults |
So a compromised keeper key cannot steal deposits, but it can still cost depositors yield or a little principal:
- parking everything in idle, or in the worst venues the caps allow;
- ignoring sentinel exits;
- churning cross-chain strategies, since each cross-chain move burns a 1 bp bridge fee and the program has no on-chain limit on how often the keeper moves;
- on devnet, setting reserve APYs that no real venue pays.
The admin can replace a compromised keeper key at any time with set_keeper.
Admin powers#
add_strategyindex: u8name: stringvenue: stringchain: u8risk_score: u8max_alloc_bps: u16Whitelists a reserve as the vault's next strategy (at most 8). Risk score 1 to 10, cap at most 10,000 bps.
cancel_admin_proposalcreate_reserveid: stringapy_bps: u32Devnet only. Creates a simulated venue and its token account.
create_vaulttier: u8fee_bps: u16perf_fee_bps: u16deposit_cap: u64Creates a vault with its share mint and idle account. Management fee at most 200 bps a year, performance fee at most 3,000 bps.
initialize_configkeeper: pubkeytreasury: pubkeyCreates the program config. The signer becomes the admin.
migrate_vaultGrows a vault created before the performance fee existed to the current layout. Idempotent.
propose_adminnew_admin: pubkeyset_keeperkeeper: pubkeyRotates the keeper key.
set_pausedpaused: boolPauses or unpauses deposits and allocations. Withdrawals, deallocations and fee sweeps keep working.
set_treasurytreasury: pubkeyChanges the account that receives fee shares.
update_strategymax_alloc_bps: u16risk_score: u8enabled: boolChanges a strategy's cap and risk score, or disables it. A disabled strategy receives nothing new but can still be drained.
update_vaultdeposit_cap: u64fee_bps: u16perf_fee_bps: u16Changes the deposit cap and fees (same limits). Management fee accrued so far is settled at the old rate first.
The program enforces these limits on the admin:
- Performance fee at most 30% of yield, management fee at most 2% a year.
- Strategy caps at most 100%, risk scores from 1 to 10, at most 8 strategies per vault.
- A strategy can only point at a reserve for the vault's own asset, and strategies cannot be removed.
- No admin instruction moves user funds.
The same key that is the admin is the program's upgrade authority on devnet. A program upgrade can replace any instruction, including the withdrawal path, so whoever holds that key could take every vault's funds. There is no multisig and no timelock today. For a mainnet launch, the upgrade authority should move to a multisig with a timelock, or the program should be made immutable.
Pause#
The admin can pause the whole protocol with set_paused. Pausing stops new money from entering the system and new risk from being taken, while everything that reduces risk keeps working:
| Instruction | While paused |
|---|---|
deposit | Blocked |
allocate | Blocked |
withdraw | Works |
deallocate | Works |
collect_fees | Works |
faucet, accrue_reserve, set_reserve_apy | Works |
| Admin instructions | Works |
A paused deposit fails with error 6000 (Paused). The pause is global: it applies to both vaults at once.
Users can always exit#
withdraw does not read the pause flag, and it pulls from strategies inside the same transaction when the idle account is short. No keeper action is needed for you to leave. The only ways a withdrawal can fail are the ones that also protect other depositors:
- the amount is zero, or so small it would pay zero USDC;
- you do not hold the shares;
- the strategies together cannot cover the amount (
InsufficientLiquidity), which cannot happen on devnet but could on mainnet if a venue were fully utilised; - the program has been upgraded to behave differently, which only the upgrade authority can do.
On-chain safeguards#
- Inflation-attack resistance. Share math uses an ERC-4626 style virtual offset of 1,000 base units, so donating to an empty vault cannot round later depositors down to zero shares.
- Account validation. Every instruction that values a vault requires all of its strategy accounts, in order, owned by the program and matched to their reserves and token accounts. A transaction cannot leave a strategy out to misprice shares, or slip in a foreign account.
- Fresh prices. Every deposit, withdrawal, allocation and fee sweep accrues all reserves and fees before it prices anything.
- Fully backed reserves. Devnet interest is minted into the reserve token account as it accrues, so reserves hold every token they owe.
- Rounding in the vault's favour, and checked arithmetic that fails instead of overflowing.
- Fees cannot be rounded away or charged twice. Fee accrual waits until at least one share unit is due, and the high-water mark stops a recovered loss being charged again.
Off-chain trust#
Some parts of the system are trusted without on-chain enforcement:
- Rate sources (venue APIs, DefiLlama)
- Trusted for
- APY, TVL and utilisation
- If it fails or lies
- The keeper allocates on wrong numbers, within the caps; the sentinel can be triggered or blinded
- Keeper host
- Trusted for
- Running the keeper and holding its key
- If it fails or lies
- As a compromised keeper key, above
- Web app host
- Trusted for
- Serving the dashboard code
- If it fails or lies
- Could ask you to sign a different transaction. Read your wallet's preview before you approve
- Your RPC endpoint
- Trusted for
- Balances, vault data, confirmations
- If it fails or lies
- Can show wrong numbers, but cannot redirect funds: the program checks every account
- Optional LLM classifier
- Trusted for
- A stricter second opinion
- If it fails or lies
- Can only tighten. Off in production
Audit status#
Neither the program nor the keeper has been audited by an independent firm. Do not deposit anything of value, and do not treat devnet behaviour as a guarantee of how a mainnet version would behave.
In October 2026 the program and the off-chain code (keeper, SDK, web app and deployment) were reviewed by an automated review agent at the owner's request. These reviews are not a substitute for an independent audit. Their findings are being fixed: the keeper and SDK changes ship with their next releases, and the program changes below need an upgrade.
The next program version adds deposit_checked and withdraw_checked, which fail if the shares or USDC you receive fall below a minimum you set, and a two-step admin handover, so the admin key can only be transferred to an account that accepts it. Until that upgrade is deployed, deposits and withdrawals settle at whatever the share price is when the transaction lands.
programs/aqueduct/src/instructions/admin.rs, programs/aqueduct/src/instructions/keeper.rs, programs/aqueduct/src/instructions/user.rs and programs/aqueduct/src/instructions/common.rs. If the code and this page disagree, the code wins.