QaxeBurnVault reference
QaxeBurnVault (0x000e8FA7cFA09827f0A0786CE3E96aC5e4E9f46C)
holds every QuaiAxe deposit's accounting, splits gross deposits into fee / burn / payout, and buys
and burns QAXE from its bonded curve. It is deployed by, and permanently bound to, one
QaxeDepositFactory — only that factory may call accountDeposit.
There is no owner, no pause, and no upgrade path. The only privileged role is the treasury, which can pull its own accrued fee and hand its role to a successor — nothing else. See security for the full threat model.
Constants
| Constant | Value | Why |
|---|---|---|
FEE_BPS | 50 (0.5%) | Owner decision: a flat 0.5% "maintenance & treasury" fee on every mined QUAI that passes through the vault, credited or anonymous. |
REFUND_CAP_BPS | 300 (3%) | Gas refunds — for a collection or a sweep — can never exceed 3% of the gross behind them. Bounds the worst case for a hostile or wasteful caller. |
SLICE_BPS | 50 (0.5%) | Each sweep buys at most 0.5% of the market's reserve in one call. |
ABS_MAX_SLICE | 1000 ether | Absolute ceiling on a slice regardless of reserve size. |
MIN_SWEEP | 100 ether | A slice below 100 QUAI is refused unless it also qualifies as stale dust (below). |
DUST_SWEEP | 1 ether | The absolute floor — even a stale balance below 1 QUAI is never swept. |
COOLDOWN_BLOCKS | 120 | Minimum blocks between sweeps (after the first). A slice cap means nothing if ten slices fit in one block. |
STALE_BLOCKS | 120960 (~7 days at 5s blocks) | A small (1–100 QUAI) balance may sweep as "stale dust" only after this many blocks have passed since deployment (first sweep) or the last sweep. |
MIN_BURN_BPS | 1000 (10%) | The floor a miner can choose for their burn share — at least 10% always burns. |
PAYOUT_GAS | 30000 | Gas forwarded on a push payment to a payout or refund address. Caps what a hostile recipient contract can spend of the vault's gas. |
SWEEP_TAIL_GAS | 10500 | Calibrated constant added to sweep's own measured gas, covering the work that happens after the measurement point (event, payment, return). |
CREDIT_TAIL_GAS | 14000 | Same idea for accountDeposit's own bookkeeping. |
refundPrice() | min(tx.gasprice, 2 * block.basefee) | Reads live from the chain — there is no owner-set price ceiling. Bounds what a miner-inflated priority fee can claim; see security. |
SLICE_BPS/ABS_MAX_SLICE together put the sandwich break-even for a slice at roughly 1.02% of
reserve — see security for the reasoning.
Immutables and state
| Name | Type | Meaning |
|---|---|---|
curve | address (immutable) | The IQaxeBurnCurve the vault buys from — the QAXE BondingCurve. |
token | address (immutable) | The token the vault buys and burns — read from curve.token() at construction. |
factory | address (immutable) | The only address allowed to call accountDeposit — the constructing QaxeDepositFactory. |
treasury / pendingTreasury | address | Receives feesOwed on withdrawFees; two-step rotation via proposeTreasury/acceptTreasury. |
feesOwed, totalFeesAccrued, totalFeesWithdrawn | uint256 | Fee ledger: currently owed, lifetime accrued, lifetime withdrawn. |
prepaid | uint256 | Principal reserved for burning — already had its fee taken, not yet spent by a sweep. |
refundAllowance | uint256 | The part of prepaid still available to fund future gas refunds (≤ 3% of the gross behind it). |
totalPending / pending[address] | uint256 / mapping | Miner-leg pushes that failed and fell back to pull. |
received[address], contributed[address], kept[address] | mapping(address => uint256) | Per-source lifetime gross received, principal contributed to burning, and miner-leg kept, keyed by the depositor (the deposit address itself for a collected deposit, or address(0) for anonymous inflow). |
totalQuaiSpent, totalTokensBurned, totalRefunded, sweeps, lastSweepBlock | uint256 | Lifetime sweep totals and cadence. |
Accounting model
Every deposit's gross g is split once, whether it arrives via credit, accountDeposit
(the factory), or anonymously (a plain transfer the vault later notices in _syncAnonymous):
fee = g * FEE_BPS / 10000 // 0.5%, to feesOwed
rest = g - fee
burnLeg = rest * burnBps / 10000 // added to prepaid
minerLeg = rest - burnLeg // pushed to payout, or added to pending
allowance = min(burnLeg, g * REFUND_CAP_BPS / 10000) // added to refundAllowanceAnonymous inflow (_syncAnonymous) always uses burnBps = 10000 implicitly — there is no payout
wallet for QUAI nobody attributed to a source, so all of rest becomes burnLeg.
Core invariant, asserted after every state-changing call in the test suite:
address(this).balance == feesOwed + totalPending + prepaid
refundAllowance <= prepaidFees and pending balances are never sweepable — sweep's slice comes only out of prepaid, net of
refundAllowance. Credited QUAI pays its fee exactly once (at credit/accountDeposit time);
anonymous QUAI pays its fee exactly once, at the point _syncAnonymous notices it (which can be a
standalone syncAnonymous() call, or implicitly inside credit, accountDeposit, or sweep,
each of which syncs before doing its own accounting).
Returned payments never mint new allowance
_pay treats anything a recipient calls back into the vault's balance, above what was sent minus
what stuck, as a return of principal, not a fresh deposit — so it goes back into prepaid and,
crucially, does not get a new refundAllowance computed against it. refundAllowance is
carved out of principal that already paid its fee; a hostile or misbehaving recipient cannot mint
extra refund budget by bouncing money back at the vault.
Functions
credit(address payout, uint256 burnBps) external payable
Anyone can call this directly (no factory needed) to deposit QUAI with an explicit burn share.
msg.value is the gross. Syncs any anonymous balance first (excluding this call's own value), then
runs the split above. If minerLeg != 0, pushes it to payout (30,000 gas); on failure, credits
pending[payout]. No refund path — this entry point pays no gas refund (that only applies to
accountDeposit, the factory's collection path, and sweep).
Reverts:
"Zero gross"—msg.value == 0."Invalid burn bps"—burnBps < 1000or> 10000."Invalid payout"—burnBps < 10000andpayoutis the zero address or the vault itself.
accountDeposit(address source, address payout, uint256 burnBps, uint256 gross, address refundTo, uint256 collectionGas) external onlyFactory
The factory's attested deposit path — only factory can call it. source is the deposit address
being collected (used as the accounting key in received/contributed/kept, not msg.sender,
since the caller here is always the factory). collectionGas is the gas the factory itself already
spent proving and forwarding this deposit; the vault adds its own measured gas
(gasStart - gasleft() + CREDIT_TAIL_GAS) before computing a refund, because the vault's own
first-time storage writes vary too much for the factory to predict.
Emits Credited, then (if a nonzero refundTo was given) computes and pays a refund out of this
deposit's own allowance, and emits CollectionRefund(source, refundTo, netRefund, refundPaid).
syncAnonymous() external
Notices any QUAI sitting in the vault's balance that isn't already booked as feesOwed + totalPending + prepaid, and runs it through the fee/burn split as if it were a 100%-burn anonymous
deposit (keyed under received[address(0)]). Anyone can call it; every other state-changing
function calls it internally first, so it rarely needs to be called directly — it exists mainly so
previewSweep/available can be trusted as up to date without waiting for the next sweep.
sweep(uint256 minTokensOut, address refundTo) external returns (uint256 quaiSpent, uint256 burned)
Buys QAXE with one bounded slice of prepaid and burns every QAXE the vault holds (its purchase
plus anything donated directly). Permissionless.
- Requires
_cooldownReady()(first sweep ever is exempt; otherwise ≥120 blocks since the last). - Syncs anonymous inflow, capturing the fee this sync just accrued (
newFee, reported in the event — informational, already excluded from the slice by definition ofprepaid). slice = min(prepaid, sliceCap()); requires_sizeReady(slice)(below).reserve = refundAllowance * slice / prepaid— this slice's proportional share of the remaining refund budget;buyValue = slice - reserveis what actually goes to the curve.- Deducts
slicefromprepaidandreservefromrefundAllowancebefore calling the curve (checks-effects-interactions). - Calls
curve.buy{value: buyValue}(minTokensOut).quaiSpentis measured as the vault's own balance delta (never trusts the curve's return value); any unspentbuyValue(e.g. a sellout-clamped buy near graduation) goes back intoprepaidwith no new allowance. - Burns the vault's entire QAXE balance — including anything sent to the vault directly, not
just what this buy purchased. Reverts
"Insufficient burn"if that's belowminTokensOut. - Computes a gas refund out of
reserve(see Calibration, below) and paysrefundTo, if given, up toreserve. Whatever ofreserveis not paid out goes back intoprepaid. - Emits
Swept(caller, slice, newFee, netRefund, quaiSpent, burned, available()).
Reverts:
"Zero minimum"—minTokensOut == 0."Cooldown"— not_cooldownReady()."Below minimum"— slice doesn't satisfy_sizeReady."Insufficient burn"— actual QAXE balance after the buy is belowminTokensOut(this is the slippage guard: the caller's floor should come from a freshcurve.quoteBuyon the net buy value — see integrate).
sliceCap() public view returns (uint256)
reserve = curve.graduated() ? curve.poolQuaiReserve() : curve.virtualQuaiReserve() + curve.realQuaiReserve()
return min(ABS_MAX_SLICE, reserve * SLICE_BPS / 10000)canSweep() external view returns (bool)
_cooldownReady() && _sizeReady(min(available(), sliceCap())). A true result is the only
sanctioned way to know a sweep will not immediately revert on readiness grounds — a nonzero
previewSweep() quote is not permission to execute (it is a live accounting quote, computed
even during cooldown or below the minimum).
previewSweep() external view returns (uint256 slice, uint256 fee, uint256 refundReserve, uint256 buyValue)
Read-only, no state change. fee is all newly-synchronized anonymous fees (not scaled to the
slice) — already excluded from the quoted slice/principal by definition. Use this to build a
fresh minTokensOut floor from curve.quoteBuy(buyValue) before calling sweep. See
integrate for a worked example.
refundPrice() public view returns (uint256)
min(tx.gasprice, 2 * block.basefee) (with an overflow guard: if doubling basefee would
overflow uint256, every possible tx.gasprice is already below the ceiling, so it returns
tx.gasprice directly). This is the only price used anywhere refunds are computed — there is no
owner-settable ceiling in this contract.
available() public view returns (uint256)
The vault's net spendable principal if everything unbooked were synced right now:
gross = balance - feesOwed - totalPending - prepaid; return prepaid + gross - gross*FEE_BPS/10000.
withdrawFees() external
Pays feesOwed to treasury and zeroes it. No caller restriction beyond needing something to
withdraw ("Nothing to withdraw" if feesOwed == 0) — but the destination is always treasury,
never msg.sender, so calling it on someone else's behalf gains the caller nothing. Uses
gasleft() as the payment's gas limit (not the 30,000 cap) because a standalone pull needs to
support treasuries whose receive/fallback costs more than the push-payment cap.
withdrawPending() external
Pays pending[msg.sender] to msg.sender and zeroes it — the pull-fallback for a miner whose
push payout previously failed (hostile or gas-hungry payout contract, or simply out of gas at
30,000). Reverts "Nothing to withdraw" if there's nothing pending.
proposeTreasury(address successor) / acceptTreasury()
Two-step handover, gated msg.sender == treasury / msg.sender == pendingTreasury respectively.
This is the only privileged action in the whole system, and it can only ever redirect where the
0.5% fee lands — it cannot touch prepaid, pending, or any miner's or the burn's principal.
What the treasury can and cannot do
The treasury can: receive its own accrued 0.5% fee via withdrawFees, and hand the treasury
role to a successor address (two-step).
The treasury cannot: pause anything, upgrade anything, change any constant, redirect a burn or
a miner's payout, touch prepaid/pending/refundAllowance, or withdraw anything beyond
feesOwed. There is no function on this contract that lets the treasury move burn principal or a
miner's kept share.
Events
| Event | Fields |
|---|---|
Credited | from (indexed), payout (indexed), burnBps, gross, fee, burnLeg, minerLeg, minerPaid |
Swept | caller (indexed), slice, fee, refund, quaiSpent, burned, remainingAvailable |
AnonymousCredited | gross, fee, burnLeg |
CollectionRefund | source (indexed), to (indexed), amount (net successful refund, 0 on failure), paid |
FeesWithdrawn | treasury (indexed), amount |
PendingWithdrawn | payout (indexed), amount |
TreasuryProposed | current (indexed), successor (indexed) |
TreasuryAccepted | previous (indexed), current (indexed) |
Revert strings
"Reentrant call" · "Not factory" · "Zero curve" · "Invalid treasury" · "Curve has no token" · "Zero gross" · "Invalid burn bps" · "Invalid payout" · "Insufficient deposit"
(anonymous-sync bookkeeping check — should never trigger in normal operation) · "Nothing to withdraw" · "Withdraw failed" · "Not treasury" · "Not pending treasury" · "Zero minimum" ·
"Cooldown" · "Below minimum" · "Insufficient burn".
Calibration — measured, not guessed
Refunds are computed from gasleft() deltas the vault measures on itself, plus a small calibrated
constant covering only the work that happens after the measurement point
(SWEEP_TAIL_GAS = 10500, CREDIT_TAIL_GAS = 14000). Nothing here is a flat padding guess.
From hartii-labs/contracts/docs/QaxeBurnVault.md (measured against real receipts,
test/QaxeBurnVault.*.test.cjs and test/QaxeDeposit.test.cjs) and the Orchard rehearsal
(scripts/orchard-rehearse-qaxe-v2.cjs):
| Operation | Hardhat refund/cost | Orchard (real Quai chain) refund/cost |
|---|---|---|
| sweep, first | 0.954–0.962 | 0.797 |
| sweep, steady state | 0.998 | not yet sampled |
| collect, fresh address | 0.936–0.942 | 0.794 (batch of 2) |
| collect, repeat | 0.997 | 0.851 |
Re-running the Hardhat suite locally (npx hardhat test test/QaxeDeposit.test.cjs) reproduces
comparable numbers, e.g. a fresh-address collect at refund/cost = 0.9359 and a repeat collect at
0.9966.
Quai's real gas pricing runs 15–20% above what the EVM-internal gasleft() delta sees — a plain
transfer to a fresh account costs 39,358 gas on Quai versus 21,000 + 25,000 on an Ethereum-style
chain, and Quai appears to price storage growth outside the interpreter's own gas accounting. The
tail constants were left conservative on purpose: no refund is ever computed to exceed the
caller's real cost in the test suite, and even where the on-chain calibration under-refunds
(≈0.79–0.85x on Quai), the protocol still nets positive when it runs its own keeper — the 0.5%
treasury fee on the gross a keeper moves (e.g. ≈4.3 QUAI of fee on an ~855-QUAI slice) outweighs the
≈2 QUAI of unrefunded gas.