Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Insurance

Each Bazaar market has its own insurance fund, owned by liquidity providers (LPs) who hold its shares. The fund earns the market’s insurance fees and absorbs what liquidations lose beyond maintenance margin. This page covers the LP flow from the SDK: reading an LP’s position, quoting at the live price, depositing, and the multi-step exit (request, cooldown, lock, paying call), with insuranceExitPlan telling a UI which step the LP is on.

Use it to build an “earn” or “provide insurance” screen, or a bot that manages an LP position. The mechanics (share pricing, the target, the rate limit, the headroom rule) are described in Bazaar’s Insurance Fund page; this page says what each SDK call does with them.

Everything here is on bz.insurance (with bz connected as in Connecting), and the same functions take a context first on the insurance namespace export (insurance.depositToInsurance(ctx, ...)), along with the constants and the pure helper claimOpensAt.

Before You Start

  • Connect a price source. Shares are priced at the fund’s value at the mark, so quotes, the budget and both exit calls on a live market use the live Pyth price. Without prices on Bazaar.connect, the quotes and the budget throw a ConnectError unless you pass a price, executeInsuranceWithdrawal throws one on a live market because it needs a price update, and the exit plan leaves its estimates null.
  • Units. USDC amounts are 18-decimal bigints (1e18 = $1), floored to whole micro-USDC by the pair; use parseUnits18("250") and formatUnits18(x, 2). Insurance shares are 18-decimal too. Times are unix seconds of the chain’s clock. See Units & Constants.
  • Writes are PreparedCalls. Simulate each as the LP, then send it with the wallet (Sending Transactions). Calls that carry a price update have a priceTime and must land within 3 seconds of it, so build them right before the wallet prompt (buildAndSend).

Reading an LP’s Position

const [sharePrice, myShares, myValue] = await Promise.all([
  bz.insurance.getInsuranceSharePrice(eth.address), // USDC per share at the stored mark
  bz.insurance.insuranceShares(eth.address, user!), // 18 decimals
  bz.insurance.getInsuranceDepositValue(eth.address, user!), // what they redeem for at the stored mark
]);
console.log(`1 share = ${formatUnits18(sharePrice, 6)} USDC`);
console.log(`you hold ${formatUnits18(myShares, 4)} shares, worth ${formatUnits18(myValue, 2)} USDC`);
FunctionReturns
getInsuranceSharePrice(pair)What one share redeems for at the pair’s stored mark (1.0 while no shares exist).
getInsuranceDepositValue(pair, user)What user’s shares redeem for at the stored mark.
insuranceShares(pair, user)user’s shares. 0 for a balance a fund recapitalization wiped (see Share Epochs).
quoteInsuranceDeposit(pair, amount, { price? })Shares a deposit would mint at the live price.
quoteInsuranceWithdrawal(pair, shareAmount, { price? })USDC a redemption would pay at the live price.

The first two read the lens at the price the pair last stored, which can be minutes old on a quiet market. For anything the LP will act on, use the quotes: they compose the live price from the price source the way the oracle composes it, so they match what a call carrying that price will do.

The market’s fund as a whole (balance, free capital, the seizure earmarked against inventory, the target ratio) is in marketSnapshot(...).insurance; see Markets.

Quotes

const amount = parseUnits18("250"); // 250 USDC, in 18 decimals
if (amount < insurance.MIN_INSURANCE_DEPOSIT) console.log("The minimum insurance deposit is 5 USDC.");
const quote = await bz.insurance.quoteInsuranceDeposit(eth.address, amount);
console.log(`250 USDC buys about ${formatUnits18(quote.value, 4)} shares at ${formatUnits18(quote.price, 2)}`);

Both quotes return an InsuranceQuote:

FieldMeaning
valueShares minted (deposit) or USDC paid (withdrawal), 18 decimals.
priceThe mark it was priced at.
publishTimeThe live update’s publish time, or null when you passed price.

Pass { price } to quote at a price of your own (for “what if” displays); it must be positive.

quoteInsuranceDeposit floors the amount to micro-USDC as the pair does and runs the lens’s quoteInsuranceDeposit, the pair’s own arithmetic. It ignores the deposit’s gates (the minimum, the market’s state, the rate limit) and returns 0 where the fund’s value is nil. quoteInsuranceWithdrawal is what the shares are worth now; a locked exit’s paying call pays the lower of this and the locked amount, and while the vault holds liquidated inventory no more than the fund’s headroom.

