Skip to main content

Leveraged Position Workflow

Open a leveraged position on DeepBook Margin end to end on Sui Testnet: create a MarginManager, read the pool's risk parameters and borrow liquidity onchain, deposit collateral, borrow to open leverage, monitor your risk ratio, then close the position, repay the borrow, and withdraw. Each step lists what to observe, so the page doubles as a verification checklist.

Margin trading borrows funds against collateral, and anyone can liquidate a borrowed position. Read Margin Risks before you start, and keep it open alongside this walkthrough.

caution

Leverage multiplies losses as well as gains, and interest accrues on your borrow continuously. If your position's risk ratio falls to the pool's liquidation threshold, anyone can liquidate it, and the fees from that are a permanent loss. This walkthrough uses small Testnet amounts to make each mechanic observable, not to model a position you should hold. See Margin Risks for liquidation, interest rate, and oracle risk in detail.

This walkthrough uses the SUI_DBUSDC pool, borrowing the stable quote asset (DBUSDC) against a volatile base collateral (SUI) to open a leveraged long on SUI. Health monitoring here is entirely oracle-priced, so the pair choice matters: on Testnet the SUI and USDC price feeds are canonical Pyth feeds, whereas the Testnet DEEP feed is a substitute (an HFT feed) rather than a dedicated DEEP oracle. Do not build oracle-priced risk math on the substitute feed.

The samples live in the examples/deepbook-margin package, type-checked with tsc --noEmit against @mysten/deepbook-v3 and @mysten/sui, and reference pools, coins, and margin pools by SDK key rather than hardcoding onchain IDs.

  • A current CLI: run suiup update sui, then sui --version to confirm it.
  • Testnet SUI on your address for collateral and gas, from the Sui faucet.
  • For the borrow-and-trade steps, enough SUI for collateral and gas, plus DBUSDC borrow liquidity in the margin pool. You supply SUI as collateral and borrow DBUSDC from the pool, so you do not need DBUSDC in your wallet.
  • The SDK installed: npm install @mysten/deepbook-v3 @mysten/sui.

The margin objects

DeepBook Margin adds three shared objects on top of the spot order book. See DeepBook Margin design for the full model.

  • MarginManager wraps a BalanceManager and binds it to one DeepBook pool. It holds your collateral, tracks your borrow, and is the account every margin order trades from.
  • MarginPool is the lending pool for a single asset. Your borrow draws from it, and interest accrues to it.
  • MarginRegistry stores each pool's risk parameters and tracks enabled pools and registered managers.

A MarginManager borrows from one margin pool at a time, base or quote, which keeps risk on a single axis.

Set up the client

The client references pools, coins, and margin pools by SDK key. On Testnet it auto-loads the margin package IDs and Pyth config. Read-only calls work without a manager; borrowing and trading need one.

import { SuiGrpcClient } from '@mysten/sui/grpc';
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
import { decodeSuiPrivateKey } from '@mysten/sui/cryptography';
import {
deepbook,
type DeepBookClient,
type MarginManager,
type BalanceManager,
} from '@mysten/deepbook-v3';
import type { ClientWithExtensions } from '@mysten/sui/client';

export type DeepBookMarginClient = ClientWithExtensions<{ deepbook: DeepBookClient }>;

export function getKeypair(privateKey: string): Ed25519Keypair {
const { secretKey } = decodeSuiPrivateKey(privateKey);
return Ed25519Keypair.fromSecretKey(secretKey);
}

// Testnet DeepBook client with margin enabled. On Testnet the SDK auto-loads the
// margin package IDs, margin pools, and Pyth config, so you reference pools,
// coins, and managers by key. Read-only calls (risk parameters, pool liquidity)
// work without a manager; borrowing and trading through a margin manager need
// `marginManagers`. Supplying to a margin pool and staking use a spot
// `BalanceManager`, so pass `balanceManagers` when you compose those legs.
export function marginClient(
address: string,
options?: {
marginManagers?: { [key: string]: MarginManager };
balanceManagers?: { [key: string]: BalanceManager };
},
): DeepBookMarginClient {
return new SuiGrpcClient({
network: 'testnet',
baseUrl: 'https://fullnode.testnet.sui.io:443',
}).$extend(deepbook({ address, ...options }));
}

