Hartii developer docs

QuaiAxe

Security and threat model

No owner, no pause, no upgrade

Neither QaxeBurnVault nor QaxeDepositFactory has an owner, a pause switch, or any upgrade path. There is no proxy, no admin key, no emergency stop. Once deployed, the code that ran on 2026-09-23 is the code that will always run at these addresses.

The only privileged actions, in full

The treasury is the sole privileged role in the entire system, and it can do exactly two things:

  1. withdrawFees() — pull its own accrued 0.5% fee (feesOwed). It cannot pull anything else: not prepaid (burn principal), not totalPending (miner payouts awaiting pull), not another address's pending balance.
  2. proposeTreasury(successor) / acceptTreasury() — hand the treasury role to a new address, in two steps (so a typo'd successor address can't accidentally lock the fee stream — the old treasury stays treasury until the new address explicitly accepts).

That's the complete list. There is no function anywhere in QaxeBurnVault or QaxeDepositFactory that lets the treasury redirect a burn, redirect a miner's payout, change any constant, or touch anything but its own fee.

Reentrancy

Both contracts guard every state-changing entry point with a standard nonReentrant modifier (_locked flag, require(_locked == 1)). QaxeBurnVault's test suite specifically drives hostile curve and token contracts that attempt to call back into sweep, syncAnonymous, credit, withdrawFees, withdrawPending, proposeTreasury, and acceptTreasury mid-execution — every one is blocked. QaxeDepositFactory is reentrancy-guarded the same way, and additionally, only the factory itself may call collectOne (require(msg.sender == address(this))), so a batch entry can't be reentered from outside the factory's own loop.

The vault's receive() is deliberately an empty, unguarded hook — it has to accept refunds and returned payments while a payment call is in flight and the lock is held, since _pay itself can trigger a call that bounces value straight back. It performs no state change itself; any actual accounting only happens when the guarded functions above re-check balances.

Hostile payout and refund contracts

Both a payout wallet (the miner-leg destination) and a refundTo address (the caller-chosen gas refund destination) can be arbitrary contracts, including malicious ones. The vault assumes neither can be trusted and handles three failure modes:

  • Gas-limited push, pull fallback. Every outbound payment (_pay) is capped at PAYOUT_GAS = 30000 gas. A payout or refund contract that reverts, or that simply runs out of that budget, cannot block anything: the payment attempt fails silently from the vault's point of view, and for a payout, the amount is credited to pending[payout] instead — the recipient pulls it later with withdrawPending(), on their own gas budget, whenever they want. A refund that fails is simply not paid; the reserved amount returns to prepaid as ordinary principal.
  • Returned-value reconciliation, not blind trust. _pay never assumes the full amount it sent actually stuck — it measures the vault's own balance delta and treats anything a recipient calls back as a partial or full return, crediting it back to prepaid with no new refundAllowance minted against it (see burn-vault). A hostile contract that "accepts" a payment only to immediately return some or all of it cannot use that trick to inflate its own future refund budget.
  • Isolation per entry. A hostile payout or refund contract on one deposit cannot affect any other deposit's collection, or the batch it's part of — QaxeDepositFactory.collectMany isolates each entry's revert via try/catch around the factory's own collectOne, and reports it as CollectFailed rather than aborting the batch.

A gas refund can never be profitable

Several independent bounds stack to make refund-farming a losing strategy, not merely an unprofitable edge case:

  • Priced from the chain, not the caller. refundPrice() = min(tx.gasprice, 2 * block.basefee) — a caller who inflates their own tx.gasprice (a "priority-fee farming" attempt) gains nothing past 2× the network's real basefee; the test suite specifically drives an absurd 50×-basefee gasprice and confirms the refund stays bounded regardless.
  • Bounded per-action, not per-caller. Every refund is capped at REFUND_CAP_BPS = 3% of the gross behind the specific deposit or sweep being refunded — there is no way to claim more by batching, repeating, or restructuring calls, because the cap is recomputed fresh from that action's own principal every time.
  • A contract caller gets no base-transaction claim. TX_BASE_GAS (21,000) and calldata cost are only added to a collection's claimed gas when the top-level caller (tx.origin) is the same address making the call — i.e. only a plain, unrelayed transaction can claim the cost it actually paid as the transaction. A contract calling through a relay cannot claim a base cost it never paid itself; the test suite confirms a relayed sweep's refund stays strictly under gasUsed * price.
  • Calldata cost is counted at the EVM's own real rate. _calldataGas() sums exactly 4 gas per zero calldata byte and 16 gas per nonzero byte — the same rates the EVM itself charges as intrinsic transaction gas. Padding a call with extra calldata to inflate the claimed overhead costs the caller precisely as much in real transaction gas as it could ever add to the claimed refund, so it cannot manufacture profit; it can, at best, break even on a value the caller was already going to pay for regardless.
  • Every measured calibration keeps refund ≤ real cost. Across the Hardhat suite and the Orchard rehearsal, every refund-over-cost ratio measured is at or below 1.0 (typically 0.79–1.00×, see burn-vault) — the tail constants (SWEEP_TAIL_GAS, CREDIT_TAIL_GAS, COLLECT_TAIL_GAS) were deliberately tuned conservative rather than to break even exactly, because underpaying by a little is safe and overpaying by any amount would not be.

