Deposit amount USDC into the vault. USDC is forwarded to the yield
adapter, which deploys it to the underlying protocol.
Returns the number of mUSDC shares minted to the caller.
fn deposit(
env: soroban_sdk::Env,
caller: soroban_sdk::Address,
amount: i128,
min_shares_out: i128,
) -> Result
Withdraw by burning shares mUSDC. Returns the USDC amount sent back
to the caller.
min_usdc_out is a caller-supplied slippage floor. If the computed
USDC output falls below this value the transaction reverts with
MinAmountOutNotMet, giving the caller a predictable, typed failure
rather than an opaque WithdrawalTooSmall. Pass 0 to opt out of the
floor (behaviour is then identical to the pre-guard contract).
This guards against ratio-shifting: a concurrent withdrawal by another
depositor changes the shared ADPT_SH/TOTAL_SH ratio before this
transaction lands, silently shrinking the payout. With min_usdc_out
the caller can bound how much shrinkage they are willing to accept.
fn withdraw(
env: soroban_sdk::Env,
caller: soroban_sdk::Address,
shares: i128,
min_usdc_out: i128,
) -> Result
Returns the current admin address.
fn get_admin(env: soroban_sdk::Env) -> Result
Returns whether deposits are currently paused.
fn is_paused(env: soroban_sdk::Env) -> bool
Retained so the ABI of vaults already deployed from earlier WASM is unchanged, and so an old vault can still be initialized by hand.
On any vault deployed from this WASM it is unreachable: __constructor
has already set ADMIN, so every call returns AlreadyInitialized.
That is the intended behavior, not a leftover. An attacker calling
this against a freshly deployed vault is rejected instead of served.
fn initialize(
env: soroban_sdk::Env,
admin: soroban_sdk::Address,
usdc: soroban_sdk::Address,
musdc: soroban_sdk::Address,
adapter: soroban_sdk::Address,
) -> Result<(), ContractError>
Admin-only emergency switch. While paused, new deposits are rejected. Withdrawals are deliberately left open so a pause can never trap funds.
fn set_paused(env: soroban_sdk::Env, paused: bool) -> Result<(), ContractError>
Returns the current adapter address.
fn get_adapter(env: soroban_sdk::Env) -> Result
Called by the mUSDC token contract immediately after it moves a
transfer's balances, splitting Principal/Entry pro-rata between
sender and receiver (#578) — the fix for the honest-0 degradation
documented on get_principal/get_entry_time, from back when mUSDC
was a plain Stellar Asset Contract with no hook for the vault to
observe a transfer at all.
Requires the mUSDC token's own require_auth(), which succeeds only
when mUSDC is the direct caller of this exact invocation — Soroban
treats a contract's own direct sub-calls as inherently authorized by
that contract, with no signature needed (see
Env::authorize_as_current_contract's doc comment: "All the direct
calls that the current contract performs are always considered to
have been authorized"). This can therefore never be triggered by
anything other than a genuine transfer on the one real, configured
mUSDC contract — the same direction-reversed pattern
adapter-common::require_vault_auth already uses for every adapter
to verify a call
fn on_transfer(
env: soroban_sdk::Env,
from: soroban_sdk::Address,
to: soroban_sdk::Address,
amount: i128,
sender_balance_before: i128,
receiver_balance_before: i128,
) -> Result<(), ContractError>
Replace the yield adapter. The vault must have no shares outstanding
(TOTAL_SH == 0) and no position left at the old adapter
(ADPT_SH == 0) before calling this.
Checking TOTAL_SH alone is not sufficient: the two counters can
desync (see migrate_adapter's NoAdapterPosition doc), so a vault
that looks empty by TOTAL_SH can still have real value sitting at
the old adapter. Deliberately does not reset ADPT_SH to zero on
swap the way it used to -- doing so would destroy the only evidence
that a stranded position existed, and it is unnecessary: ADPT_SH
is only ever nonzero here in a genuinely-empty vault as a result of a
bug, and clearing it would delete the observability that bug needs
to be diagnosed, not fix it. In the normal case (both counters
already zero) leaving ADPT_SH alone is a no-op.
fn set_adapter(
env: soroban_sdk::Env,
new_adapter: soroban_sdk::Address,
) -> Result<(), ContractError>
Completes a pending admin handover. Requires the nominee's own
require_auth(), not the current admin's, so the transfer can only
complete once the new address has demonstrably proven it controls a
working signing key. Fails with NoPendingAdmin if no
transfer_admin nomination is outstanding.
fn accept_admin(env: soroban_sdk::Env) -> Result<(), ContractError>
Returns the address's mUSDC share balance, read from the share token itself. mUSDC received by transfer counts immediately and withdraws normally, exactly like minted shares; there is no separate vault-side balance that could disagree with the token.
Returns 0 before initialize, rather than erroring: this is a view
used by dashboards, and "no position" is the truthful answer for a
vault that holds nothing yet.
fn get_position(env: soroban_sdk::Env, address: soroban_sdk::Address) -> i128
Returns the address's cost basis: the net USDC it deposited and has not yet withdrawn. Yield earned is current share value minus this value.
Cost basis is history, not a holding, so unlike the share balance it
cannot be derived from the token on its own — but as of #578, mUSDC is
a custom SEP-41 token the vault controls the code of (not a plain
Stellar Asset Contract), so its transfer/transfer_from call back
into on_transfer, which splits a sender's basis and hands the
pro-rata share to the receiver at the moment of transfer. See
on_transfer's doc comment for the split math.
An address that transferred its entire position away reports 0
here because it holds nothing, rather than a stale basis for shares
it no longer has — on_transfer clears both records itself the
moment a full transfer-out reaches zero.
Like get_entry_time, the self-heal below (clearing a zero-position
holder's leftover record) should be unreachable in the current
system; it's kept as a defensive fallback, and as a bridg
fn get_principal(env: soroban_sdk::Env, address: soroban_sdk::Address) -> i128
Sets the admin, USDC token address, mUSDC share token address, and
the initial yield adapter address, inside the deploying transaction's
own CreateContract operation. Unlike a separate initialize() call,
there is no intervening ledger where an attacker could land a
self-authorized call first: the deployer's transaction is the only
one that can ever set this contract's state (#551, same bug class as
#505, fixed for the adapters/mUSDC in #550).
Unlike the adapters/mUSDC's constructor arguments, admin is a
human-held key, not a programmatically-derived contract address, so
require_auth() is called on it here too: without it, DEPLOYER
alone could set any address as admin with no proof it is controlled
by anyone, permanently bricking the vault on a typo (transfer_admin/
accept_admin both require the current admin's own signature to
move away from it). Soroban only honors require_auth() inside a
constructor for the address that is the deploying transaction's own
source account, so this requires `
fn __constructor(
env: soroban_sdk::Env,
admin: soroban_sdk::Address,
usdc: soroban_sdk::Address,
musdc: soroban_sdk::Address,
adapter: soroban_sdk::Address,
)
Returns the ledger timestamp of the address's deposit, or 0 if it holds no position. Reset whenever the position is fully withdrawn.
Entry time belongs to a depositor, not to the shares: it is recorded
when an address first deposits, and now (#578) is also split
pro-rata on a transfer by on_transfer — see that function's doc
comment for the split math. An address holding no mUSDC reports 0
even if it deposited earlier and later transferred everything away,
so a record left behind by a full transfer-out is never reported as
a live position.
on_transfer clears a sender's record itself the moment a full
transfer-out reaches zero, so a leftover record found here should be
unreachable in the current system; this self-heal is kept as a
defensive fallback for any balance that reached zero another way
this contract doesn't yet account for (and for state left behind by
mUSDC's pre-#578 life as a plain Stellar Asset Contract with no
transfer hook at all, before a live-holder migration if one hasn't
run yet). Cl
fn get_entry_time(env: soroban_sdk::Env, address: soroban_sdk::Address) -> u64
Admin-only: nominate new_admin as the next admin. Requires the
current admin's require_auth(). Does not itself change who the
admin is — that only happens once the nominee calls accept_admin
with their own signature, so a typo'd or unreachable address can
never brick admin: the old admin stays in control until a working
key on the other end proves it can sign. Overwrites any prior,
not-yet-accepted nomination.
fn transfer_admin(
env: soroban_sdk::Env,
new_admin: soroban_sdk::Address,
) -> Result<(), ContractError>
Phase 1 of a two-phase migration, and the trigger for
MIN_LEDGER_GAP's ~1-day timelock. Snapshots the target adapter's
total_assets() and the current ledger sequence so that a later
migrate_adapter call can verify the valuation has been stable for
at least MIN_LEDGER_GAP ledgers. This prevents an observer from
griefing or masking a migration by front-running a
transiently-shifted valuation (issue #567), and separately gives
observers or automated monitoring a real window to notice and react
to a migration triggered by a compromised admin key before it can
execute (issue #557) — the same unattended signer used for routine
migration-keeper operations can call this function, so the delay
itself is the only thing standing between a leaked key and the
vault's entire position moving to an address the attacker
controls.
Can be called repeatedly for the same or different adapters; each call overwrites the previous snapshot. The admin must then wait for the ledger gap to elapse before calling `migrate_adapt
fn begin_migration(
env: soroban_sdk::Env,
new_adapter: soroban_sdk::Address,
) -> Result<(), ContractError>
Phase 2 of a two-phase migration. Must be preceded by
begin_migration(new_adapter) and at least MIN_LEDGER_GAP
ledgers must have elapsed. Moves the vault's entire position from
the current adapter to new_adapter in one atomic transaction,
without requiring depositors to withdraw first. Unlike
set_adapter, this is safe to call with shares outstanding.
Withdraws everything from the old adapter into the vault, deposits
it into new_adapter, and performs two independent checks:
Slippage: new_adapter.total_assets() must be at least
(10_000 - max_slippage_bps) / 10_000 of the old adapter's
pre-migration value.
Stability: new_adapter.total_assets() must be at least
(10_000 - max_slippage_bps) / 10_000 of the snapshot value
recorded by begin_migration, proving the valuation has been
stable across the ledger-gap cooldown.
If either check fails the whole call reverts and nothing moves
(Soroban transactions are atomic). On success the snapshot is
cleared. TOTAL_SH, every holder's m
fn migrate_adapter(
env: soroban_sdk::Env,
new_adapter: soroban_sdk::Address,
max_slippage_bps: u32,
) -> Result<(), ContractError>
Total USDC value managed by the vault as reported by the adapter. Includes yield accrued by the underlying protocol.
fn get_total_assets(env: soroban_sdk::Env) -> Result
Returns total mUSDC shares outstanding.
fn get_total_shares(env: soroban_sdk::Env) -> i128
Returns the pending admin nominee, if any.
fn get_pending_admin(env: soroban_sdk::Env) -> Option
Permissionless entry point that extends the TTL of instance storage
and the position records for address. Anyone can call it, so
off-chain keepers or the user themselves can keep a position alive
without needing a signature on the vault.
fn extend_position_ttl(env: soroban_sdk::Env, address: soroban_sdk::Address)
Returns the current migration snapshot, if one has been recorded by
begin_migration. Off-chain callers can use this to verify the
cooldown is progressing.
fn get_migration_snapshot(
env: soroban_sdk::Env,
) -> Result