Create a MarginManager

Create one MarginManager for the pool and reuse it across runs. A single margin_manager::new call creates the manager, shares it, and registers it in the MarginRegistry, so there is no separate registration step. Creating a new manager each run leaves orphaned shared objects that fragment your collateral, so discover or persist the ID and create only when you have none.

// Reuse an existing MarginManager instead of minting a new one on every run.
// `getMarginManagerIdsForOwner` returns the managers the registry tracks for an
// owner across every pool. A MarginManager is bound to one DeepBook pool, so
// return only the one for `poolKey`: reading a manager's state with `poolKey`
// succeeds when the manager belongs to that pool and aborts otherwise, so a
// manager for a different pool is skipped. Returning the first manager of any
// pool would wire the rest of the flow to the wrong pool. Treat the lookup as
// best-effort and also persist the ID you create, because a new manager each run
// leaves orphaned shared objects that fragment your collateral.
export async function findMarginManagerId(
client: DeepBookMarginClient,
owner: string,
poolKey: string,
): Promise<string | undefined> {
const ids = await client.deepbook.getMarginManagerIdsForOwner(owner);
for (const id of ids) {
try {
const states = await client.deepbook.getMarginManagerStates({ [id]: poolKey });
if (Object.keys(states).length > 0) return id;
} catch {
// Reading with `poolKey` aborts for a manager bound to another pool.
}
}
return undefined;
}

If that returns nothing, create one, then read the created shared object ID from the effects and save it:

// Create a MarginManager for a pool. A single `margin_manager::new` call creates
// the manager, shares it, and registers it in the MarginRegistry, so there is no
// separate registration step for the normal path. After execution, read the
// created shared object whose type contains `MarginManager` from the effects,
// persist its ID, and pass it to the client under `marginManagers` so later
// steps address it by key.
export function createMarginManager(
client: DeepBookMarginClient,
poolKey: string,
): Transaction {
const tx = new Transaction();
tx.add(client.deepbook.marginManager.newMarginManager(poolKey));
return tx;
}

Observe: the effects contain a created shared object whose type ends in MarginManager, and a MarginManagerCreatedEvent naming the manager, its wrapped balance manager, and the DeepBook pool. Save the ID and pass it to the client as a named manager.

Read the pool's risk parameters

Every threshold that governs your position is stored per pool in the MarginRegistry and set by governance, not fixed in code. Read them onchain rather than assuming defaults. See Risk ratio for what each one means.

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

// A pool's risk thresholds and liquidation rewards are stored per pool in the
// MarginRegistry and are set by governance, not fixed in code. Read them on
// chain rather than hardcoding: the values below differ from the protocol
// defaults and can change. `liquidation < minBorrow < minWithdraw` always holds.
export interface RiskParams {
liquidationRiskRatio: number; // at or below this, the position can be liquidated
minBorrowRiskRatio: number; // a borrow must leave the ratio at or above this
minWithdrawRiskRatio: number; // a withdraw must leave the ratio at or above this
targetLiquidationRiskRatio: number; // liquidation restores the ratio to this
userLiquidationReward: number; // fraction of collateral paid to the liquidator
poolLiquidationReward: number; // fraction of collateral paid to the pool
}

export async function readRiskParams(
client: DeepBookMarginClient,
poolKey: string,
): Promise<RiskParams> {
const db = client.deepbook;
const [
liquidationRiskRatio,
minBorrowRiskRatio,
minWithdrawRiskRatio,
targetLiquidationRiskRatio,
userLiquidationReward,
poolLiquidationReward,
] = await Promise.all([
db.getLiquidationRiskRatio(poolKey),
db.getMinBorrowRiskRatio(poolKey),
db.getMinWithdrawRiskRatio(poolKey),
db.getTargetLiquidationRiskRatio(poolKey),
db.getUserLiquidationReward(poolKey),
db.getPoolLiquidationReward(poolKey),
]);
return {
liquidationRiskRatio,
minBorrowRiskRatio,
minWithdrawRiskRatio,
targetLiquidationRiskRatio,
userLiquidationReward,
poolLiquidationReward,
};
}