Depositing

async function deposit(amount: bigint): Promise<string> {
  // The deposit pulls USDC from the wallet: approve the pair for exactly what it pulls.
  const draft = await bz.insurance.depositToInsurance(eth.address, user!, amount);
  const funding = await bz.allowance.usdcFunding(user!, eth.address, draft.pullsUsdc!);
  if (!funding.enoughBalance) return "Your wallet does not hold enough USDC.";
  if (!funding.enoughAllowance) {
    const approve = await bz.allowance.approveUsdc(user!, eth.address, funding.approveUsdc);
    await bz.waitForReceipt(await bz.send(window.ethereum, approve));
  }
  // Build again right before the wallet prompt: a deposit made while the vault holds liquidated
  // inventory carries a price update that must still be fresh when it lands.
  const { hash } = await buildAndSend(window.ethereum, async () => {
    const call = await bz.insurance.depositToInsurance(eth.address, user!, amount);
    const sim = await bz.simulate(call);
    if (!sim.ok) throw new CallError(sim.error!.message, sim.error);
    return call;
  });
  await bz.waitForReceipt(hash);
  return "Deposited.";
}

try {
  console.log(await deposit(amount));
} catch (e) {
  console.error(errorMessage(e)); // one sentence for the LP
}

depositToInsurance(pair, from, amount, opts?) builds the deposit. The amount is floored to whole micro-USDC and pulled from the wallet, so the wallet needs that much USDC approved to the pair (the sample approves exactly pullsUsdc) or a USDC permit passed as permitData; see Collateral, Approvals & Permits.

OptionDefaultMeaning
withPriceattach when a price source is connectedfalse sends no price update. The pair reads a price only while its vault holds liquidated inventory, and then needs a fresh one; an update it does not read has its fee refunded. true requires one (a ConnectError without a price source).
permitData"0x"A signed USDC permit, so no separate approval transaction is needed.

The builder refuses up front, with an InputError:

  • an amount under 5 USDC (MIN_INSURANCE_DEPOSIT) or above 2^128 - 1 (MAX_INSURANCE_DEPOSIT);
  • a terminated market, a market whose settlement price is fixed, and a market within 6 seconds of a scheduled cutoff;
  • a market with an ADL window pending.

The contract also refuses more than 100 deposits per user in four days (InsuranceVaultLib__TooManyRecentInsuranceDeposits) and a deposit whose price would open an ADL window. Those show up in the simulation.

The returned call’s pullsUsdc is the floored amount, and value is the Pyth fee when a price update is attached.

Leaving the Fund

An exit on a live market takes three transactions, spread over at least 13 days:

  1. Request (requestInsuranceWithdrawal): starts a 13-day cooldown. Moves no USDC.
  2. Lock (the first executeInsuranceWithdrawal in the window): once the cooldown ends, an execution window opens: 2 days on a market that trades around the clock, 4 days on a market with trading hours. The first execute call in it records what the requested shares are worth and pays nothing.
  3. Pay (the next executeInsuranceWithdrawal): from 5 minutes after the lock, it burns the shares and pays the lower of the locked amount and what they are worth then. The paying call is pushed back to 5 minutes after the last stored price that jumped 2% or more, and to 30 minutes after the last stored price that ended a gap of an hour or more.

On a terminated market the request is followed by a single execute call that pays at once.

ConstantValueMeaning
INSURANCE_WITHDRAWAL_COOLDOWN13 daysFrom the request to the window.
INSURANCE_WITHDRAWAL_WINDOW_CONTINUOUS2 daysThe window on a 24/7 market.
INSURANCE_WITHDRAWAL_WINDOW_SESSION4 daysThe window on a market with trading hours.
INSURANCE_WITHDRAWAL_CLAIM_DELAY300 sFrom the lock (and from the last 2% jump) to the paying call.
INSURANCE_WITHDRAWAL_REOPEN_CLAIM_DELAY1,800 sFrom the end of a price gap to the paying call.
REOPEN_GAP_MIN_STALENESS3,600 sA stored price after this long without one ends a gap.

