Module 06: Staking (STASIS Vault)

One vault serves every main; each main has its own pool in it. Stake the main token 1:1, earn rewards in the main token, and borrow the main's pair asset against the stake. The stake is a position, not a token, and the pledged part keeps earning while it backs a loan.

Prerequisites

  • Hold the main (STASIS on the default main): buy it via trading; wrapTokens = true on a buy of the main stakes it in the same transaction
  • Wallet funded with the pair asset (→01), client initialized (→02)
  • Understand Stable+ ratchet mechanics: STASIS is the canonical Stable+ token

Next steps after staking

Examples use the default main, STASIS (the USD main), whose pair asset is USDG (6 decimals). There is one vault for every main (SWAP.pairs(main) → (pairAsset, staking, active), /contracts.json mains[].vault); each main has its own pool and every vault call takes the main first. On another main read "STASIS" as that main and "USDG" as its pair asset, and use the pair asset's decimals().


Why Stake

10% of the base part of every fee paid on this main's tokens goes to its vault: trades, bets and loan-side fees. The vault buys the main with it and credits it to stakers as rewards, in proportion to their stake. Factory-token trades on this main count; trades on another main feed that main's vault. Your stake does not grow by itself: rewards wait in earned(main, user) until you claim(main) them or compound(main) them into the stake.

The second reason: the stake is collateral. borrow pledges part of it and pays out the pair asset; deploy that into trades, markets, or more staking. The pledged stake keeps earning while it backs the loan. Capital works twice simultaneously.

Yield depends on two numbers that both move: fee volume on the main and the total stake. More stakers split the same fees more ways; more volume grows the pot. Nothing fixes the rate. The main's price cannot fall in its pair asset, but on a main whose pair asset is not a dollar stable its dollar value can.


How It Works

The vault holds the main one for one: there is no receipt token, no share price and no lock step. Two layers:

Layer 1: Stake and earn:

STASIS → stake(amount) → your position's `staked` (1:1) rewards → claim() → STASIS to your wallet rewards → compound() → added to `staked` staked → unstake(amount, false, 0) → STASIS back 1:1 staked → unstake(amount, true, minMainPair) → sold through SWAP for the pair asset (a normal taxed sale)

Layer 2: Borrow:

staked → borrow(stasisAmount, days) → pledges that much stake, pays out the pair asset pair asset → repay() → loan cleared and pledge released in the same call

Quick exit: unstake(amount, true, minMainPair) sells the unstaked STASIS for the pair asset in the same transaction, at your own trade fee.


Actions

The SDK names below are those of the SDK hand-over (client.vault(main?), one vault per main; no main = the default main). The contract call is given beside each.

Stake: client.vault(main).stake(amount)

Contract: stake(address main, uint256 amount). Stakes the main 1:1. Approve the main to the vault first (the SDK does it).

Pre-flight:

  1. Confirm the main's balance >= amount (use balanceOf())
  2. Amount >= minStake() (1 STASIS today), else Below min stake
// JS: stake 100 STASIS
const result = await client.vault().stake(parseUnits("100", 18));
console.log("Staked:", result.hash);
# Python: stake 100 STASIS
result = client.vault().stake(100 * 10**18)
print("Staked:", result["hash"])

Fee: 0% to stake. The cost is the base trade fee (0.60% today, 0.54% with a referrer) plus slippage paid when you bought the STASIS.


Claim and Compound

  • earned(main, user): unclaimed rewards, in STASIS.
  • claim(main) pays them out (Nothing to claim at 0). With a loan open, only the part not needed as collateral can leave.
  • compound(main) adds them to the stake (Nothing to compound at 0).
  • Unclaimed rewards already count as collateral (collateralOf(main, user) = stake + unclaimed rewards).
const vault = client.vault();
if ((await vault.earned(wallet)) > 0n) await vault.compound(); // or vault.claim()

Unstake: client.vault(main).unstake(amount, toMainPair?, minMainPair?)

Contract: unstake(address main, uint256 amount, bool toMainPair, uint256 minMainPair). Takes STASIS out of the stake, 1:1.

ParamTypeDescription
amountbigintSTASIS to unstake (18 decimals)
toMainPairbooleanSell it through SWAP for the pair asset in the same transaction. It is a normal sale at your own tax rate, not fee-free. Default: false
minMainPairbigintMinimum pair asset received when toMainPair is true