Sandwich exposure

QaxeBurnVault.sweep has no independent price oracle — the only floor against a bad execution price is the caller-supplied minTokensOut, checked against the vault's actual post-buy token balance. This is an accepted design limitation, not an oversight: caps and cooldown bound how much and how often exposure exists, they do not by themselves guarantee a fair price if a caller supplies a weak floor.

The bounds that do exist:

  • Slice size. Each sweep buys at most min(1000 QUAI, 0.5% of the market's reserve) (SLICE_BPS = 50, ABS_MAX_SLICE = 1000 ether) — a sandwich on any single sweep is capped at what it can extract from 0.5% of reserve, roughly putting the sandwich break-even around ~1.02% of reserve given the round-trip fees involved.
  • Cooldown. COOLDOWN_BLOCKS = 120 between sweeps (after an unconditional first sweep) — the size cap is meaningless if an attacker can stack ten slices into one block; the cooldown forces genuinely separate blocks, and separate opportunities to notice and use a fresh, honest quote.
  • A stale-price rollback is proven, not assumed. The test suite drives a real price move between a caller's quote and their sweep call and confirms the transaction reverts ("Insufficient burn") and every piece of state — prepaid, refundAllowance, feesOwed, supply, the vault's balance — rolls back completely. What is not proven or claimed is protection when a caller supplies a weak floor in the first place; see integrate for how to build a floor from a fresh curve.quoteBuy immediately before sending.

Front-running collect

collect/collectMany/collectAndSweep are permissionless by design — anyone can collect anyone else's deposit address, and a caller can be front-run by a faster bot doing the exact same call. This is intentionally low-stakes: the split between burn principal and payout wallet is fixed by the deposit address's own salt (payout, burnBps) and is identical no matter who calls collect — front-running only competes for the small, bounded gas refund attached to refundTo, never for the miner's principal or payout. Losing that race costs a would-be collector only the gas of a reverted or unnecessary call; it never puts a miner's funds at risk.

What Hardhat cannot prove about Quai, and what Orchard did

Hardhat's in-process EVM models Ethereum-style gas accounting. It cannot show:

  • Quai's real account-creation cost (measured ~39,358 gas for a plain transfer to a fresh account, versus ~21,000 + 25,000 on an Ethereum-style chain) or its storage-growth pricing, which appears to run outside the interpreter's own gas accounting.
  • Whether a CREATE2-deployed, same-transaction selfdestruct genuinely clears EXTCODESIZE back to zero on real Quai state, leaving the address reusable.
  • Whether Quai's real gas pricing pushes the vault's calibrated refund constants above or below their intended ratio in practice.

scripts/orchard-rehearse-qaxe-v2.cjs exists specifically to close that gap on a real (non-mainnet) Quai chain — its last recorded run was PASS 12/12, proving (among the checks listed in deposit-factory): the forwarder's code is genuinely cleared and the address genuinely reusable after collection, a real graduated-curve sweep burns exactly the reported amount, and every measured refund-over-cost ratio stayed at or below 1.0 on real Quai gas — with the honest caveat that real Quai gas runs 15–20% above what Hardhat's in-process EVM sees, which is exactly why the refund ratios measured on Orchard (0.79–0.85×) sit below the Hardhat-measured ones (0.94–1.00×).

Review status

An independent adversarial/spec review of the accounting and sweep mechanics (see hartii-labs/contracts/docs/QaxeBurnVault-C2-review.md) found zero Critical and zero High findings. Two informational items were logged and are reflected in this document as accepted design limitations, not defects: the lack of an independent execution-price oracle for sweep (covered above, under Sandwich exposure), and — at the time of that review — that gas reimbursement was still staged and not yet calibrated against real receipts; it has since been calibrated (see burn-vault) and rehearsed end-to-end on Orchard.

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.