Observe: for SUI_DBUSDC on Testnet at the time of writing, the read returns a liquidation risk ratio of 1.1, a min borrow risk ratio of 1.2499, a min withdraw risk ratio of 2, a target liquidation risk ratio of 1.25, and liquidation rewards of 0.02 (user) and 0.03 (pool). The ordering liquidation < min borrow < min withdraw always holds. Note the rewards are not the protocol defaults, which is why you read them: a borrow must leave your ratio at or above the min borrow ratio, and a withdraw at or above the min withdraw ratio.

Check borrow liquidity

You borrow from a margin pool, so it must hold enough idle supply to lend. The pool's max utilization rate caps borrowing, and anything already borrowed counts against that cap. A pool with no headroom fails a borrow the same way a thin order book fails a trade, so check before you borrow. Utilization also sets the interest rate, so a nearly full pool is an expensive one.

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 };
}

Observe: borrowableNow is positive for the asset you intend to borrow. For the DBUSDC pool on Testnet at the time of writing, supply is about 10087 and borrow about 119, so headroom is ample; the SUI pool runs at higher utilization with less headroom and a higher rate. If borrowableNow is at or near zero, borrow a smaller amount, wait for supply, or borrow the other asset.

info

The remaining steps borrow real funds: they supply SUI as collateral and borrow DBUSDC from the margin pool, so you need SUI for collateral and gas and enough DBUSDC borrow liquidity in the pool, not DBUSDC in your wallet. They appear as type-checked samples with the observable result to confirm when you run them. The code is identical to what an executed run uses, so once the pool has borrow liquidity you run each step in order and check the stated result.

Deposit collateral

Deposit collateral into the manager. Collateral backs your borrow: the more you deposit before borrowing, the higher your starting risk ratio. This walkthrough deposits SUI as base collateral.

// Deposit collateral into the MarginManager. Use `depositBase` for the pool's
// base asset (SUI in SUI_DBUSDC) and `depositQuote` for the quote asset
// (DBUSDC). Collateral is what backs your borrow: the more you deposit before
// borrowing, the higher your starting risk ratio. The amount is in whole coins;
// the SDK scales it to the coin's decimals.
export function depositBaseCollateral(
client: DeepBookMarginClient,
managerKey: string,
amount: number,
): Transaction {
const tx = new Transaction();
tx.add(client.deepbook.marginManager.depositBase({ managerKey, amount }));
return tx;
}

Size the deposit to your wallet balance, and keep a SUI reserve for gas:

// Never deposit more collateral than the wallet holds. `depositBase` sources
// coins with coinWithBalance, which throws at build time otherwise. Leave a SUI
// reserve so gas still has funds after the deposit.
export async function safeCollateralAmount(
client: DeepBookMarginClient,
owner: string,
coinType: string,
decimals: number,
want: number,
reserve = 0,
): Promise<number> {
const b = await client.core.getBalance({ owner, coinType });
const have = Number(b?.balance?.balance ?? '0') / 10 ** decimals;
return Math.max(0, Math.min(want, have - reserve));
}

Observe: getMarginManagerState('MM') reports the collateral, and the manager's base balance increases by the deposited amount. No debt yet, so the risk ratio reads at its maximum.

Borrow to open leverage

Borrow the quote asset against your collateral. The pool rejects the borrow unless the resulting risk ratio stays at or above the min borrow risk ratio you read, and that ceiling is what sets your maximum leverage. Size the borrow from the risk parameters, not by trial and error.

// Borrow against your collateral. This example opens a leveraged long on SUI:
// with SUI collateral deposited, borrow the quote asset (DBUSDC) so you can buy
// more SUI than your own funds cover. A MarginManager borrows from one margin
// pool at a time, base or quote, not both.
//
// The borrow is rejected unless the resulting risk ratio stays at or above the
// pool's Min Borrow Risk Ratio. That ceiling on how much you can borrow per unit
// of collateral is what sets your maximum leverage, so size the borrow from the
// risk parameters you read, not by trial and error.
export function borrowQuote(
client: DeepBookMarginClient,
managerKey: string,
amount: number,
): Transaction {
const tx = new Transaction();
tx.add(client.deepbook.marginManager.borrowQuote(managerKey, amount));
return tx;
}

