Skip to main content

Interest Rates

Margin pools use a kinked interest rate model where the borrow rate increases gradually up to an optimal utilization point, then rises sharply to discourage excessive borrowing and maintain liquidity for withdrawals.

Utilization rate

Utilization drives every rate in the pool. It measures how much of the supplied liquidity borrowers have taken:

utilization = totalBorrow / totalSupply

An empty pool reports a utilization of zero. As borrowing rises, so does the rate, which draws in new suppliers and discourages further borrowing. The pool's max utilization rate caps how far this can go, so suppliers can always withdraw a portion of their funds.

Borrow interest formula

The formula for the borrow interest rate (APR) is:

if utilization < optimalUtilization:
borrowRate = baseRate + utilization × baseSlope
else:
borrowRate = baseRate + optimalUtilization × baseSlope + (utilization - optimalUtilization) × excessSlope

Where:

  • Utilization: The ratio of total borrowed assets to total supplied assets.
  • Base rate: The minimum interest rate when utilization is 0%.
  • Base slope: The rate of increase in interest below optimal utilization.
  • Optimal utilization: The target utilization rate (typically 80%), also called the kink.
  • Excess slope: The steep rate of increase above optimal utilization.

The contract holds all four values in an InterestConfig inside the pool's ProtocolConfig, as fixed-point numbers with 9 decimal places, so a base slope of 15% becomes 150_000_000. The pool operator changes them with update_interest_params, and the contract rejects any configuration where the optimal utilization exceeds the max utilization rate.

Current parameters

AssetBase rateBase slopeOptimal utilizationExcess slopeMax utilization
USDC0%15%80%500%90%
SUIUSDE0%15%80%500%90%
SUI3%20%80%500%90%
DEEP5%25%80%500%90%
WAL5%25%80%500%90%

The max utilization rate caps how much of the pool's liquidity borrowers can take, so suppliers can always withdraw a portion of their funds.

info

Treat these as example values. Each pool operator sets its own parameters and can update them, so read the live values onchain rather than hardcoding them. See Read the current rate.

Worked examples

At 50% utilization in the USDC pool (below optimal):

borrowRate = 0% + 50% × 15% = 0% + 7.5% = 7.5% APR

At 80% utilization (at optimal):

borrowRate = 0% + 80% × 15% = 0% + 12% = 12% APR

At 85% utilization (above optimal, below max):

borrowRate = 0% + 80% × 15% + (85% - 80%) × 500% = 0% + 12% + 25% = 37% APR

The jump from 12% to 37% across five points of utilization is the kink working as intended. Borrowing past the optimal point becomes expensive quickly, which pushes utilization back down before the pool runs out of withdrawable liquidity. Size a borrow against the rate you will pay after the borrow lands, not the rate you see before it.

What suppliers earn

The borrow rate is not what suppliers receive. Borrowers pay interest on the borrowed portion only, and the protocol takes a spread off the top:

supplyRate = borrowRate × utilization × (1 - protocolSpread)

The pool exposes this directly as true_interest_rate, alongside the borrow rate as interest_rate. At 50% utilization in the USDC pool with a 10% protocol spread:

supplyRate = 7.5% × 50% × (1 - 10%) = 3.375% APR

Two suppliers in pools with identical borrow rates earn different amounts when utilization differs, because idle liquidity earns nothing. The contract caps the protocol spread at 20%, and the portion it takes splits between supply referrals, the protocol treasury, and the pool maintainer. See Design for that split.

How interest accrues

The pool does not apply interest continuously. It recomputes interest whenever its state changes, which means on every supply, borrow, repay, and withdraw against that pool, by anyone. Each update charges simple interest over the elapsed interval:

interest = totalBorrow × borrowRate × (elapsedMs / YEAR_MS)

YEAR_MS is 365 days in milliseconds. The pool then adds the full interest to totalBorrow, and adds the interest minus the protocol spread to totalSupply.

Because each update grows totalBorrow, the next update charges interest on the larger balance. Interest therefore compounds, but at the frequency of pool activity rather than on a fixed schedule. A busy pool compounds more often than a quiet one, so the realized annual yield runs slightly above the quoted APR.

For a pool with 100,000 USDC borrowed at 12% APR, one day of accrual with a 10% protocol spread produces:

interest       = 100,000 × 12% × (86,400,000 / 31,536,000,000) = 32.88 USDC
protocol fees = 32.88 × 10% = 3.29 USDC
to suppliers = 32.88 - 3.29 = 29.59 USDC

Shares absorb the interest

Neither borrowers nor suppliers hold a balance that the pool rewrites. Both hold shares, and the pool tracks a ratio for each side:

borrowRatio = totalBorrow / borrowShares
supplyRatio = totalSupply / supplyShares

Your borrow shares never change until you borrow or repay. Your debt grows because accrued interest raises borrowRatio, so the same shares convert to a larger amount. That is why your debt increases and your risk ratio drifts toward liquidation even when prices hold flat and you take no action.

Share conversions round in the pool's favor. Borrowing rounds shares up, and converting borrow shares back to an amount rounds up, so a repayment never leaves a residual debt on the pool's side.

Read the current rate

Parameters are per pool and adjustable, so query them instead of assuming defaults. In Move, MarginPool exposes interest_rate, true_interest_rate, total_supply, total_borrow, max_utilization_rate, and protocol_spread. From TypeScript, the following helper reads the current rate alongside the supply, borrow, and utilization headroom it depends on:

import type { DeepBookMarginClient } from './client.js';

// You borrow from a margin pool, so it must hold enough idle supply to lend.
// Borrowing is capped by the pool's max utilization rate: the most that can be
// borrowed is `maxUtilizationRate * totalSupply`, and anything already borrowed
// counts against it. A pool with no headroom fails a borrow the same way a thin
// order book fails a trade, so check before you borrow. The interest rate rises
// with utilization, so a nearly full pool is also an expensive one.
export interface BorrowLiquidity {
totalSupply: number;
totalBorrow: number;
maxUtilizationRate: number;
interestRate: number; // current borrow APR, moves with utilization
borrowableNow: number; // headroom before the utilization cap
}

export async function readBorrowLiquidity(
client: DeepBookMarginClient,
coinKey: string,
): Promise<BorrowLiquidity> {
const db = client.deepbook;
const [supplyStr, borrowStr, maxUtilizationRate, interestRate] = await Promise.all([
db.getMarginPoolTotalSupply(coinKey),
db.getMarginPoolTotalBorrow(coinKey),
db.getMarginPoolMaxUtilizationRate(coinKey),
db.getMarginPoolInterestRate(coinKey),
]);
// Supply and borrow come back as decimal strings; the rates as numbers.
const totalSupply = Number(supplyStr);
const totalBorrow = Number(borrowStr);
const borrowableNow = Math.max(0, maxUtilizationRate * totalSupply - totalBorrow);
return { totalSupply, totalBorrow, maxUtilizationRate, interestRate, borrowableNow };
}

Check the rate before you borrow, because utilization also determines whether a borrow succeeds at all. A pool at its max utilization rate rejects new borrows outright. The Leveraged Position Workflow shows this check in context.