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

Collateral, Approvals & Permits

Each Bazaar market (pair) keeps its own collateral for each account: USDC deposited into the account’s position bucket on that pair, which backs its position and its resting orders. This page covers moving USDC in and out, the allowance a deposit or a keeper bounty needs, USDC permits as a signed alternative to an approve transaction, and what stops a withdrawal.

The samples assume bz connected as in Connecting, user the wallet’s address, eth a market from Markets, and a run(call) helper that simulates, sends and waits as on Sending Transactions. For the margin rules that decide how much collateral a position needs, see Margin.

Units

Two precisions meet here:

  • BAZAAR precision (18 decimals, 1e18 = $1): every amount the pair takes or reports, including PreparedCall.pullsUsdc. The pair floors what it moves to whole micro-USDC (multiples of 1e12).
  • USDC units (6 decimals): what USDC’s own functions count in: balanceOf, allowance, approve, permit. Every function in the allowance and permits areas takes and returns USDC units.
const amount = parseUnits18("100"); // 100 USDC, BAZAAR precision: 100n * 10n ** 18n
toUsdcUnits(amount); // 100_000_000n: what USDC's own functions count in (6 decimals)
fromUsdcUnits(1_500_000n); // 1.5 USDC back in BAZAAR precision
toUsdcUnitsCeil(parseUnits18("0.0000005")); // 1n: rounded up, for an allowance that must cover it
formatUnits18(amount); // "100"
allowance.formatUsdcUnits(2_500_000n); // "2.5"
HelperConverts
parseUnits18("12.5")A decimal string to BAZAAR precision. Throws UnitError on more than 18 decimals.
formatUnits18(x, maxDecimals?)BAZAAR precision to a decimal string, trailing zeros trimmed.
toUsdcUnits(x)BAZAAR precision to USDC units, floored as the pair floors it.
toUsdcUnitsCeil(x)BAZAAR precision to USDC units, rounded up: for an allowance or permit that must cover a pull.
fromUsdcUnits(x)USDC units to BAZAAR precision.
floorToUsdc(x)BAZAAR precision floored to a multiple of 1e12: what the pair actually moves.
allowance.formatUsdcUnits(x)USDC units to a decimal string.

Units & Constants lists the rest.

Depositing

bz.trade.depositCollateral(pair, from, amount, opts?) builds a deposit of amount (BAZAAR precision) into from’s bucket on pair. The pair pulls the USDC from the wallet, so the wallet needs that much approved to the pair, or a permit carried in the call.

ArgumentMeaning
pairThe market’s pair address (eth.address). It must be one of the deployment’s pairs: any other address is refused with InputError on pair before anything is built.
fromThe depositing account. The collateral is credited to it.
amountBAZAAR precision. Floored to whole micro-USDC first; at least 5 USDC after flooring, or InputError on amount (“The minimum deposit is 5 USDC.”).
opts.permitDataA USDC permit (SignedPermit.permitData) the pair executes before it pulls. Default "0x", none.

The call’s pullsUsdc is the floored amount; its summary reads Deposit 100 USDC as collateral. It carries no price update and sends no ETH.

The pair refuses a deposit once the market is terminated (CollateralLib__DepositsAreBlocked), in the seconds before a scheduled termination cutoff, and while its settlement window is open. The builder does not check these; the simulation reports them.

Checking Balance and Allowance

const deposit = await bz.trade.depositCollateral(eth.address, user, amount);
const funding = await bz.allowance.usdcFunding(user, eth.address, deposit.pullsUsdc ?? 0n);