Pre-flight: amount ≤ freeToUnstake(main, user). Above the stake it reverts Insufficient stake; when what is left plus unclaimed rewards would be less than the pledge it reverts Collateral in use (repay the loan first). Part of a stake can leave while a loan is open. There is no vote lock.

// Unstake to STASIS
const result = await client.vault().unstake(amount);

// Unstake and sell for the pair asset (single transaction)
const result2 = await client.vault().unstake(amount, true, minMainPair);

Vault Loan: Borrow, Manage, Repay

Vault loans differ from regular loans in one key way: the pledged stake keeps earning rewards. Fees are identical: 2% origination + 0.005%/day. There is one loan per wallet per main: no simultaneous second loan on the same main.

Full sub-flow:

Step 1: Borrow the pair asset against your stake

borrow(main, stasisAmount, days) pledges stasisAmount of your stake plus unclaimed rewards (STASIS units, 18 decimals) and pays out the pair asset: value × (1 − 2% − 0.005% × days), STASIS valued at its floor value (for STASIS, its price). There is no lock step. It needs the vault's liquidation hook on the hub (Hook not registered: nothing the user can do).

Duration: minDaysLoan() to maxDaysLoan() of the main (10 and 1000 today). Take the minimum and extend as needed.

// Borrow against 50 STASIS of stake for 30 days
const borrowResult = await client.vault().borrow(parseUnits("50", 18), 30n);
borrow_result = client.vault().borrow(50 * 10**18, 30)

To find your maximum borrow capacity:

const vault = client.vault();
const available = await vault.getAvailableStasis(wallet); // stake + unclaimed rewards - pledged
await vault.borrow(available, 10n);

Borrowing everything available: nothing can be unstaked until the loan is repaid. Right after a borrow the debt equals the pledge's value, so an extension with the fee added to the debt fails (Not enough collateral to cover fees) until STASIS has risen: pay the extension fee in the pair asset (payInMainPair = true).

Fee: 2% flat origination + 0.005%/day interest (prepaid). Pair asset received = collateral value minus the fees.

One loan per wallet per main: the critical rule

StateWhat to call
No active loanborrow(main, stasisAmount, days)
Active loan existsaddToLoan(main, additionalStasis), not borrow(). The vault calls the hub's increaseLoan, which needs more than 10 days left on the loan (Duration too short).
Need more timeextendLoan(main, daysToAdd, payInMainPair, refinance): 400x cheaper than new loan

Calling borrow() when a loan already exists reverts with "Position active. Use addToLoan". Check positions(main, user).hub first: a loan is open when it is not the zero address.

Step 2: Extend if needed

Extension costs only 0.005%/day: the same daily rate with no 2% origination fee. That makes it roughly 400x cheaper than closing and opening a new loan.

On the contract extendLoan takes (main, daysToAdd, payInMainPair, refinance); the SDK fills in the main. With payInMainPair = true you pay the fee in the pair asset (approve it to the vault); the vault checks the extension first and reverts not possible when it cannot go through. With 0 days, refinance must be true and payInMainPair false: 0 days without a refinance reverts Must be refinance if not extending time (or not possible with payInMainPair = true), and both flags together revert Pay in stable or refinance, not both. Always call it on the vault: client.loans.extendLoan(hubId) looks the id up under your own wallet and hits Invalid loan or another loan of yours.

// Extend by 30 more days: pay the fee in the pair asset
const extendResult = await client.vault().extendLoan(30n, true, false);

Rule of thumb: take short initial durations (10-14 days), extend as needed. Prepaid interest is not refunded on early repayment.

Step 3: Repay

Repays the vault loan in full on the hub the position remembers (positions(main, user).hub). Approve the pair asset to the vault for the loan's full amount; the vault pulls it. The pledge is released in the same call; there is no unlock step.

const repayResult = await client.vault().repay();
repay_result = client.vault().repay()

If the loan was liquidated

After liquidationTime a keeper liquidates the loan. Assume it can happen in the first block after the deadline. Until that transaction lands a repay or a paid extension still goes through; do not plan on the gap. The keeper closes the loan: the pledged stake that covers the debt is taken to repay it, and 10% of the rest is the liquidation fee. The remainder returns to your position in the same transaction, and the loss comes off your stake first, then off unclaimed rewards. There is nothing to claim and nothing to call.


