Skip to main content
SODAX already had leverage-yield vaults: deposit, receive a share token, and the vault runs the leverage loop for everyone in it. SDK 2.2.0 adds the unpooled version. A leverage position is a lending account that belongs to one owner, opened at the leverage the user picks, and the SDK opens it with a single call. This post covers what a position is, how it differs from a vault, and the four things an integration does with one: size it, open it, track it and adjust it.

Vaults and positions

A vault is one shared position. Every depositor sits in the same eMode category at the same target loan-to-value, which is what lets the vault hand out a simple share token. A position gives that up in exchange for control. Under the hood, each position is a clone created by the LeveragePositionFactory on the Sonic hub, with its own account in the SODAX money market. The leverage itself comes from an intent: the position borrows, and a solver on SODAX trades the borrowed asset into more collateral. You set the floor on what that trade must return, so a bad fill cannot go through. Both products live on sodax.leverageYield, and new Sodax() already knows the factory address. No extra configuration is needed.

1. Size the leverage from a quote, not from oracle prices

This is the one step not to shortcut. A position’s leverage is bought through a solver, and the two legs do not trade at exactly their oracle ratio. If you size the borrow from oracle prices alone, the position can land above the pool’s maximum LTV. The intent is accepted, then the fill is rejected. The SDK ships two helpers that do this properly: sizeLeverageBorrow for the first estimate and projectLeverageLeg for the real numbers once you have a quote.
The projection gives you everything a confirmation screen needs:
  • exceedsMaxLtv is a hard gate. If it is true, do not post the intent.
  • usableMaxLeverage is the real ceiling at this price. It is slightly below the textbook 1 / (1 - ltv), because the trade costs something. Use it to cap your leverage slider.
  • exposureLeverage is what the open position will report, which is a little above the leverage the user asked for. Show this number, not the input.
  • minCollateralOut is the floor to post with the intent. Everything in the projection is computed at that floor, so if the floor is safe, every possible fill is.
Leave out feeBps and the projection understates LTV whenever a partner fee is configured, so a position near the limit can pass exceedsMaxLtv and still fail at fill. If you pass a partnerFee override to the open call, pass the same override to getEffectivePositionFee. The collateral and borrow reserves, with their prices and risk parameters, come from the money market data you already read for lending (collateralReserve and borrowReserve above). If the user picks an eMode category, use that category’s LTV and liquidation threshold instead of the reserve’s base values.

2. Open the position

openLeveragePosition funds the position, creates it and posts the leverage intent in one call, then reports the intent to solvers so it gets filled. The deposit needs one token approval first, and the SDK knows which spender that network uses:
Two details worth building your UI around:
  • Resolving means the intent is live, not that the leverage has arrived. The solver fills it right after. Track it with the pending state in the next section.
  • side: 'debt' opens from the borrow asset instead. A user holding only the debt token (a stablecoin, for example) can end up in a leveraged collateral position without ever buying the collateral. Pass collateral and totalInput instead of borrowToken and borrowAmount, and size it with side: 'debt' in the request.
The approval goes to the user’s own hub wallet on Sonic, or the asset manager on other networks, never to a shared position contract. The deposit lands at the address the position will be created at, and the transfer and the create happen together.

3. Track it

Positions are owned by the user’s hub wallet, and listPositionsForUser resolves that from the address the user connected with.
  • getPositionAccount is the live snapshot: collateral, debt, maximum LTV and health factor. Its ltv is the most the account may borrow against, so compute the position’s current LTV from totalDebtBase / totalCollateralBase. A health factor below 1 means the position can be liquidated, so this is the number to warn users about.
  • getPositionPendingState tells you whether an operation is in flight (isLive) or finished and waiting to be cleaned up (needsSettle). A position accepts one operation at a time.
  • getPositionCollateralBalance gives the exact collateral amount. Use it, not the USD figures, whenever an amount goes into a transaction such as a full close.

4. Adjust and close

Every change to a position runs as the owner’s hub wallet. You build the call, then hand it to one of two methods depending on whether it posts an intent: The split is enforced by the compiler: the builders return branded types, and each method only accepts its own kind. Sending a leverage change down the path that does not notify solvers is a type error, not a silent expiry.
Withdraw, settle and cancel complete on the hub right away, so they go through runLeveragePositionOperation:
There are two ways out of a position:
  • Into the collateral. Decrease leverage by enough collateral to cover the debt plus a small margin, since interest keeps accruing until the fill and any dust debt left behind blocks a full withdrawal. Wait for the fill, check that totalDebtBase is zero, then withdraw the rest to any address.
  • Into the debt token. Decrease leverage with the collateral balance in one operation, net of the position’s fee: read feeBps from getPositionInfo and sell balance * 10_000 / (10_000 + feeBps), because the fee is given up on top of the input and the whole balance cannot settle when it is nonzero. The debt is repaid and the remaining equity, now in the debt token, is swept to the owner by buildSettlePosition.
A full exit cannot half-happen: if the fill would not cover the debt, the whole operation reverts and the position stays as it was. Defending a position never has to wait for a solver either. Adding collateral or repaying debt works directly against the position’s account in the money market.

In React

@sodax/dapp-kit wraps every call above in a hook, and its write hooks refresh the position reads after each action.
The others follow the same pattern: useLeveragePositionPending, useSubmitLeveragePositionIntent and useRunLeveragePositionOperation.

Funding from other networks

Positions live on the Sonic hub, but the user does not have to. Set srcChainKey to another supported network and the same calls deposit from there, create the position and post the intent in one relayed message. A refund for an intent that never fills goes back to the network and address the user funded from.
Opening and operating positions from a network other than Sonic is marked experimental in SDK 2.2.0. It is fully exercised on Sonic. Verify a non-Sonic origin against your own deployment before putting user funds through it. Bitcoin is not supported as a funding source for positions yet, and the SDK refuses it with a validation error.

Where to go next