withdrawalWindowOf(continuous) returns the window for a market, and claimOpensAt(lockTs, lastGapTs, lastJumpTs) is the pair’s own rule for when the paying call opens.

Requesting

requestInsuranceWithdrawal(pair, from, shareAmount) files the request. shareAmount is in shares (18 decimals) and must be positive and no more than the LP holds; the builder refuses an LP with no shares. Filing again replaces a pending request and restarts its cooldown, and a lock the earlier request made no longer counts; the call’s summary says so when a request is pending.

Shares that back an insurer termination vote cannot leave while the vote holds them. Request no more than shares - lockedForVoting from the exit plan, or the execute calls are refused with InsuranceVaultLib__SharesLockedForVoting.

The Lock and the Paying Call

executeInsuranceWithdrawal(pair, from, opts?) builds whichever execute call comes next. It reads the exit plan first and refuses with an InputError carrying the plan’s message when no execute call can land now (still in the cooldown, waiting for the claim to open, the window closed, a freeze). The summary says which call it is:

  • the lock: “Lock your withdrawal of … shares (pays nothing; collect it from … later)”;
  • the paying call: “Collect your insurance withdrawal (at most … USDC)”;
  • on a terminated market: “Redeem … insurance shares”.

Both calls on a live market need a fresh price, so the update is attached when a price source is connected (withPrice: false sends none, and the call then reverts on a live market). On a market with trading hours that means they can land only while its venue trades. On a terminated market no price is read, so none is attached unless you pass withPrice: true.

While the vault holds liquidated inventory, a paying call is held to the fund’s headroom: it may pay part of the claim and leave the rest requested, under the same lock scaled to it, for another paying call later in the window. Run the exit plan again after each paying call.

The Exit Plan

const plan = await bz.insurance.insuranceExitPlan(eth.address, user!);
console.log(plan.step, plan.message); // e.g. "cooldown" "Your withdrawal's cooldown ends in 9 d 4 h; ..."
for (const note of plan.notes) console.log(`note: ${note}`);
console.log(`shares ${formatUnits18(plan.shares, 4)}, requested ${formatUnits18(plan.requestedShares, 4)}, locked by votes ${formatUnits18(plan.lockedForVoting, 4)}`);
console.log(`window ${at(plan.windowOpensTs)} to ${at(plan.windowEndTs)}; next call from ${at(plan.nextCallAt)} until ${at(plan.nextCallBy)}`);
if (plan.payoutEstimate !== null) console.log(`the paying call would pay at most ${formatUnits18(plan.payoutEstimate, 2)} USDC now`);

insuranceExitPlan(pair, user, { quote? }) reads everything the exit depends on in one batch (the shares, the request and its share epoch, the lock, the market’s state, the price-gap clocks, the vault’s inventory, the shares locked by votes) and says where the LP stands. step is one of:

StepMeaningnextAction
noneNo shares and no request.null
requestNo request yet, or only a voided one (see Share Epochs). On a terminated market the redemption then pays at once.requestInsuranceWithdrawal
cooldownRequested; the window opens at nextCallAt.executeInsuranceWithdrawal
lockThe window is open; the next execute locks and pays nothing.executeInsuranceWithdrawal
waitClaimLocked; the paying call opens at nextCallAt.executeInsuranceWithdrawal
claimLocked and open: the next execute pays.executeInsuranceWithdrawal
redeemTerminated market: the next execute pays.executeInsuranceWithdrawal
expiredThe window closed, too little of it is left to lock and collect, or a price move pushed the claim past its end. Request again.requestInsuranceWithdrawal
blockedNo exit call can land now: the settlement window, the 6 seconds before a cutoff, the settlement of open positions after finalize, or shares committed to a vote. nextCallAt says when, where it is known.varies

The plan’s fields:

FieldMeaning
messageOne line for the LP, with the waits written out (“9 d 4 h”).
nextActionThe function to call next, or null.
nextCallAtThe earliest chain time the next call can land (now when it can land now), or null.
nextCallByThe latest chain time it can land, or null for no limit. For a lock this is the window’s end less the 5-minute claim delay.
nowThe chain time of this reading.
shares, requestedSharesThe LP’s shares, and those in the live request (0 without one).
lockedForVotingShares committed to insurer votes, which the exit cannot take.
requestTs, windowOpensTs, windowEndTsThe request and its window, or null.
lockTs, lockedAmount, claimOpensTsThe lock, once made. For the lock step claimOpensTs is when a lock made now would open its claim.
payoutEstimateFor lock, waitClaim, claim and redeem, with a price source: the most the paying call would pay now (the lower of the locked amount and the live value).
budgetRemainingThe shared budget left, with a price source; null when unlimited or unknown.
notesWhat can still hold the paying call back: trading hours, an open ADL window, inventory in the vault, shares locked by a vote.