Observe: the manager's borrowed quote shares increase, its quote balance increases by the borrowed amount, and getMarginManagerState('MM').riskRatio drops from its maximum to a finite value at or above the min borrow ratio. A borrow that would breach that ratio aborts.

Open the position

With borrowed DBUSDC in the manager, place a bid on SUI_DBUSDC to buy SUI. Margin orders go through the pool proxy, not the spot order entry, so the borrow and the trade stay bound to the manager. The clientOrderId is your own tag: the SDK types it as a string, but the contract encodes it as u64, so pass a numeric string such as a timestamp, not a label. Price and quantity must respect the pool's tick and lot size.

// Open the leveraged position. With borrowed DBUSDC in the manager, place a bid
// on SUI_DBUSDC to buy SUI: your position is now larger than your own collateral
// funded, which is the leverage.
export function openLongPosition(
client: DeepBookMarginClient,
marginManagerKey: string,
poolKey: string,
clientOrderId: string, // numeric string, encoded as u64
price: number,
quantity: number,
): Transaction {
const tx = new Transaction();
tx.add(
client.deepbook.poolProxy.placeLimitOrder({
poolKey,
marginManagerKey,
clientOrderId,
price,
quantity,
isBid: true,
}),
);
return tx;
}

Observe: the order rests or fills against the book, and your position in SUI is now larger than your collateral alone could fund. The borrow funds that surplus, which is the leverage.

Monitor your risk ratio

Read your live risk ratio and see how close it is to liquidation. getMarginManagerState values your collateral and debt through the pool's Pyth oracles and returns the risk ratio in one call, along with the prices it used.

import type { DeepBookMarginClient } from './client.js';
import type { RiskParams } from './risk-params.js';

// Read your live risk ratio and see how close it is to liquidation.
// `getMarginManagerState` values your collateral and debt through the pool's
// Pyth oracles and returns the resulting risk ratio in one call, along with the
// oracle prices it used. Because it is oracle-priced, it moves whenever SUI
// moves, and it drifts down on its own as interest accrues on the debt.
export interface RiskStatus {
riskRatio: number;
baseDebt: string;
quoteDebt: string;
// Distance to the liquidation threshold, as a fraction of the current ratio.
// Small and shrinking is the danger sign.
marginToLiquidation: number;
liquidatable: boolean;
}

export async function readRiskStatus(
client: DeepBookMarginClient,
marginManagerKey: string,
params: RiskParams,
): Promise<RiskStatus> {
const state = await client.deepbook.getMarginManagerState(marginManagerKey);
const marginToLiquidation =
(state.riskRatio - params.liquidationRiskRatio) / state.riskRatio;
return {
riskRatio: state.riskRatio,
baseDebt: state.baseDebt,
quoteDebt: state.quoteDebt,
marginToLiquidation,
liquidatable: state.riskRatio <= params.liquidationRiskRatio,
};
}

Observe: riskRatio sits above liquidationRiskRatio, and marginToLiquidation is the cushion you have. Because the ratio is oracle-priced, it moves whenever SUI moves, and it drifts down on its own as interest accrues, even if the price is flat.

The oracle prices must be fresh. Each pool sets a maximum price age and a price tolerance, which default to these constants and are adjustable by governance within the age bounds shown:

If the price is older than the pool's maximum age, valuation reverts rather than acting on a stale price. See Margin Risks for oracle risk.

What happens if you do nothing and the price moves against you

If you hold the position and SUI falls, your collateral is worth less against a debt that only grows with interest, so the risk ratio falls. Nothing intervenes on your behalf. When the ratio reaches the pool's liquidation risk ratio, can_liquidate returns true and the position becomes eligible for liquidation.

Liquidation is permissionless. Today it runs through the margin_liquidation package: liquidate_base (and its quote counterpart) is callable by anyone, checks eligibility with can_liquidate, and sources the repay capital from a shared liquidation vault.

That entry point calls the inner margin_manager::liquidate, which does the settlement: it repays part of your debt and pays the liquidator and the pool a reward out of your collateral, sized to restore your risk ratio to the target liquidation ratio rather than closing the whole position.