Complete Example: Full Stake → Borrow → Repay → Exit

async function stakingOperations() {
  const client = await BasisClient.create({ privateKey: "0xYourPrivateKey..." });
  const { parseUnits } = require("viem");
  const vault = client.vault();                              // the default main's vault

  await vault.stake(parseUnits("100", 18));                  // stake(main, amount)
  await vault.borrow(parseUnits("50", 18), 30n);             // pledges 50 STASIS; pays the pair asset
  // ... deploy the borrowed pair asset ...
  await vault.repay();                                       // releases the pledge
  if ((await vault.earned(wallet)) > 0n) await vault.claim();
  await vault.unstake(parseUnits("100", 18), false, 0n);     // back to STASIS
}
def staking_operations():
    client = BasisClient.create(private_key="0xYourPrivateKey...")
    vault = client.vault()

    vault.stake(100 * 10**18)
    vault.borrow(50 * 10**18, 30)
    # ... deploy the borrowed pair asset ...
    vault.repay()
    if vault.earned(wallet) > 0:
        vault.claim()
    vault.unstake(100 * 10**18, False, 0)

Read Methods

SDKContractReturns
getPosition(user)positions(main, user)(staked, pledged, hubId, checkpoint, pending, hub); a loan is open when hub is not the zero address. staked includes the pledged part; pledged backs the open loan and cannot be unstaked
earned(user)earned(main, user)Unclaimed rewards (STASIS)
getAvailableStasis(user)getAvailableStasis(main, user)Stake + unclaimed rewards − pledged: the most borrow / addToLoan can pledge
freeToUnstake(user)freeToUnstake(main, user)What unstake can take now
collateralOf(user)collateralOf(main, user)Stake + unclaimed rewards
vault totalspools(main) → (totalStaked, totalPledged, rewardsHeld, accRewardPerStake), totalStasisAvailable(main)All positions on that main; minStake() is the stake minimum
getVaultLoan(user)positions(main, user).hub and .hubId, then hub.getUserLoanDetails(vault, hubId)The loan (the vault, not the wallet, is the borrower of record on the hub)

The stake is not a token: the vault has no balanceOf / totalSupply. Read positions(main, user)[0] / pools(main)[0].

const pos = await client.vault().getPosition(wallet);
const available = await client.vault().getAvailableStasis(wallet);
console.log(`Stake: ${pos.staked} STASIS, ${available} available to pledge`);

When and How Much to Stake

The question is not whether to stake: it is how much, given what you plan to do next.

Recommended starting split: 30–50% of STASIS holdings staked, rest liquid.

SituationAdjustment
Lots of attractive trades and new marketsKeep more liquid
Platform is quiet, capital sitting idleStake more
Want to borrow meaningfullyStake enough that the borrowable amount is worth the 2% origination fee
First time stakingStart smaller, verify the full cycle before committing large amounts

Break-even consideration: a round trip through STASIS costs two base fees (0.60% each today, 0.54% with a referrer) plus slippage both ways. A $100 position needs more than $1.20 in rewards before it is ahead. Vault staking is a days-to-weeks position, not hours.

// Estimate your round-trip cost before staking (pair-asset units).
// getAmountsOut does not include the trade fee: take getTaxRate(main, wallet) bps off each side.
const MAIN = client.mainTokenAddress; const PAIR_ASSET = client.usdbAddress; // the default main and its pair asset
const pairDecimals = await client.publicClient.readContract({ address: PAIR_ASSET, abi: erc20Abi, functionName: "decimals" });
const entry = parseUnits("1000", pairDecimals);
const tax = BigInt(await client.taxes.getTaxRate(MAIN, wallet));
const tokens = await client.trading.getAmountsOut(entry - entry * tax / 10000n, [PAIR_ASSET, MAIN]);
const gross = await client.trading.getAmountsOut(tokens, [MAIN, PAIR_ASSET]);
const back = gross - gross * tax / 10000n;
const roundTripCost = entry - back; // both taxes and the price impact both ways
// Your stake needs to earn more than roundTripCost in rewards to be profitable

Fees