{ quote: false } skips the price reads (payoutEstimate and budgetRemaining stay null), which is what executeInsuranceWithdrawal does internally. Without a price source they are null too. Every time is the chain’s: compare against now, not the browser’s clock.

Taking the Next Step

/** Take the LP's next exit step, if one can land now. `shares` sizes a new request (all free shares by default). */
async function nextExitStep(shares?: bigint): Promise<string> {
  const p = await bz.insurance.insuranceExitPlan(eth.address, user!, { quote: false });
  if (p.nextAction === null || p.nextCallAt === null || p.nextCallAt > p.now) return p.message;
  let build: () => Promise<PreparedCall>;
  if (p.nextAction === "requestInsuranceWithdrawal") {
    const amount = shares ?? p.shares - p.lockedForVoting; // shares backing a vote cannot leave
    build = () => bz.insurance.requestInsuranceWithdrawal(eth.address, user!, amount);
  } else {
    // The lock and the paying call are the same function; both carry a fresh price on a live market.
    build = () => bz.insurance.executeInsuranceWithdrawal(eth.address, user!);
  }
  const { call, hash } = await buildAndSend(window.ethereum, async () => {
    const c = await build();
    const sim = await bz.simulate(c);
    if (!sim.ok) throw new CallError(sim.error!.message, sim.error);
    return c;
  });
  await bz.waitForReceipt(hash);
  return `Done: ${call.summary}`;
}

try {
  console.log(await nextExitStep());
} catch (e) {
  console.error(errorMessage(e));
}

A UI can run the plan on a timer and enable one button: “Request withdrawal” for request and expired, “Lock” for lock, “Collect” for claim and redeem, a countdown to nextCallAt for cooldown and waitClaim. Remind LPs that the window is short (2 or 4 days) and the lock and the paying call must both land inside it.

Budgets and Locks

const [budget, lock, k] = await Promise.all([
  bz.insurance.getInsuranceWithdrawalBudget(eth.address), // at the live price
  bz.insurance.getInsuranceWithdrawalLock(eth.address, user!),
  bz.insurance.getInsuranceWithdrawalConstants(),
]);
if (budget.unlimited) console.log("No withdrawal budget applies (terminated, or no open interest).");
else console.log(`${formatUnits18(budget.remaining, 2)} of ${formatUnits18(budget.cap, 2)} USDC left until ${at(budget.resetsAt)}`);
console.log(`coverage ${budget.coverageRatioBp} bp against a target of ${budget.targetRatioBp} bp`);
if (lock.isLocked) console.log(`locked at ${formatUnits18(lock.lockedAmount, 2)} USDC; collect from ${at(lock.claimOpensTs)} to ${at(lock.windowEndTs)}`);
console.log(`cooldown ${k.cooldown / 86_400} days; windows ${k.continuousWindow / 86_400} or ${k.sessionWindow / 86_400} days`);
// The claim delay, in the SDK's own terms: 5 minutes after the lock and after the last 2% jump,
// 30 minutes after the end of a price gap of an hour or more.
const opens = insurance.claimOpensAt(lock.lockTs, 0, 0);
console.log(`with no jump or gap since, the claim would open at ${at(opens)}`);

getInsuranceWithdrawalBudget(pair, { price? }) is the lens’s reading of the market’s shared 6-hour withdrawal budget at the live price (or price): the number a paying call is checked against, as of now. It is a reading, not a reservation. Every LP on the market draws on the same budget, and the check runs at the paying call, not at the request or the lock.

FieldMeaning
remainingWhat the current period still admits, 18 decimals.
capThe period’s budget.
resetsAtWhen the period ends and the budget refills; null when unlimited.
unlimitedNo limit applies: the market is terminated or has no open interest.
coverageRatioBpFree capital over open-interest notional, in basis points.
targetRatioBpThe live target; below it the stricter budget applies.
priceThe price it was read at.