if (!funding.enoughBalance) {
  show(`You have ${allowance.formatUsdcUnits(funding.balance)} USDC; this deposit needs ${allowance.formatUsdcUnits(funding.needUsdc)}.`);
} else {
  // An approval replaces the previous one, so approve the whole amount the call pulls, not the difference.
  const approved = funding.enoughAllowance || (await run(await bz.allowance.approveUsdc(user, eth.address, funding.approveUsdc)));
  if (approved) await run(deposit); // "Deposit 100 USDC as collateral"
}
FunctionReturns
bz.allowance.usdcBalance(owner)The owner’s USDC balance, in USDC units.
bz.allowance.usdcAllowance(owner, spender)What spender (a pair) may pull from owner, in USDC units.
bz.allowance.usdcFunding(owner, spender, pullBazaar)Balance and allowance against a pull, in one batched read (below).

usdcFunding takes the pull in BAZAAR precision (pass call.pullsUsdc) and returns:

FieldMeaning
needUsdcThe pull in USDC units, rounded up.
balanceThe wallet’s USDC balance.
allowanceWhat the pair may pull now.
enoughBalancebalance >= needUsdc.
enoughAllowanceallowance >= needUsdc.
approveUsdcWhat to approve: needUsdc when the allowance falls short, else 0n. It is the whole amount, not the difference, because an approval replaces the previous one.

Without enough allowance the pull fails inside USDC’s transferFrom with the token’s own revert (on native USDC, ERC20: transfer amount exceeds allowance), so check before simulating, and wait for the approval’s receipt before simulating the deposit.

Approving USDC

bz.allowance.approveUsdc(owner, spender, amountUsdc, opts?) builds USDC’s approve(spender, value) from owner, as a PreparedCall to the USDC contract (action: "approve").

ArgumentMeaning
ownerThe wallet giving the allowance (the call’s from).
spenderA pair of this deployment. Anything else is refused with InputError on pair, revocations included, so a link cannot pass another contract off as a market.
amountUsdcUSDC units. 0n revokes. Ignored with unlimited.
opts.unlimitedApprove 2^256 - 1. Default false.
// Only on the user's explicit choice, with a way to undo it.
const unlimited = await bz.allowance.approveUsdc(user, eth.address, 0n, { unlimited: true });
unlimited.summary; // "Allow 0x... to take an unlimited amount of USDC from your wallet"

const revoke = await bz.allowance.approveUsdc(user, eth.address, 0n);
revoke.summary; // "Revoke 0x...'s USDC allowance"

Exact approvals are the default on purpose. An unlimited approval saves one approve transaction per deposit, but it leaves the whole wallet balance pullable by that contract for as long as the approval stands. Offer it only as the user’s explicit choice, say what it means, and give them a way to revoke it (approveUsdc(owner, pair, 0n)). Each pair pulls for itself, so every market needs its own allowance.

Approvals and deposits are two transactions. A permit makes them one.

USDC Permits

A permit (ERC-2612) is an approval the user signs instead of sending. The pair call carries it in permitData and executes it just before it pulls, so the user signs once and sends once, and on the gasless path needs no ETH at all.

// One signature instead of an approve transaction. A permit SETS the allowance, so skip it when the
// allowance already covers the deposit: signing one anyway would replace a larger approval.
const needUsdc = permits.permitValueFor(amount); // USDC units, rounded up
const current = await bz.allowance.usdcAllowance(user, eth.address);

let permitData: `0x${string}` = "0x";
if (current < needUsdc) {
  const permit = await bz.permits.signPermit(window.ethereum, {
    owner: user,
    spender: eth.address, // must be one of the deployment's pairs
    value: needUsdc,
    // deadline: unix seconds; default 30 minutes from now
  });
  permitData = permit.permitData;
}
await run(await bz.trade.depositCollateral(eth.address, user, amount, { permitData }));

bz.permits.signPermit(wallet, req) asks the wallet to sign a USDC permit with eth_signTypedData_v4, so the wallet shows every field.

req fieldDefaultMeaning
ownerrequiredThe signing wallet’s address.
spenderrequiredThe pair that will pull. Must be one of the deployment’s pairs, or InputError on pair before the wallet is asked.
valuerequiredUSDC units, more than zero. permits.permitValueFor(pullBazaar) converts a BAZAAR-precision pull, rounded up.
deadline30 minutesUnix seconds. The default is DEFAULT_PERMIT_WINDOW (1800 s) past the lower of the chain’s time and the device’s clock.
noncereadThe owner’s permit nonce on USDC; read from the token unless given.

