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(action: PoolAction, is_liquidation = false, protocol_fee = 0), then re-checks the LTV gate and health factor on cached indexes. The owner 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. Preview the largest executable amount with max_withdraw(account_id, asset).Preconditions
calleris the account owner.- Each asset has a live supply position in the account.
- After the withdrawal, if any debt remains, the health factor stays at or above
1e18and the LTV gate holds. - The pool holds enough free liquidity for the requested amount (else
#112). - Accounts with debt are priced under a strict risk-increasing policy before the final health-factor gate; a debt-free withdrawal uses the risk-decreasing policy.
Inputs
Account owner. Authorizes the withdrawal; receives the tokens when
to is absent.The account to withdraw from. Must already exist.
Each entry is
(asset_address, amount) in asset-native units. amount = 0 withdraws the full position for that asset.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; only the account owner can call it. 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"]) and UpdateMarketStateBatchEvent (["market", "batch_state_update"]).
The call returns the actual amounts paid per asset (a full close pays the floor rounding of the position value). Confirm with collateral_amount_for_token(account_id, asset) (zero after a full close), total_collateral_in_usd(account_id), and health_factor(account_id). If the withdrawal cleared the last supply and debt position, the account is removed — 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
| Error | Cause | Fix |
|---|---|---|
#102 HealthFactorTooLow | Post-withdrawal health factor below 1e18 | Withdraw less, or repay debt first. |
#112 InsufficientLiquidity | Pool free liquidity below the requested amount | Withdraw less, or wait for repayments. |
#100 InsufficientCollateral | LTV gate fails after the withdrawal | Withdraw less, or repay debt first. |
#126 MinBorrowCollateralNotMet | LTV-weighted collateral below instance floor while debt remains | Repay debt first or withdraw less. |
Repay
repay reduces an account’s debt. It is permissionless: any caller may repay any account_id, and overpayment is refunded to caller.
(asset, amount) from caller into the pool, then calls the pool’s repay(action: PoolAction) per asset. If a payment exceeds the outstanding debt, the pool refunds the excess to caller.
Preconditions
callerholds at leastamountof every asset inpayments. No account ownership is required.- Each asset has a live debt position in the account (else
#120). - Active markets use the permissive
RiskDecreasingpolicy; disabled markets useRepay, which also tolerates a disabled market status.
Inputs
Funds the repayment and authorizes the transfers. Need not own the account. Receives any overpayment refund.
The account whose debt is repaid. Must already exist.
Each entry is
(asset_address, amount) in asset-native units. Overpayment is refunded.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 UpdateMarketStateBatchEvent (["market", "batch_state_update"]).
Confirm with borrow_amount_for_token(account_id, asset), total_borrow_in_usd(account_id), and 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
| Error | Cause | Fix |
|---|---|---|
#120 DebtPositionNotFound | No debt position for the asset in this account | Repay an asset the account actually owes. |
#16 InvalidPayments | Empty payments vector | Pass at least one (asset, amount) entry. |
#14 AmountMustBePositive | A payment amount is zero or negative | Use a positive amount per asset. |
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.