getInsuranceWithdrawalLock(pair, user) is the lens’s view of the two execute calls: isLocked, lockedAmount (the most the paying call pays), lockTs, claimOpensTs (a later price jump or reopen pushes it back) and windowEndTs (0 without a request).

getInsuranceWithdrawalConstants() reads the protocol’s terms from the lens: cooldown, continuousWindow, sessionWindow and rateLimitPeriod in seconds, and rateLimitBp, aboveTargetRateLimitBp, aboveTargetFundCapBp and belowTargetFundCapBp in basis points.

Share Epochs and Voided Requests

When bad debt drains a fund below $1, the next deposit starts a new share epoch: balances from before the drain read as zero, and the rescuer is minted shares 1:1 against their own deposit. A withdrawal request is stamped with the epoch it was filed in, and the pair refuses to execute one from an earlier epoch (InsuranceVaultLib__WithdrawalRequestStaleEpoch).

The SDK reads the epoch with the request, so a voided request is no request at all: the plan reports request (or none when the LP holds no shares in the new epoch) and its message starts with “Your earlier withdrawal request was voided when the insurance fund was recapitalized.”, and requestInsuranceWithdrawal describes a new request rather than a replacement.

On a Terminated Market

After finalizeTermination, the fund is whatever remains once every trader is paid, and its shares redeem against that residual. The plan reports request and then redeem, and one execute call pays with no price update.

If finalize ran through the 14-day escape hatch with positions still open, redemptions wait until those positions are settled (anyone can settle them; see Termination & Settlement), or 28 days after the settlement price was fixed at the latest. The plan reports blocked with that time as nextCallAt.

Insurers also take part in ending a market: voting shares for an insurer termination locks them, and some outcomes levy part of them. See Insurer Votes.

Gasless

All three LP calls have relayed forms (depositToInsurance, requestInsuranceWithdrawal, executeInsuranceWithdrawal), signed with bz.relay.signRelayed like any other action; see Gasless Trading & Relayers. A relayed deposit pulls the amount plus the relayer fee from the wallet. A relayed request and a relayed lock pay their fee from the wallet too (the lock pays nothing out), so they need an allowance or a permit; a relayed paying call takes its fee out of the payout.

What Can Go Wrong

Builder refusals are InputErrors whose field names the argument (amount, shareAmount, pair); errorMessage(e) turns them and every decoded revert into one sentence. The contract errors an LP is most likely to see in a simulation:

ErrorMeaning
InsuranceVaultLib__CooldownNotElapsedThe 13-day cooldown has not ended.
InsuranceVaultLib__ClaimNotOpenLocked; the paying call opens at claimOpensTs.
InsuranceVaultLib__LockTooLateThe claim a lock would open could only open after the window closes. Request again.
InsuranceVaultLib__WithdrawalWindowExpiredThe window closed. Request again.
InsuranceVaultLib__NoWithdrawalRequestNo request to execute.
InsuranceVaultLib__WithdrawalRequestStaleEpochThe request predates a fund recapitalization.
InsuranceVaultLib__RateLimitExceededThe shared 6-hour budget is used up. Withdraw less or wait for resetsAt.
InsuranceVaultLib__AdlBlockingThe fund’s headroom cannot pay any of the claim (ADL due or running).
InsuranceVaultLib__VaultUnhealthyExits are paused while the market is unhealthy.
InsuranceVaultLib__SharesLockedForVotingShares in the request back an open termination vote.
InsuranceVaultLib__SweepIncompleteAfter an escape-hatch finalize, positions are still being settled.
InsuranceVaultLib__PairScheduledForTerminationWithin 6 seconds of a scheduled cutoff, or settling.
InsuranceVaultLib__TooManyRecentInsuranceDepositsMore than 100 deposits in four days.
InsuranceVaultLib__DepositTooSmallThe deposit buys no shares.
BazaarPair__InsufficientPythFeeThe price update’s fee changed; rebuild the call.

A call that carries a price update and lands more than 3 seconds after its priceTime is refused: rebuild it and send again. See Errors & Messages.

History

Bazaar’s subgraph records every insurance event: bz.history.insuranceEvents (deposits, requests, locks and payouts), insuranceProviders (each LP’s position per market) and insurerLevies. See History.