> ## Documentation Index
> Fetch the complete documentation index at: https://xoxno.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Stellar lending withdraw and repay

> Withdraw collateral and repay debt through the controller, including the full-position sentinel and permissionless repay.

Withdraw collateral out of an account and repay its debt, both through the controller. **Read first:** [Supply and borrow](/stellar-lending/dev/supply-and-borrow), [Health factor](/stellar-lending/dev/health-factor).

Both calls go to the **controller**. Withdraw is owner-only and re-checks risk. Repay is permissionless: anyone can repay any account. Confirm state from controller events and views.

## 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.

```rust theme={"system"}
withdraw(
    caller: Address,
    account_id: u64,
    withdrawals: Vec<(Address, i128)>,
    to: Option<Address>,
) -> Vec<(Address, i128)>
```

For each asset the controller calls the pool's `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.

<Note>
  **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)`.
</Note>

### Preconditions

* `caller` is 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 `1e18` and 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

<ParamField path="caller" type="Address" required>
  Account owner. Authorizes the withdrawal; receives the tokens when `to` is absent.
</ParamField>

<ParamField path="account_id" type="u64" required>
  The account to withdraw from. Must already exist.
</ParamField>

<ParamField path="withdrawals" type="Vec<(Address, i128)>" required>
  Each entry is `(asset_address, amount)` in asset-native units. `amount = 0` withdraws the full position for that asset.
</ParamField>

<ParamField path="to" type="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.
</ParamField>

### Build the call

<CodeGroup>
  ```bash Stellar CLI theme={"system"}
  # Withdraw the full USDC position (amount 0) from account 42.
  stellar contract invoke \
    --id <CONTROLLER> \
    --source-account caller \
    --network testnet \
    -- withdraw \
    --caller C... \
    --account_id 42 \
    --withdrawals '[["CUSDC...", "0"]]' \
    --to null
  ```

  ```typescript TypeScript theme={"system"}
  import { Contract, nativeToScVal, Address } from "@stellar/stellar-sdk";

  const controller = new Contract("C...");

  const withdrawals = nativeToScVal(
    [[new Address("CUSDC...").toScVal(), nativeToScVal(0n, { type: "i128" })]], // 0 = full position
    { type: "Vec" },
  );

  const op = controller.call(
    "withdraw",
    new Address(callerPublicKey).toScVal(),
    nativeToScVal(42n, { type: "u64" }),
    withdrawals,
    nativeToScVal(null, { type: "option" }), // or an Address to redirect the payout
  );
  // build, prepareTransaction (simulate), sign with caller, send
  ```
</CodeGroup>

### 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 emits `UpdatePositionBatchEvent` (`["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`.

```rust theme={"system"}
repay(
    caller: Address,
    account_id: u64,
    payments: Vec<(Address, i128)>,
)
```

The controller pre-transfers each `(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

* `caller` holds at least `amount` of every asset in `payments`. No account ownership is required.
* Each asset has a live debt position in the account (else `#120`).
* Active markets use the permissive `RiskDecreasing` policy; disabled markets use `Repay`, which also tolerates a disabled market status.

### Inputs

<ParamField path="caller" type="Address" required>
  Funds the repayment and authorizes the transfers. Need not own the account. Receives any overpayment refund.
</ParamField>

<ParamField path="account_id" type="u64" required>
  The account whose debt is repaid. Must already exist.
</ParamField>

<ParamField path="payments" type="Vec<(Address, i128)>" required>
  Each entry is `(asset_address, amount)` in asset-native units. Overpayment is refunded.
</ParamField>

### Build the call

<CodeGroup>
  ```bash Stellar CLI theme={"system"}
  # Repay 200 XLM toward account 42's debt (overpayment is refunded).
  stellar contract invoke \
    --id <CONTROLLER> \
    --source-account caller \
    --network testnet \
    -- repay \
    --caller C... \
    --account_id 42 \
    --payments '[["CXLM...", "2000000000"]]'
  ```

  ```typescript TypeScript theme={"system"}
  import { Contract, nativeToScVal, Address } from "@stellar/stellar-sdk";

  const controller = new Contract("C...");

  const payments = nativeToScVal(
    [[new Address("CXLM...").toScVal(), nativeToScVal(2_000_000_000n, { type: "i128" })]],
    { type: "Vec" },
  );

  const op = controller.call(
    "repay",
    new Address(callerPublicKey).toScVal(), // any payer
    nativeToScVal(42n, { type: "u64" }),
    payments,
  );
  // build, prepareTransaction (simulate), sign with caller, send
  ```
</CodeGroup>

### 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 emits `UpdatePositionBatchEvent` (`["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

<CardGroup cols={2}>
  <Card title="Liquidations" icon="scale-balanced" href="/stellar-lending/dev/liquidations">
    What happens when health factor drops below 1, and the liquidator's recipe.
  </Card>

  <Card title="Accounts and risk" icon="user-shield" href="/stellar-lending/dev/accounts-and-risk">
    Accounts, positions, and how account close-out works.
  </Card>
</CardGroup>