It returns a SignedPermit: the message (owner, spender, value, nonce, deadline), the signature and its v, r, s, and permitData, the abi.encode(value, deadline, v, r, s) the pair call takes.

Rules a permit follows:

  • It sets the allowance; it does not add to it. value must cover everything the call pulls, and a permit signed when a larger allowance is in place replaces that allowance with the smaller one. Check usdcAllowance first, as the sample does. (signRelayed makes that check for you on the gasless path.)

  • It is best effort. A permit that fails (expired, already used, front-run) does not fail the call: the pair emits PermitExecutionFailed and falls back to the allowance already in place, so the call then succeeds or fails on that allowance.

  • One at a time. Each permit uses the owner’s next USDC nonce. Sign one, submit the call that carries it, and only then sign the next; two permits signed in a row share a nonce and the second fails.

  • What it must cover, by call:

    CallThe permit covers
    depositCollateral, depositToInsuranceThe deposit (plus the relayer fee when relayed).
    createOrderThe trigger order’s keeper bounty (plus the relayer fee when relayed).
    cancelOrders, amendOrders, requestInsuranceWithdrawal, the locking executeInsuranceWithdrawalThe relayer fee alone. A direct call of these pulls nothing and ignores a permit.
    withdrawCollateral, the paying executeInsuranceWithdrawalTakes no permit: the fee comes out of the payout.
  • Only an account’s own signature works. The SDK recovers the signature locally and refuses one that does not recover to owner (SigningError), which is what a smart-contract wallet signing through ERC-1271 produces. Smart accounts approve with an approve call instead, usually in the same batch (Smart Accounts & Batching).

  • The wallet must be on the deployment’s chain: the permit’s domain names the chain id, and wallets refuse to sign for another one.

  • The deadline is counted from a checked clock. The SDK takes the lower of the latest block’s timestamp and the device’s clock, and refuses with ClockSkewError when they disagree by more than 2 minutes (MAX_CLOCK_SKEW), so an RPC reporting a far-future block cannot stretch a permit’s life. Local development chains (chain ids 31337 and 1337) skip the check.

The lower-level pieces are in the permits namespace: usdcPermitDomain(ctx) (USDC’s EIP-712 domain, checked against the token’s own DOMAIN_SEPARATOR()), permitNonce(ctx, owner), permitTypedData(domain, msg), encodePermitData(value, deadline, v, r, s), decodePermitData(data), and checkPermit(ctx, permitData, { owner, spender, needUsdc }), which a relayer uses to check a permit it received (see Gasless Trading & Relayers).

Withdrawing

bz.trade.withdrawCollateral(pair, from, amount, opts?) withdraws amount (BAZAAR precision) from from’s bucket to the wallet, which receives it floored to micro-USDC.

const want = parseUnits18("40");
const preview = await bz.previews.previewWithdrawal(eth.address, user, want);

if (preview.freezes.length > 0) {
  show(preview.freezes[0]!.message); // why nothing can be withdrawn right now
} else if (!preview.ok) {
  show(`${preview.blocker!.message} (at most ${formatUnits18(preview.maxWithdrawable, 2)} USDC now)`);
} else {
  await run(await bz.trade.withdrawCollateral(eth.address, user, want));
}
ArgumentMeaning
amountMore than zero, or InputError on amount.
opts.withPriceWhether to attach a Pyth update. Default: attached whenever the connection has a price source with an update the oracle would accept (the pair refunds the fee when it does not read the price); without one, a flat account’s withdrawal goes without and one with exposure is refused on a continuously traded market (NoFreshPriceError). false attaches none. true requires a price source and a fresh price.