ActionFee
Stake0% (gas only)
Unstake to STASIS0% (gas only)
Unstake to the pair assetA normal sale: your trade fee plus slippage
Claim / compound0% (gas only)
Entry (buy STASIS)Base fee (0.60% today, 0.54% with a referrer) + slippage
Exit (sell STASIS)Base fee (0.60% today, 0.54% with a referrer) + slippage
Full round-tripAbout 1.2% in fees + variable slippage both ways
Vault loan origination2% flat (today)
Vault loan interest0.005% per day (prepaid, no refund on early repayment)
Loan extension0.005% per day: no origination fee

Vault yield is variable. No fixed APY exists. Yield depends on the fees paid on this main's tokens and on the total stake in this vault. Both variables change continuously. There is no APY view: a rate has to be computed from the vault's YieldInjected(mainPairAmount, stasisAdded) events over totalStaked.


Errors

ErrorTriggered byFix
"Below min stake"stake() under minStake()Increase the stake amount
"Zero amount"unstake() or a pledge of 0Pass an amount above 0
"Insufficient stake"unstake() above the stakeUnstake at most freeToUnstake(main, user)
"Collateral in use"unstake() would leave less than the pledgeRepay the vault loan first, or unstake less
"Nothing to claim" / "Nothing to compound"claim() / compound() with no rewardsWait for rewards (earned(main, user))
"Insufficient collateral value"borrow() or addToLoan() above stake + rewards − pledgedStake more, or pledge at most getAvailableStasis(main, user)
"Contract low on liquidity"That amount is too large right nowTry less, or wait
"Position active. Use addToLoan"borrow() when a loan already existsUse addToLoan()
"Hook not registered"The hub has not registered this vaultNothing the user can do
"No active loan"addToLoan(), repay() or extendLoan() without a loanBorrow first
"No stable received"The loan paid out nothingPledge more
"Collateral short"The repay returned less than the pledgeRead the loan with getVaultLoan
"Must be refinance if not extending time"extendLoan(0, …) without refinance = trueAdd days, or refinance
"Pay in stable or refinance, not both"extendLoan with payInMainPair and refinance both truePick one
"Main not on this vault"stake(), borrow() or addToLoan() on a main the swap does not list with this vaultRead SWAP.pairs(main).staking
"not possible"The main refused the extensionCheck the fee with extensionEligibility
"Duration too short"addToLoan() with 10 days or fewer leftCall extendLoan() first: see Module 05 §Recovery Flows

For the full alphabetical error index across all modules, see Module 18: SDK Reference.


Pitfalls

  • Staking for hours: Round-trip fees require rewards to cover them. Think days minimum, not hours.
  • Not checking for an open loan before borrow(): One loan per wallet per main. Read positions(main, user) first. If its hub is not the zero address, a loan is open: use addToLoan().
  • Calling client.loans.extendLoan on a vault loan: it fails (Invalid loan); use the vault's own extendLoan(days, payInMainPair, refinance).
  • Re-originating instead of extending: Each new loan costs 2% origination. Extension costs 0.005%/day. Take a short loan, extend as needed: do not close and re-open.
  • Letting the loan expire: once a keeper liquidates it, the pledged stake used for the debt is gone: the loss comes off the stake 1:1 first, then off unclaimed rewards. What is left stays in your position; there is nothing to claim. Repay or extend before expiry.
  • Not calculating break-even: Use getAmountsOut() with the tax included to preview actual swap costs before entering. Factor in size and planned hold duration.

What Next

Staking is Path A in the stacking strategies guide: the foundation layer that everything else builds on.

After staking:

  • Borrow the pair asset against your stake → Lending
  • Buy more tokens with the borrowed pair asset → Trading
  • Create a new token → Token Creation
  • Bet on prediction markets → Predictions
  • See Path A in the full stacking strategies context → Strategy & Stacking

The core compound loop:

Buy STASIS → stake → borrow the pair asset → reinvest ↑ | └── rewards → compound() → more to pledge → addToLoan ──┘ extend the loan → repeat

Your stake earns from every fee on this main's tokens. The borrowed pair asset earns wherever you deploy it. Both run simultaneously. When rewards have grown the position, compound() and addToLoan() to draw more without closing the loan (it needs more than 10 days left). A refinance surplus comes from the price rise of the pledged STASIS, not from vault rewards, which accrue separately.


See Also