Start Here
BSKUSD is a dollar backed by a basket of stablecoins held in the contract: USDT, USDC, DAI and PYUSD. Any BSKUSD can be redeemed for any coin in the basket at any time.
sBSKUSD is staked BSKUSD. sBSKUSD holders receive 95% of the pool's fees. The term is 30 days.
Where the yield comes from
When stablecoin prices drift apart, bots swap through the pool and pay the 0.02% swap fee. That is the only source of yield: no emissions, no subsidies, no governance token.
Leaving before 30 days returns only what you put in; the income stays in the vault for the remaining stakers.
The three main risks
- One coin depegs. Up to 50% of the basket can end up in the coin that lost value.
- An issuer blocks the pool's address. USDC, USDT and PYUSD can freeze an address — that coin's share is unavailable until it is rotated out.
- A bug in the contract. The code is immutable: a bug cannot be fixed, only migrated away from. Worst case — total loss.
Details with numbers — in Risks.
What BSKUSD is not
- not an algorithmic stablecoin — every BSKUSD is backed by coins held in the contract;
- not an LP token exposed to volatile assets — the basket holds only dollar stablecoins;
- not an emission token — new BSKUSD appear only against deposited coins and as fees.
Live numbers
TVL, daily volume and 30-day yield are on the Dune dashboard. Basket composition and rates are in the app.
Where next
2. How The Pool Works
The mechanics without reading code. Each topic: an explanation, the numbers, and a formula at the end for those who need it.
The basket
The basket holds 4 coins. Addresses and decimals are read from the contract:
| Index | Coin | Address | Decimals | Status |
|---|---|---|---|---|
| Reading the contract… | ||||
Why these coins. They are the largest fiat-backed dollar stablecoins from different issuers (Circle, Tether, Paxos, Sky). A problem at one issuer affects only that issuer's share of the basket.
How a new coin is added. The administrator announces it (schedule_add_token); after 14 days there is a 14-day window to add it. The basket can hold up to 6 coins. Each new coin lowers the share cap: with 5 coins it is 40%.
Price and invariant
The swap rate follows from the pool's balances through the StableSwap formula — there is no oracle. While the coins are roughly equal, the rate is almost 1:1. The more uneven the basket, the cheaper the over-supplied coin becomes in the pool: that is how the pool pays bots to rebalance it.
How long the rate stays near 1:1 is set by the amplification A. Now A = … (ann = …). The higher A, the flatter the rate under imbalance.
| Share of one coin in the basket | Its rate to the others | Deviation from 1:1 |
|---|---|---|
| Calculating… | ||
Invariant (as in _get_d): ann·Σx + D = ann·D + D^(n+1) / (n^n·Πx), where ann = A·n and x are balances in 18 decimals. The swap rate is the _get_y solution for the new balance.
Deposit (mint)
Deposit one coin (mint) or all coins in the pool's current proportion (mint_proportional). The deposit fee is 0%. You receive as much BSKUSD as the invariant D grows: almost 1:1 when the basket is even; under imbalance, depositing the over-supplied coin gives slightly less and the scarce coin slightly more.
| Example: deposit $10,000 of one coin into a $1,000,000 pool | BSKUSD received |
|---|---|
| Calculating… | |
Withdraw (burn)
Burn BSKUSD into one coin (burn) or into all coins proportionally (burn_proportional). The withdrawal fee is 0.02%.
- A paused coin can still be withdrawn. A pause only closes deposits and swaps into that coin.
- A coin that cannot be transferred (the issuer blocked the pool). Withdrawing into that coin reverts; other coins work. A proportional withdrawal goes through and the untransferred part is queued — collect it later with
claim(token, to).
Swap (exchange)
Swapping one basket coin for another costs 0.02%. get_dy(i, j, dx) quotes it with the fee included. The contract requires the swap fee to be no higher than deposit plus withdrawal: otherwise a swap would cost more than "deposit one coin and withdraw another", and bots would route around it.
Share cap — the main protection
No coin may exceed 50% of the basket (cap = 2 ÷ number of coins, at most 60%). When a coin depegs, bots bring it into the pool and take the healthy ones. The cap stops this: no more than 50% of the bad coin can enter the basket, so at least half of the backing stays in other coins.
An operation that would push a coin past the cap reverts with error 15. Operations that reduce the imbalance always pass. Near the limit the app shows the maximum amount.
Slot floor
No coin may fall below 0.5% of the average share (D ÷ n × 0.5%). This keeps the pool's math away from division by near-zero and prevents fully draining one coin. Violation — error 16.
Where fees go
Fees do not stay in the pool as coins; they become BSKUSD:
- swap — the pool mints new BSKUSD equal to the growth of D from the fee;
- deposit and withdrawal — the fee is taken in BSKUSD from the operation's amount.
This BSKUSD is split at once: 95% to the sBSKUSD vault (…), 5% to System Revenue (…). If no vault is connected or it has no shares, the whole fee goes to System Revenue.
Virtual price
get_virtual_price() = D ÷ BSKUSD supply. It is the backing ratio: invariant dollars per 1 BSKUSD. It is at least 1 and barely moves. The swap fee is minted as new BSKUSD for the same growth of D, so virtual price does not show yield. Staking yield is measured by the growth of the sBSKUSD share price — see section 3.
3. Staking sBSKUSD
- The term is 30 days. Leaving earlier returns only what you put in.
- The yield is not fixed. It depends on the pool's volume and can be zero.
- Transferring sBSKUSD restarts the receiver's term.
How to enter
- Let the vault spend BSKUSD:
BSKUSD.approve(vault, amount). - Deposit:
deposit(amount, receiver). On the Staking page it is one button.
You get fewer sBSKUSD shares than the BSKUSD you deposit: the share price grows with every fee, so one share is worth more than 1 BSKUSD. There is no minimum. The very first deposit into the vault burns 10⁶ shares (10⁻¹² sBSKUSD) — a guard against rounding attacks.
How yield is calculated
Fees arrive in the vault as BSKUSD. The number of shares stays the same while the vault's BSKUSD grows — so each share's price rises (totalAssets ÷ totalSupply).
Yield ≈ staking share × pool fees per year ÷ BSKUSD staked
Example (a calculation, not a promise). A $5M pool with daily swaps of 150% of TVL ($7.5M) at 0.02%: $1,500 of fees a day. Stakers get 95% — $1,425 a day. With $4M staked that is ≈ 13% a year. A $10,000 position earns ≈ $107 in 30 days. At half the volume — half the yield.
Yield changes day to day because it depends on how many swaps happened. On a quiet day it can be close to zero.
Where to see the yield
On the dashboard — for 7 and 30 days. It is calculated from the share price: (price today ÷ price N days ago − 1) × 365 ÷ N. Past yield does not guarantee future yield.
The 30-day term
The term counts from the deposit. Leaving before it ends returns only the principal; the income your position earned stays in the vault and is shared by the remaining stakers. After the term the full share value is paid and no new term starts — you can leave at any time.
| Example: 10,000 BSKUSD deposited, 100 BSKUSD earned over the period | You receive |
|---|---|
| Leave on day 20 | 10,000 BSKUSD |
| Leave on day 40 | 10,100 BSKUSD |
Topping up
The start date is averaged by amount: new start = (P × start + X × now) ÷ (P + X), where P is the principal and X the top-up.
- Position not yet matured. 10,000 on day 25, top up 10,000: the start moves from day 0 to day 12.5, unlock to day 42.5 — 17.5 days from now.
- Matured position. It is pinned to "exactly matured": its value becomes the principal and start = now − 30 days. A top-up X to a position P delays unlock by
X ÷ (P + X) × 30days: an equal amount — 15 days.
Transferring sBSKUSD
sBSKUSD can be transferred. The matching part of the principal moves with the shares (the full value if the sender has matured). But the receiver's term for the incoming shares starts at the transfer. For trading sBSKUSD on a DEX: the buyer gets the position's principal and can take its income only after 30 days.
How to leave
- Check the position:
position(owner)returns shares, value, principal, start date, unlock date, payout now and a "term complete" flag.preview_exit(owner, shares)gives the exact payout for the chosen shares. - Leave:
redeem(shares, receiver, owner)orwithdraw(assets, receiver, owner).
The standard ERC-4626 previewRedeem, previewWithdraw and convertToAssets do not know the owner. Before the term they overstate the payout (full value instead of the principal). Owner-specific numbers come from preview_exit, maxWithdraw and position.
On to stablecoins
After leaving, burn BSKUSD into any basket coin (0.02%) or into all of them proportionally. Full path "sBSKUSD → dollars": leave the vault (no fee) → leave the pool (0.02%).
What the vault does not have
- no administrator and no pause — nobody can stop withdrawals or change the rules;
- the term is set at deployment and never changes;
- no support for fee-on-transfer or rebasing tokens (the vault works only with BSKUSD).
4. Economics And Governance
The answer to "who can change my terms, and how". Everything is published here: the completeness of this section is what makes the rules a commitment rather than an intention.
Fee split
Stakers 95%, system 5%. System Revenue: … (fixed in the contract forever). The system's share pays for audits, bug bounties and development.
The share ladder
The contract allows a staking share from 5% to 95%, moving at most 10 points per 7 days. The ladder below is a narrower rule the team commits to.
| Stage | Condition | Stakers | System |
|---|---|---|---|
| 0 — Launch | from day one | 95% | 5% |
| 1 — Growth | TVL ≥ $2M for 30 days in a row, yield ≥ 9% | 85% | 15% |
| 2 — Maturity | TVL ≥ $5M for 30 days, yield ≥ 8% | 75% | 25% |
| 3 — Scale | TVL ≥ $10M for 30 days, yield ≥ 8% | 65% | 35% |
| 4 — Stability | TVL ≥ $25M for 30 days, yield ≥ 7% | 55% | 45% |
| Limit | never | not below 50% | not above 50% |
Three rules:
- Only at a milestone. Average TVL at the milestone for 30 days in a row, and at least 4 weeks since the last change.
- Yield guarantee. The share drops only if the expected staker yield stays at or above 7% afterwards. Otherwise the step is postponed, and that is announced.
- Up is always allowed. If yield stays below 5% for two weeks, the staking share goes up one step.
Check it yourself: TVL and 30-day yield are on the dashboard; an announced split change is visible in the contract (pending_system_revenue_bp, pending_split_eta) and in the app under "Upcoming changes".
Fees
Now: deposit … · swap … · withdraw ….
Contract limits: each fee at most 0.06%; a swap never costs more than deposit plus withdrawal.
Deposits are always 0%. Swaps never above 0.04%. The swap fee moves to 0.03% when daily volume is steadily above 100% of TVL and more than half of the trades come from outside bots.
Timelocks
Every change is first announced in the contract, then waits, then has a window to be applied. If the window is missed, the change does not happen.
| What changes | Notice | Window | Step limit | Where to see it |
|---|---|---|---|---|
| Fees | 7 days | 14 days, anyone can apply | each ≤ 0.06% | pending_fees() |
| Staking share | 7 days | 14 days, anyone | ≤ 10 points per step, system 5–95% | pending_split_eta |
| Amplification A | 1 day | 2 days, administrator; then a 3-day ramp | at most ×2; steps 7 days apart | pending_ann_eta |
| New coin | 14 days | 14 days, administrator | up to 6 coins | pending_token_eta |
| Coin rotation | pause ≥ 7 days + 14 days | 14 days, anyone | the new coin is deposited in advance and visible in the contract | rot_token(i), rot_eta(i) |
| Administrator | until accepted | — | the new address must accept | pending_admin |
All announced changes with dates are shown in the app, "Upcoming changes".
Roles
| Role | Who | What it can do |
|---|---|---|
| admin | … · … | announce fees, split, A, a new coin; pause and unpause; run rotations; set the pauser; hand over the role |
| pauser | … | only pause a coin |
| System Revenue | … | only receives its share of fees; the address is immutable |
The administrator can — with notice
- change fees within 0.06%;
- change the staking share in 10-point steps (5–95%);
- change A at most ×2 per step;
- add a coin (14 days);
- replace a coin through rotation (pause ≥ 7 days + 14 days);
- pause a coin immediately.
Can never
- withdraw the pool's reserves;
- block withdrawals (burn) — they work even when paused;
- change the staking vault or the System Revenue address;
- change the staking term;
- take more than 95% of fees;
- set a fee above 0.06%;
- upgrade the code — there is no proxy.
Amplification
Now A = …. For a staker: a higher A gives a flatter rate, more volume and more fees — but in a depeg the bad coin fills the pool faster (up to the cap). ann range: 120 – 999,960, a multiple of 60; at most ×2 per step, no more than once in 7 days, with a 3-day ramp. A can change only when the basket is even (each coin 70–130% of the average share).
Change history
| Date | What changed | Why | Transaction |
|---|---|---|---|
| No changes since launch yet. All contract events are on the contract page in the explorer. | |||
5. Risks
Each risk in one format: what can happen → what the contract does → what the team does → what a staker loses in the worst case. No softening.
One coin depegs
An issuer blocks the pool's address
claim.
TeamRotate the frozen coin to another stablecoin.
Worst caseThat coin's share (up to 50%) is frozen until rotation or unblocking.A bug in the contract
Yield below expectations
Early exit
Parameter changes by the administrator
Key compromise
rot_token), enough time to leave.Regulatory risk
Known limitations
What the contract deliberately does not do:
- no oracle — the rate follows only from balances;
- no automatic pause on a depeg — the pauser or administrator pauses;
- no support for fee-on-transfer or rebasing tokens;
- the vault's
previewRedeemdoes not know the term and overstates the payout before it; - a coin cannot be removed from the basket — only replaced by rotation.
6. Developers And Bots
Everything here is copyable. Connect in an hour, without asking in chat.
Addresses
| Network | … · chain id … |
| BSKUSD pool (also the BSKUSD token) | … |
| sBSKUSD vault | … |
| admin | … |
| pauser | … |
| System Revenue | … |
Coins with indices and decimals — in The basket.
ABI
Full JSON: pool v5.13.0 · vault v1.3.0. Minimal swap ABI:
[ "function coins(uint256) view returns (address)", "function N_COINS() view returns (uint256)", "function balances(uint256) view returns (uint256)", "function get_dy(int128 i, int128 j, uint256 dx) view returns (uint256)", "function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy) returns (uint256)", "function fees() view returns (uint256, uint256, uint256)", "function fee() view returns (uint256)", "function A() view returns (uint256)", "function paused(uint256) view returns (bool)" ]
StableSwap-compatible interface
Routers and bots that already work with StableSwap pools trade this pool without changes.
| Function | Same as StableSwap | Difference |
|---|---|---|
get_dy, exchange | yes, int128 indices | — |
coins, balances, N_COINS | yes | balances are in each coin's own units |
A, A_precise | yes | A = ann ÷ n |
fee | yes, 10 decimals | the swap fee; deposit and withdrawal are in fees() |
get_virtual_price | yes | D ÷ BSKUSD supply |
add_liquidity / remove_liquidity | no | use mint / burn and the proportional versions |
| LP token | no separate one | BSKUSD is the LP token — the pool is the token |
Quote and swap
Order: get_dy → min_dy with a tolerance → approve → exchange. Gas: 166–191k per swap, 111–134k per single-coin deposit or withdrawal, ≈ 245k for a basket deposit or withdrawal (measured on the USDT, USDC, PYUSD and DAI code, 4 coins).
from web3 import Web3
w3 = Web3(Web3.HTTPProvider(RPC_URL))
pool = w3.eth.contract(address=POOL, abi=SWAP_ABI)
erc20 = w3.eth.contract(address=pool.functions.coins(0).call(), abi=ERC20_ABI)
dx = 10_000 * 10**6 # 10,000 of coin 0 (6 decimals)
dy = pool.functions.get_dy(0, 1, dx).call() # quote, fee included
min_dy = dy * 9_990 // 10_000 # 0.1% tolerance
erc20.functions.approve(POOL, dx).transact({"from": ME})
pool.functions.exchange(0, 1, dx, min_dy).transact({"from": ME})interface IBasketPool {
function get_dy(int128 i, int128 j, uint256 dx) external view returns (uint256);
function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy) external returns (uint256);
function coins(uint256 i) external view returns (address);
}
function swap(IBasketPool pool, int128 i, int128 j, uint256 dx, uint256 slippageBp) external returns (uint256) {
uint256 dy = pool.get_dy(i, j, dx);
uint256 minDy = dy * (10_000 - slippageBp) / 10_000;
IERC20(pool.coins(uint256(int256(i)))).approve(address(pool), dx);
return pool.exchange(i, j, dx, minDy);
}Slippage. The price is get_dy(i, j, dx) ÷ dx adjusted for decimals. Compare it with a quote for a small amount: the difference is the trade's price impact.
Deposit and withdraw
| Operation | Quote | Call |
|---|---|---|
| Deposit one coin | quote_mint_net(i, amount) | mint(i, amount, min_bsk) |
| Deposit the basket | quote_mint_proportional(bsk) | mint_proportional(amounts[6], min_bsk) |
| Withdraw one coin | quote_burn(i, bsk) | burn(i, bsk, min_out) |
| Withdraw the basket | quote_burn_proportional(bsk) | burn_proportional(bsk, min_out[6]) |
| Collect a queued payout | claimable(token, me) | claim(token, to) |
Quotes revert exactly where the operation would, with the same error code. Indices in mint/burn are uint256; arrays have 6 elements (pad with zeros).
Trade limits
- Share cap (15). After a trade,
x_i ÷ Σx ≤ capfor the coin whose share grows. Maxdxfor coin i:(cap × Σx − x_i) ÷ (1 − cap)in 18 decimals (ignoring the outgoing coin — for the exact value binary-search withget_dy). - Slot floor (16). Every coin after the operation >
D ÷ n × 0.5%. - A swap cannot reduce D (31). A rounding guard: a swap that would shrink the invariant reverts.
- Pause (23). A paused coin cannot be deposited or sold to the pool; it can be bought and withdrawn.
Error codes
A refused call reverts with Err(uint256), selector 0x39eabce6. The 10 most common first; the full list of every pool and vault code follows.
| Code | Why | What to do |
|---|---|---|
| 15 | The coin would exceed the share cap | Reduce the amount or deposit the whole basket |
| 16 | A coin would fall below the slot floor | Withdraw less of that coin or proportionally |
| 25 | Output below your minimum | Refresh the quote, check the tolerance |
| 23 | The coin is paused | Pick another coin; withdrawing into it works |
| 12 | Transferring the coin into the pool failed | Check approval and balance |
| 14 | The coin cannot be sent from the pool | Withdraw into another coin or proportionally (part goes to claim) |
| 21 | The pool is not launched yet | Wait for launch |
| 31 | The swap would reduce the invariant | The amount is too small or hit rounding — increase it |
| 60 | A basket withdrawal would deliver under 99% of what is owed | Withdraw less or coin by coin |
| 72 | One coin of a proportional withdrawal is below its minimum | Update min_out |
All pool codes bskUSD v5.13.0
| Code | What happened | Where | What to do |
|---|
sBSKUSD vault codes v1.3.0
The vault uses the same Err(uint256) format, but its own numbering: code 2 in the vault is not code 2 in the pool. Check which contract the call went to.
| Code | What happened | Where | What to do |
|---|
Reading a code: revert data = 0x39eabce6 + the code as a 32-byte number. Example: 0x39eabce6…000f = 15. Code 65 is not used in v5.13.0.
Events
| Event | Fields |
|---|---|
| TokenExchange | buyer, sold_id, tokens_sold, bought_id, tokens_bought, fee_bsk |
| Mint | user, token_in_idx, token_in_raw, bsk_gross, bsk_fee, bsk_net |
| MintProportional | user, bsk_net, bsk_fee |
| Burn | user, token_out_idx, bsk_in, bsk_fee, bsk_effective, token_out_raw |
| BurnProportional | user, bsk_in, bsk_fee |
| ClaimQueued / Claimed | token, funder, amount_raw |
| FeesScheduled / FeesApplied / FeesCancelled | mint_bp, swap_bp, burn_bp, eta |
| SplitScheduled / SplitApplied / SplitCancelled | new_system_revenue_bp, eta / old, new / cancelled_bp |
| AnnChangeScheduled / AnnChanged / AnnChangeCancelled | new_ann, eta / old_ann, new_ann |
| AddTokenScheduled / TokenAdded / AddTokenCancelled | token, amount_raw, eta / index, bsk_net, ann |
| TokenPaused / TokenUnpaused | index |
| RotationFunded / RotationScheduled / RotationExecuted | index, new_token, amount_raw, eta / old_token, old_raw, new_raw, old_sent, surplus_sent |
| RotationCancelled / RotationReclaimed | index, token, funder, returned |
| AdminProposed / AdminChanged / PauserSet / VaultSet | role addresses; VaultSet — staking_vault, system_revenue_bp |
| Vault: Deposit / Withdraw | ERC-4626 standard |
| Vault: Exit | owner, receiver, shares, assets, forfeited, matured — how much income was left in the vault on an early exit |
The sBSKUSD vault and ERC-4626
previewRedeem,previewWithdraw,convertToAssetsdo not know the owner and show the full price before the term. Usepreview_exit(owner, shares),maxWithdraw(owner),position(owner).maxRedeem(owner)= 0 for an unmatured position with no principal.- A transfer restarts the receiver's term. For lending markets and protocols like Pendle: shares taken as collateral or wrapped are under a 30-day term from the transfer, and an early exit returns only the principal.
Adapters
A DefiLlama adapter (TVL and yield) and an example for aggregator routers are in the repository.
Source code
Code is on GitHub, version tags v5.13.0 / v1.3.0. Vyper 0.4.3, evm-version prague; the pool uses optimize codesize. To verify bytecode: vyper -f bytecode_runtime bskUSD_v5.13.0.vy and compare it with the code at the contract address.
7. Security
| Audits | Reports will be published in full: who, when, which commit, a findings table with severity and status (fixed / accepted as a risk — with an explanation). |
| Bug bounty | A program on Immunefi: payouts by tier, scope — both contracts. |
| Immutability | No proxies, no upgrades. Fixed forever: the System Revenue address, the vault (once connected), the staking term, fee limits (0.06%) and split limits (5–95%). |
| Keys | admin — …; pauser — a separate key for instant pauses. Key holders are listed by role. |
| Admin powers | See "Can / can never". |
| Incident plan | The pauser pauses a coin within minutes of a depeg or a block. Status is published in the official channels. What a staker can do alone: leave the vault and burn BSKUSD into healthy coins — this always works. |
| Vulnerability contact | email and PGP key — to be published |
FAQ
What backs BSKUSD and where can I check it?
The basket coins held in the pool contract. Read the balances directly with balances(i) or in the app. → The basket
Where does the yield come from if there is no token?
From swap fees paid by bots that rebalance prices. No emissions. → Yield
Why was the yield 14% yesterday and 6% today?
Income depends on daily volume: many swaps on a volatile day, few on a quiet one. Look at the 30-day yield. → Yield
What happens if I leave on day 20?
You get your principal; the 20 days of income stay with the other stakers. → Term
I topped up — why did my unlock date move?
The start date is averaged by amount: a top-up is new money with its own term. An equal amount on day 25 moves the unlock to day 42.5. → Topping up
Can I sell sBSKUSD on a DEX? What does the buyer get?
Yes. The buyer gets the principal, and their term starts at the transfer. → Transfers
What if USDT or PYUSD blocks the pool?
That coin can no longer be withdrawn; other coins work. Its share is frozen until rotation. → Risks
What happens if one coin depegs? How much can I lose?
No more than that coin's share, which the 50% cap limits. A coin at $0.50 → backing ≈ 75%. → Risks
Can the admin take the money or stop withdrawals?
No. There is no function to withdraw reserves, and withdrawals work even when paused. → Roles
How fast can fees and the staking share change? How do I know in advance?
No sooner than 7 days after the announcement. Announcements are visible in the contract and in the app under "Upcoming changes". → Timelocks
Why does previewRedeem show more than I receive?
It does not know the owner and shows the full price. Before the term you receive only the principal — use preview_exit. → How to leave
How do I get USDC back instead of BSKUSD?
Leave the vault, then burn BSKUSD into USDC (0.02%). → Withdraw
What is claim and when do I need it?
If a coin could not be sent to you on withdrawal, it waits in a queue. Collect it with claim(token, to) or the button in the app. → Withdraw
Who audited it?
Reports are published in full in Security.
Is there a governance token or an airdrop?
No, and none is planned.
Why is depositing free but withdrawing is not?
Deposits add reserves and benefit every holder. The withdrawal fee keeps "deposit + withdraw" from undercutting the swap fee. → Swap
How do I connect a bot?
Addresses, a minimal ABI and a swap example are in section 6.