Margin Risks: how liquidation works and the risk ratio position lifecycle work the consequences through with concrete SUI/USDC numbers. In short: partial liquidation leaves you with a smaller, healthier position, but the reward the liquidator takes from your collateral is a permanent loss, and if the position is deeply underwater a full liquidation can occur and the lending pool can take bad debt. Liquidation is immediate once your ratio reaches the threshold, so monitoring and acting early (adding collateral or reducing the position) is the only defense.

Close the position

To unwind, reduce your exposure with a reduce-only order. Reduce-only guarantees the order can only shrink your position, never flip it or add leverage: here it sells SUI back for DBUSDC so you can repay.

// Close or shrink the position with a reduce-only order. Reduce-only guarantees
// the order can only decrease your exposure, never flip you to the other side or
// add leverage: here it sells SUI back for DBUSDC so you can repay the borrow.
export function reduceLongPosition(
client: DeepBookMarginClient,
marginManagerKey: string,
poolKey: string,
clientOrderId: string, // numeric string, encoded as u64
price: number,
quantity: number,
): Transaction {
const tx = new Transaction();
tx.add(
client.deepbook.poolProxy.placeReduceOnlyLimitOrder({
poolKey,
marginManagerKey,
clientOrderId,
price,
quantity,
isBid: false,
}),
);
return tx;
}

Then repay the borrow plus the interest accrued since you opened it. Pass an amount to repay part, or omit it to repay the full outstanding balance.

// Repay borrowed funds plus the interest accrued since you borrowed. Pass an
// amount to repay part of the debt, or `undefined` to repay the full outstanding
// balance. Repaying raises your risk ratio and is what you do before withdrawing
// collateral or closing the position.
export function repayQuote(
client: DeepBookMarginClient,
managerKey: string,
amount?: number,
): Transaction {
const tx = new Transaction();
tx.add(client.deepbook.marginManager.repayQuote(managerKey, amount));
return tx;
}

Observe: after the reduce-only order fills, your SUI position shrinks; after repay, the manager's borrowed shares drop to zero (or by the repaid amount) and getMarginManagerState('MM').riskRatio returns to its maximum once debt is cleared.

Withdraw collateral

Withdraw your collateral back to your wallet. A withdraw must leave your risk ratio at or above the min withdraw risk ratio, so you cannot pull collateral out from under an open borrow: repay first, then withdraw.

// Withdraw collateral back to your wallet. A withdraw must leave your risk ratio
// at or above the pool's Min Withdraw Risk Ratio, so you cannot pull collateral
// out from under an open borrow: repay first, then withdraw. The amount is in
// whole coins.
export function withdrawBaseCollateral(
client: DeepBookMarginClient,
managerKey: string,
amount: number,
): Transaction {
const tx = new Transaction();
tx.add(client.deepbook.marginManager.withdrawBase(managerKey, amount));
return tx;
}

Observe: your wallet balance increases and the manager's collateral balance decreases by the withdrawn amount. With the debt repaid, you can withdraw the full collateral.

Troubleshooting

  • Borrow aborts on risk ratio: the borrow would leave your ratio below the pool's min borrow risk ratio. Deposit more collateral or borrow less. Read the current threshold with getMinBorrowRiskRatio.
  • Withdraw aborts on risk ratio: you are trying to withdraw collateral that a borrow still depends on. Repay first, then withdraw, and keep the ratio at or above the min withdraw ratio.
  • Borrow fails for insufficient pool liquidity: the margin pool has no headroom under its max utilization rate. Check borrowableNow from Check borrow liquidity, borrow less, or wait for supply.
  • Valuation reverts on a stale price: the Pyth price is older than the pool's maximum price age. Refresh the price feed before the risk-reading or trading call. See Oracle risk.
  • Cannot convert <value> to a BigInt: clientOrderId is encoded as u64. The SDK types it as a string, but a non-numeric value fails to encode. Use a numeric string, such as a timestamp.
  • Every run mints a new manager: you are not reusing an existing MarginManager. Discover or persist the ID and create only when absent, as in Create a MarginManager.