Contract 013a5318c0a4d25cbabd476f8d8f62863c060f7939ae6ca71b683e4fe7ef0756

← Back to Index 📥 Download WASM

Meta

cliver 27.0.0#5a7c5fe76530bf4248477ac812fc757146b98cc4
rssdkver 26.1.0#175aa41306f383057a8cdfc84b68d931664fc34e
rsver 1.97.1

Instances

  • CA7Z4KUVURE5WQJVXPXEXCYJA6WPXTJUC23I45CKEAJSLVZRHGRYTQJZ

Interface

fn admin(env: soroban_sdk::Env) -> soroban_sdk::Address
fn keeper(env: soroban_sdk::Env) -> Option
fn router(env: soroban_sdk::Env) -> Option

Replace the Registry Wasm. Auth: admin (checked BEFORE the swap).

The gen-1 Registry shipped WITHOUT this ("holds no funds, keep it dumb") and that immutability backfired: the 2026-08-01 audit fixes touched Registry code, and with Factory + every Strategy holding this address permanently, the only patch path was discarding the entire fleet. The admin already controls everything the Registry decides (whitelist, router, keeper, fees), so an admin-gated upgrade adds repairability without adding power.

fn upgrade(env: soroban_sdk::Env, new_wasm_hash: soroban_sdk::BytesN<32>)

Some = upsert, None = remove. Enforces the two fee caps loudly: fee_bps ≤ MAX_FEE_BPS, and no non-zero fee_bps while the treasury is unset (set the treasury first). Auth: admin.

fn set_vault(
    env: soroban_sdk::Env,
    vault: soroban_sdk::Address,
    info: Option,
) -> Result<(), soroban_sdk::Error>

(treasury, rebalance_delta_bps) — the protocol-wide fee config read by every Strategy on the withdraw/rebalance path.

fn fee_config(env: soroban_sdk::Env) -> (Option, u32)

Set/rotate the fleet-wide keeper address (key rotation in one tx). Auth: admin.

fn set_keeper(env: soroban_sdk::Env, keeper: soroban_sdk::Address)

Set the Soroswap router (used from T2). Auth: admin.

fn set_router(env: soroban_sdk::Env, router: soroban_sdk::Address)

Read a vault's record (None if not whitelisted). Bumps the entry's TTL on read so an ACTIVELY-USED listing can't silently archive and revert new deposits until re-listed (the Strategy reads this inside every invest, so a vault in active use stays alive; a real tx persists the bump, a bare simulate does not).

fn vault_info(env: soroban_sdk::Env, vault: soroban_sdk::Address) -> Option

Accept a proposed admin rotation, step 2 of 2. Auth: the NOMINEE.

fn accept_admin(env: soroban_sdk::Env) -> Result<(), soroban_sdk::Error>

Deploy-time init. Stores admin; every other key follows the read-default rule (Router/Treasury/Keeper ⇒ None, RebalanceDeltaBps ⇒ 0, KeeperEnabled ⇒ false) and is written only when first set.

fn __constructor(env: soroban_sdk::Env, admin: soroban_sdk::Address)

The keeper-swap oracle floor config, or None while unset (⇒ keeper cannot swap).

fn oracle_config(env: soroban_sdk::Env) -> Option
fn keeper_enabled(env: soroban_sdk::Env) -> bool

Set the protocol-wide treasury + rebalance surcharge. Validates delta_bps ≤ BPS − MAX_FEE_BPS so fee_bps + delta_bps ≤ 10_000 for every vault by construction. Once set, the treasury stays Some. Auth: admin.

fn set_fee_config(
    env: soroban_sdk::Env,
    treasury: soroban_sdk::Address,
    delta_bps: u32,
) -> Result<(), soroban_sdk::Error>

Opt asset in/out of the set a KEEPER may name as rebalance_swap's asset_out. Auth: admin.

Why this exists: rebalance_swap takes a caller-supplied path and amount_out_min with no on-chain price oracle, and the same keeper can call rebalance_withdraw first in the SAME transaction — so without a bound the reachable amount is an entire invested position, not the idle float. Every hop of the path is checked against this list, not just the output (an endpoint-only check is trivially routed around via [allowed, EVIL, allowed]).

What this does NOT do. It narrows the total-loss case; it does not eliminate it. Allowlisting ASSETS says nothing about pool DEPTH: a compromised keeper can seed the canonical pair between two allowlisted assets with dust and still dump a whole position into it at amount_out_min = 1. The real residual bound is the depth of the shallowest canonical pair among allowlisted assets — which an attacker can influence. Only the T3 oracle floor closes this; do not read this l

fn set_swap_asset(env: soroban_sdk::Env, asset: soroban_sdk::Address, allowed: bool)

Rotate the admin (the 2-of-3 multisig address), step 1 of 2. Auth: current admin. Nothing changes until the nominee calls accept_admin.

Why this is two-step, and was not

It used to be a single set_admin, justified in the security model as "recoverable by the multisig itself". That was wrong. require_admin gates set_admin AND upgrade on whatever RegAdmin currently holds, so a rotation to an address nobody controls leaves no path back: the Registry can never be re-administered and never upgraded. User funds stay safe — owners withdraw from their own Strategy, which needs no Registry admin — but vault listings, fees, router, keeper and oracle freeze permanently and recovery means deploying a whole new fleet.

The nominee signing step 2 proves the key exists and can sign BEFORE it takes effect, which is the only thing that makes a typo survivable.

fn transfer_admin(env: soroban_sdk::Env, new_admin: soroban_sdk::Address)

Configure the price oracle that bounds a KEEPER's rebalance_swap. Auth: admin.

This closes the accepted T2 HIGH risk: with no oracle, amount_out_min and the path are caller-supplied, so a compromised keeper key could dump a whole position into a dust pool at amount_out_min = 1 and the measured-output check would call it a success. With a floor, that swap simply reverts.

decimals is pinned here rather than read per swap: the Strategy would otherwise spend a second cross-contract call on every rebalance to discover a constant. An admin who points this at an oracle with different decimals and does not update the field mis-scales the floor, so the two MUST be set together — which is why they live in one struct and one setter.

Use an INDEPENDENT oracle, never Soroswap: Soroswap is the venue being bounded, so pricing the bound from it makes the whole check circular.

fn set_oracle_config(
    env: soroban_sdk::Env,
    oracle: soroban_sdk::Address,
    base: soroban_sdk::Address,
    decimals: u32,
    max_slippage_bps: u32,
    max_price_age: u64,
) -> Result<(), soroban_sdk::Error>

Global kill-switch — fleet-wide halt of keeper-driven rebalances. Auth: admin.

fn set_keeper_enabled(env: soroban_sdk::Env, enabled: bool)

Is asset allowlisted as a keeper-path swap output? Read-default false.

fn swap_asset_allowed(env: soroban_sdk::Env, asset: soroban_sdk::Address) -> bool

Remove the oracle config. Auth: admin.

A TARGETED kill-switch: because the floor is fail-closed, clearing it stops every keeper swap fleet-wide while leaving rebalance_withdraw / rebalance_deposit — which are leak-proof by construction — running. The alternative, set_keeper_enabled(false), is a much bigger hammer: it also halts every rebalance. If the ORACLE is what misbehaved, this is the proportionate response.

fn clear_oracle_config(env: soroban_sdk::Env)

Imports

WebAssembly Text (WAT) ▶