Hartii developer docs

QuaiAxe

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

ConstantValueWhy
FEE_BPS50 (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_BPS300 (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_BPS50 (0.5%)Each sweep buys at most 0.5% of the market's reserve in one call.
ABS_MAX_SLICE1000 etherAbsolute ceiling on a slice regardless of reserve size.
MIN_SWEEP100 etherA slice below 100 QUAI is refused unless it also qualifies as stale dust (below).
DUST_SWEEP1 etherThe absolute floor — even a stale balance below 1 QUAI is never swept.
COOLDOWN_BLOCKS120Minimum blocks between sweeps (after the first). A slice cap means nothing if ten slices fit in one block.
STALE_BLOCKS120960 (~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_BPS1000 (10%)The floor a miner can choose for their burn share — at least 10% always burns.
PAYOUT_GAS30000Gas 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_GAS10500Calibrated constant added to sweep's own measured gas, covering the work that happens after the measurement point (event, payment, return).
CREDIT_TAIL_GAS14000Same 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

NameTypeMeaning
curveaddress (immutable)The IQaxeBurnCurve the vault buys from — the QAXE BondingCurve.
tokenaddress (immutable)The token the vault buys and burns — read from curve.token() at construction.
factoryaddress (immutable)The only address allowed to call accountDeposit — the constructing QaxeDepositFactory.
treasury / pendingTreasuryaddressReceives feesOwed on withdrawFees; two-step rotation via proposeTreasury/acceptTreasury.
feesOwed, totalFeesAccrued, totalFeesWithdrawnuint256Fee ledger: currently owed, lifetime accrued, lifetime withdrawn.
prepaiduint256Principal reserved for burning — already had its fee taken, not yet spent by a sweep.
refundAllowanceuint256The part of prepaid still available to fund future gas refunds (≤ 3% of the gross behind it).
totalPending / pending[address]uint256 / mappingMiner-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, lastSweepBlockuint256Lifetime 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):

text
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 refundAllowance

Anonymous 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:

text
address(this).balance == feesOwed + totalPending + prepaid
refundAllowance <= prepaid

Fees 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 < 1000 or > 10000.
  • "Invalid payout"burnBps < 10000 and payout is 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.

  1. Requires _cooldownReady() (first sweep ever is exempt; otherwise ≥120 blocks since the last).
  2. Syncs anonymous inflow, capturing the fee this sync just accrued (newFee, reported in the event — informational, already excluded from the slice by definition of prepaid).
  3. slice = min(prepaid, sliceCap()); requires _sizeReady(slice) (below).
  4. reserve = refundAllowance * slice / prepaid — this slice's proportional share of the remaining refund budget; buyValue = slice - reserve is what actually goes to the curve.
  5. Deducts slice from prepaid and reserve from refundAllowance before calling the curve (checks-effects-interactions).
  6. Calls curve.buy{value: buyValue}(minTokensOut). quaiSpent is measured as the vault's own balance delta (never trusts the curve's return value); any unspent buyValue (e.g. a sellout-clamped buy near graduation) goes back into prepaid with no new allowance.
  7. 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 below minTokensOut.
  8. Computes a gas refund out of reserve (see Calibration, below) and pays refundTo, if given, up to reserve. Whatever of reserve is not paid out goes back into prepaid.
  9. 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 below minTokensOut (this is the slippage guard: the caller's floor should come from a fresh curve.quoteBuy on the net buy value — see integrate).

sliceCap() public view returns (uint256)

text
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

EventFields
Creditedfrom (indexed), payout (indexed), burnBps, gross, fee, burnLeg, minerLeg, minerPaid
Sweptcaller (indexed), slice, fee, refund, quaiSpent, burned, remainingAvailable
AnonymousCreditedgross, fee, burnLeg
CollectionRefundsource (indexed), to (indexed), amount (net successful refund, 0 on failure), paid
FeesWithdrawntreasury (indexed), amount
PendingWithdrawnpayout (indexed), amount
TreasuryProposedcurrent (indexed), successor (indexed)
TreasuryAcceptedprevious (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):

OperationHardhat refund/costOrchard (real Quai chain) refund/cost
sweep, first0.954–0.9620.797
sweep, steady state0.998not yet sampled
collect, fresh address0.936–0.9420.794 (batch of 2)
collect, repeat0.9970.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.

Quai Network mainnet · chain 9 · Cyprus-1. Figures marked "read on" a date were read from the chain that day; re-read before relying on them.