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 = trueon 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
- Want the pair asset without selling? Use the vault loan path (borrow)
- Borrowed against your stake? Redeploy the pair asset into trades, predictions, or token creation
- Stacking multiple paths? Read Module 12: staking is Path A
- Need the regular (non-vault) loan flow? See Module 05
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.jsonmains[].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'sdecimals().
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:
- Confirm the main's balance >= amount (use
balanceOf()) - Amount >=
minStake()(1 STASIS today), elseBelow 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 claimat 0). With a loan open, only the part not needed as collateral can leave.compound(main)adds them to the stake (Nothing to compoundat 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.
| Param | Type | Description |
|---|---|---|
amount | bigint | STASIS to unstake (18 decimals) |
toMainPair | boolean | Sell 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 |
minMainPair | bigint | Minimum 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
| State | What to call |
|---|---|
| No active loan | borrow(main, stasisAmount, days) |
| Active loan exists | addToLoan(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 time | extendLoan(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
| SDK | Contract | Returns |
|---|---|---|
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 totals | pools(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.
| Situation | Adjustment |
|---|---|
| Lots of attractive trades and new markets | Keep more liquid |
| Platform is quiet, capital sitting idle | Stake more |
| Want to borrow meaningfully | Stake enough that the borrowable amount is worth the 2% origination fee |
| First time staking | Start 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
| Action | Fee |
|---|---|
| Stake | 0% (gas only) |
| Unstake to STASIS | 0% (gas only) |
| Unstake to the pair asset | A normal sale: your trade fee plus slippage |
| Claim / compound | 0% (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-trip | About 1.2% in fees + variable slippage both ways |
| Vault loan origination | 2% flat (today) |
| Vault loan interest | 0.005% per day (prepaid, no refund on early repayment) |
| Loan extension | 0.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
| Error | Triggered by | Fix |
|---|---|---|
"Below min stake" | stake() under minStake() | Increase the stake amount |
"Zero amount" | unstake() or a pledge of 0 | Pass an amount above 0 |
"Insufficient stake" | unstake() above the stake | Unstake at most freeToUnstake(main, user) |
"Collateral in use" | unstake() would leave less than the pledge | Repay the vault loan first, or unstake less |
"Nothing to claim" / "Nothing to compound" | claim() / compound() with no rewards | Wait for rewards (earned(main, user)) |
"Insufficient collateral value" | borrow() or addToLoan() above stake + rewards − pledged | Stake more, or pledge at most getAvailableStasis(main, user) |
"Contract low on liquidity" | That amount is too large right now | Try less, or wait |
"Position active. Use addToLoan" | borrow() when a loan already exists | Use addToLoan() |
"Hook not registered" | The hub has not registered this vault | Nothing the user can do |
"No active loan" | addToLoan(), repay() or extendLoan() without a loan | Borrow first |
"No stable received" | The loan paid out nothing | Pledge more |
"Collateral short" | The repay returned less than the pledge | Read the loan with getVaultLoan |
"Must be refinance if not extending time" | extendLoan(0, …) without refinance = true | Add days, or refinance |
"Pay in stable or refinance, not both" | extendLoan with payInMainPair and refinance both true | Pick one |
"Main not on this vault" | stake(), borrow() or addToLoan() on a main the swap does not list with this vault | Read SWAP.pairs(main).staking |
"not possible" | The main refused the extension | Check the fee with extensionEligibility |
"Duration too short" | addToLoan() with 10 days or fewer left | Call 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. Readpositions(main, user)first. If itshubis not the zero address, a loan is open: useaddToLoan(). - Calling
client.loans.extendLoanon a vault loan: it fails (Invalid loan); use the vault's ownextendLoan(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
- Module 04: Trading: Buy STASIS with
wrapTokens=trueto skip the separatestake()step - Module 05: Lending: Compare vault loans vs regular hub loans
- Module 11: Token Mechanics: Why STASIS is up-only, the Stable+ ratchet
- Module 12: Strategy & Stacking: Path A, staking as the foundation of multi-stack capital efficiency
- Module 18: SDK Reference: Full vault method index and error reference
- Module 19: FAQ: Conversational answers on rewards, vault math, and unstaking