Withdraw
withdraw removes one or more supplied assets from an account and sends them to
to when provided, else to caller. It returns the actual amount paid per
deduped asset.
withdraw(receiver, is_liquidation = false, entries), then re-checks the LTV gate
and health factor on cached indexes. The account owner (or delegate) stays the
auth subject regardless of to; only the token destination changes.
Full-position sentinel. Pass
amount = 0 for an asset to withdraw its
entire post-accrual position. The controller forwards i128::MAX to the
pool, which clamps it to the current actual supply; over-asking with any amount
at or above the position value clamps the same way. The zero is sticky: once
an asset’s running total across the batch is zero, further legs for that asset
stay at “all”.The return value is the resolved figure — the only way a caller learns what a
withdraw-all actually paid. There is no max_withdraw view; simulate the call
instead.Preconditions
calleris the account owner or an active delegate.- Each asset has a live supply position in the account (else
#121). - After the withdrawal, if any debt remains, the health factor stays at or
above
1e18, the LTV gate holds, and LTV-weighted collateral stays above the minimum-borrow floor. - The pool holds enough cash for the requested amount (else
#112). - Every priced asset resolves a valid price. A
frozenlisting blocks entry but not exit, so a frozen asset stays withdrawable; apausedlisting blocks both.
Inputs
Address
required
Account owner. Authorizes the withdrawal; receives the tokens when
to is
absent.u64
required
The account to withdraw from. Must already exist.
Vec<(HubAssetKey, i128)>
required
Each entry is
({ hub_id, asset }, amount) in asset-native units. amount = 0
withdraws the full position for that asset. Duplicate legs for the same asset are
summed, preserving first-appearance order.Option<Address>
Optional recipient override. When set, the pool pays the withdrawn tokens
directly to this address — useful for contracts (vaults, routers) forwarding
funds to an end user without an extra hop.
Build the call
Authorize
caller.require_auth() runs inside withdraw; the caller must be the account
owner (the position-NFT holder) or an active delegate. The controller transfers
the withdrawn assets from the pool to the recipient
(to or caller) — pool-internal, no extra signature. The transaction’s only
authorization is caller ‘s top-level call. Simulate first: an account with
debt can revert at the post-withdrawal health-factor gate.
Verify
On success the controller emitsUpdatePositionBatchEvent (["position", "batch_update"], legs tagged Withdraw) and the pool emits
PoolMarketStateBatchEvent (["market", "batch_state_update"]).
The call returns the actual amounts paid per asset (a full close pays the
floor-valued balance). Confirm with
get_collateral_amount(account_id, hub_asset) (zero after a full close),
get_total_collateral_usd(account_id), and get_health_factor(account_id). If
the withdrawal cleared the last supply and debt position, the account is removed
and its NFT burned — get_account_attributes(account_id) then raises
#24 AccountNotFound, and a later supply must pass account_id = 0 to open a
new account.
Failure modes
Repay
repay reduces an account’s debt. It is permissionless: any caller may
repay any account_id, and overpayment is refunded to caller.
caller into the pool, then calls the
pool’s batched repay(payer, actions). If a payment exceeds the outstanding
debt, the pool refunds the excess to caller. Credit is measured: a token
that delivers less than it is sent retires only the delivered amount.
Preconditions
callerholds at leastamountof every asset inpayments. No account ownership is required.- Each asset has a live debt position in the account (else
#120). repayloads and persists the debt side only, and is not pause-gated at the protocol level. Apausedlisting on that asset still blocks it (#315); afrozenlisting does not.
Inputs
Address
required
Funds the repayment and authorizes the transfers. Need not own the account.
Receives any overpayment refund.
u64
required
The account whose debt is repaid. Must already exist.
Vec<(HubAssetKey, i128)>
required
Each entry is
({ hub_id, asset }, amount) in asset-native units, strictly
positive. Overpayment is refunded to caller.Build the call
Authorize
caller.require_auth() runs inside repay. The transaction authorizes one
token transfer per asset from caller to the pool. Because repay is
permissionless, the only signature required is the payer’s — never the account
owner’s.
Verify
On success the controller emitsUpdatePositionBatchEvent (["position", "batch_update"]) and PoolMarketStateBatchEvent (["market", "batch_state_update"]).
Confirm with get_borrow_amount(account_id, hub_asset),
get_total_borrow_usd(account_id), and get_health_factor(account_id) (rises as
debt falls; i128::MAX once debt-free).
Account close behavior
Clearing all positions cleans up the account. Repaying the last debt while collateral remains does not remove the account — the supply positions still exist. The account’s storage is removed only when both its supply and debt maps are empty, which happens when an owner withdrawal (or a close-position strategy) takes out the final position. To fully close, repay all debt, then withdraw all collateral with the full-position sentinel.Failure modes
Next
Liquidations
What happens when health factor drops below 1, and the liquidator’s recipe.
Accounts and risk
Accounts, positions, and how account close-out works.