An account with a position or resting orders is priced fresh: the pair keeps the position’s margin, its resting orders’ reservation and a minimum behind, and refuses its stored fallback price for a position holder. A flat account with no orders reads no price. Without a price source, the builder refuses (ConnectError) for an account that needs a price.

The most an account can withdraw right now is previewWithdrawal(...).maxWithdrawable, also on the account snapshot as maxWithdrawable (Accounts & Portfolios). Use previewWithdrawal to tell a user why a larger amount is refused.

Withdrawal Freezes

Some states stop withdrawals whatever the account holds. previewWithdrawal lists the ones that apply in freezes, each with its error name, a user message and, when known, until (when it lifts at the latest, unix seconds). The account snapshot’s withdrawalBlocked and the market’s flags (positionWithdrawalsFrozen, allWithdrawalsPaused, with reasons) report the same states for a UI to grey out its button (Markets).

FreezeWhoLifts
An auto-deleveraging window is open (BazaarPair__WithdrawalsFrozenAdlPending)Accounts with a positionWhen ADL resolves, or the market terminates (at most 24 hours plus a 30-minute grace).
The settlement price is fixed and the termination is not finalized (BazaarPair__SweepWindowActive)EveryoneAt finalization, at most 14 days after the price was fixed.
Limbo, the seconds before a scheduled termination cutoff (CollateralLib__PairScheduledForTermination)EveryoneWhen the settlement price is fixed, the settlement window (the row above) takes over.
The vault has realized bad debt (CollateralLib__WithdrawalBlockedVaultUnhealthy)Accounts that are priced (a position or orders)When the market settles.
A market with trading hours whose venue is closed (CollateralLib__WithdrawalRequiresFreshPrice)Accounts with a positionWith the first fresh price at the reopen.

The ledger can also be short: no withdrawal may exceed the pair’s deposits ledger (CollateralLib__LedgerShortfall), which previewWithdrawal reports in ledgerAvailable. For the mechanics behind each freeze see Margin, Termination and Oracle.

Terminated-Pair Payouts

Once a market is terminated and finalized, a withdrawal pays amount × principalRatio, the share of collateral the market’s estate can pay (1e18 = 100%). withdrawCollateral reads the ratio and puts the payout in the call’s summary:

// On a terminated market a withdrawal pays amount x principalRatio; null on a live market.
const terminal = await trade.terminalWithdrawal(bz.ctx, eth.address, user, want);
if (terminal) {
  show(trade.terminalPayoutNote(terminal.payout, terminal.principalRatio));
  // "the market is terminated: it pays 38.5 USDC, 96.25% of the amount"
}

trade.terminalWithdrawal(ctx, pair, user, amount) returns { payout, principalRatio } on a terminated pair and null on a live one; it is not bound on bz, so call it with bz.ctx. A withdrawal that would pay nothing is refused (InputError on amount). Registered profit claims are paid at the separate profitRatio; previewWithdrawal(...).terminal and the account snapshot’s terminal give the whole entitlement. Termination & Settlement covers the rest.

What Can Go Wrong

You seeIt means
InputError on amount: “The minimum deposit is 5 USDC.”The deposit, floored to micro-USDC, is under 5 USDC.
InputError on pair: “… is not a market of this Bazaar deployment …”The address is not one of the factory’s pairs. Nothing was built or signed.
Error: “ERC20: transfer amount exceeds allowance” (or “… balance”)USDC refused the pull: approve or permit first, or the wallet holds too little.
CollateralLib__InvalidDepositAmountThe pair’s own minimum check.
CollateralLib__InsufficientMarginAfterWithdrawal, CollateralLib__MustRetainMinimumCollateralThe withdrawal would leave the position or orders under-margined. Withdraw less (maxWithdrawable).
ConnectError: “this call needs a fresh Pyth price”The account needs a priced withdrawal and the connection has no price source.
ClockSkewErrorThe device’s clock and the chain’s time disagree by more than 2 minutes; nothing was signed.
SigningError: “You declined the signature.”The user rejected the permit prompt.

See Errors & Messages for how to show these.