StellarLend exposes two separate repay paths — one for the single-asset borrow system and one for the cross-asset system. Their handling of overpayment, interest ordering, and dust differs. Integrators must understand which path they are calling.
pub fn repay(env: &Env, user: Address, asset: Address, amount: i128) -> Result<(), BorrowError>If amount exceeds the total outstanding debt (principal + accrued interest), repay returns
BorrowError::RepayAmountTooHigh. The position is left unchanged.
Integrator requirement: read the exact current balance with get_debt_balance() before
submitting a repay. Never pass a hardcoded "large" amount expecting it to be silently capped.
Interest is settled before principal on every repay:
calculate_interestis called at repay time and added tointerest_accrued.- The repay amount is first consumed against
interest_accrued. - Any remaining amount reduces
borrowed_amount.
This means a repay of exactly interest_accrued zeros interest without touching principal, and
the borrower retains their collateral-backed position.
calculate_interest uses ceiling division:
interest = ceil(principal × INTEREST_RATE_PER_YEAR × elapsed / (BPS_SCALE × SECONDS_PER_YEAR))
For any non-zero principal and any elapsed time ≥ 1 second, interest ≥ 1. Therefore
get_debt_balance() at the moment of repay equals the exact amount repay will consume.
No sub-unit dust can remain after a correctly-sized repay call.
Repay is permitted during EmergencyState::Recovery. Users must be able to unwind positions
even when high-risk operations (new borrows, withdrawals) are paused. The gate is:
if is_paused(PauseType::Repay) || (!is_recovery && blocks_high_risk_ops)
| Condition | Error |
|---|---|
amount ≤ 0 |
BorrowError::InvalidAmount |
| No outstanding debt | BorrowError::InvalidAmount |
asset does not match position asset |
BorrowError::AssetNotSupported |
amount > interest + principal |
BorrowError::RepayAmountTooHigh |
| Protocol paused for repay | BorrowError::ProtocolPaused |
pub fn repay_asset(env: &Env, user: Address, asset: Address, amount: i128) -> Result<(), CrossAssetError>If amount exceeds the current debt balance for that asset, the repay amount is silently
clamped to the outstanding balance:
let repay_amount = amount.min(current_debt);This means callers may safely pass an amount larger than the debt (including i128::MAX - 1)
without triggering an error. The position will be fully cleared.
The cross-asset system does not accrue interest. Debt balances change only through explicit borrow and repay operations.
Repaying one asset's debt does not affect any other asset's balance in the user's position.
Each asset key is independent in the debt_balances map.
| Condition | Error |
|---|---|
amount ≤ 0 |
CrossAssetError::InvalidAmount |
| Protocol paused for repay | CrossAssetError::ProtocolPaused |
| Dimension | borrow::repay |
cross_asset::repay_asset |
|---|---|---|
| Overpay handling | Error (RepayAmountTooHigh) |
Silent clamp to balance |
| Interest accrual | Yes (ceiling-rounded) | No |
| Repay ordering | Interest first, then principal | N/A |
| Dust risk | None (ceiling rounding) | None (clamp floors at 0) |
| Recovery-mode repay | Allowed | Allowed |
| Asset isolation | Single asset per position | Per-asset in a shared map |
Do not overpay the borrow system. Unlike many DeFi protocols that silently refund excess,
borrow::repay returns RepayAmountTooHigh. Always query get_debt_balance() for the exact
amount required.
Dust debt cannot block withdrawals. Because interest uses ceiling division, get_debt_balance()
at any timestamp is exactly what a full repay consumes. There is no scenario where a tiny
residual interest amount (< 1 unit) blocks a user's withdrawal.
Cross-asset overpay is safe by design. A malicious caller cannot use an overpay to extract
excess credit — the clamp is a floor operation, not a refund. repay_asset will never reduce
a balance below zero.
Recovery-mode positions can always be unwound. If a user is in a recovery-mode scenario, they retain the ability to repay and reduce their health-factor risk regardless of what other operations are paused.