Chapter 11 · Security model

Security model

What no one can do to a live pool, the locks, Arc EVM rules, the invariants and the test suites.

Hookarc's security model is built on subtraction: the functions that would let anyone move liquidity, change a live pool's rules or reach into a pool from the outside do not exist. What remains is a small set of owner powers over future launches and fee routing, and a set of invariants that the test suite enforces with reverts treated as failures. On top of that sit a handful of Arc-specific EVM rules that the contracts are written to respect.

What nobody can do

Trust boundariesEvery privileged call in the system, by role. The right-hand column has no caller at all: those paths do not exist in the bytecode.
nobody can · no such code pathHook ownerclaimProtocolFees(quote, to)setFeeSink(quote, sink)setTrustedRouter(router, bool)Factory ownersetHook · oncesetLaunchesPaused(bool)withdrawCreationFees(to)Registry owneradd(quote, feed, heartbeat)disableForNewLaunches(quote)setSequencerFeed · unused on ArcBurner ownerconfigure(key, cap) · oncebuybackAndBurn ≤ cap · 1 / blockAnyonebuy / sell through the RoutersweepProtocolFees(quote)claimPot · own winningsremove the seed liquiditychange a live pool's blocks or feesmint, pause or tax a LaunchTokenreach a live pool via the registryupgrade any contractOwnership moves only viatransferOwnership → acceptOwnership(hook, factory, burner) or a directtransfer on the registry.
  • Remove liquidity. The LPLocker has no decrease or withdraw function, and the hook's beforeRemoveLiquidity reverts unconditionally with LiquidityLocked. The seed position exists until the chain does.
  • Add liquidity. beforeAddLiquidity accepts the locker exactly once per pool (seeded flips true); anyone else gets NotAuthorized.
  • Change a live pool. Blocks and their parameters are written once in register(); there is no setter. The fee split is a constant. The quote and orientation are immutable pool state.
  • Mint or tax the token. LaunchToken has no owner, no mint, no pause, no blacklist and no transfer hook. burn only burns the caller's own balance.
  • Upgrade. No proxies anywhere.
  • Reach a pool through the registry. The registry is read at launch and by Halt Guard; disabling a quote affects only whether new launches succeed, and Halt Guard pools react only to the feed and the token's paused() flag.

What owners can do

RolePowers
Hook ownerclaimProtocolFees, setFeeSink, setTrustedRouter, two-step ownership transfer
Factory ownersetHook (once), setLaunchesPaused, withdrawCreationFees, two-step ownership transfer
Registry owneradd, disableForNewLaunches, setSequencerFeed (unused on Arc), direct ownership transfer
Burner ownerconfigure (once), setMaxQuotePerCall, buybackAndBurn within the cap and once per block, two-step ownership transfer

Pausing launches stops new pools; it does not stop swaps or claims on existing pools. Marking a router trusted only changes who the hook believes about trader attribution (for the pot and the event); an untrusted caller is labelled with tx.origin.

Locks and ordering

  • Launch lock (EIP-1153). Set in register, checked in beforeSwap, cleared by the end of the transaction. No swap can happen in the launch transaction.
  • Unlock discipline. Every PoolManager interaction goes through unlock callbacks that check msg.sender == poolManager. The hook's receive() only accepts native value from the PoolManager.
  • Pot advance once per block. potLastBlock prevents landing on N by repetition inside one block.
  • Fee determinism. _buyFees is a view of frozen config plus block.number, called identically in beforeSwap (to book) and afterSwap (to report). Ledgers and events cannot diverge.
  • Exact-input only. Removes the class of bugs where a fee has to be solved for on the output side.

Arc EVM notes

Arc is an EVM chain (Osaka) with a few rules that differ from a plain Ethereum node. The contracts and scripts honour each of them:

Arc ruleWhat it means for Hookarc
Native value to address(0) reverts (Zero address not allowed); burning native USDC is forbiddenNothing ever sends value to the zero address. The burner delivers $HARC to 0x000…dEaD; LaunchToken.burn is an ERC-20 supply reduction, not a transfer.
Circle's blocklist is enforced at the protocol level, before the mempool and again at execution; the fee is still chargedA claim, sweep or withdrawCreationFees to a blocked address reverts and the ledger keeps the balance. Choose recipients that can receive USDC.
PREVRANDAO is always 0Nothing in Hookarc uses on-chain randomness. The pot counter is deterministic by design.
No public mempool (eth_newPendingTransactionFilter is unsupported)A launch cannot be seen before it lands; the launch lock still guards the same block. Front-running a specific buy is not possible from the mempool, but a bot can still react to events in the next block.
Blob (type-3) transactions are rejected; EIP-155 is mandatoryDeploy scripts use plain EIP-1559 transactions with the chain id.
Sub-second blocks can share a timestampThe app orders swaps by block number and log index, never by timestamp alone.
The public RPC caps eth_getLogs at roughly 10,000 blocks and rate-limits aggressivelyThe web app's log scanner halves its range on failure and shares one in-flight scan across callers.

Invariants

The invariant suite runs with fail_on_revert = true, so an unexpected revert in the handler counts as a failure rather than a skipped step:

InvariantStatement
invariant_claimsCoverLedgersFor every quote, the hook's ERC-6909 claim balance is at least protocolFees + Σ creatorFees of pools in that quote.
invariant_claimsCoverLedgersWithPotThe same, adding pot and Σ potOwed.
invariant_hookHoldsNoLooseBalancesThe hook's native balance and ERC-20 balances are zero.
invariant_seedLiquidityLockedThe seed position's liquidity never decreases.
invariant_burnOnlyShrinksSupplytotalSupply of a LaunchToken never increases and equals 1e9 minus what was burned.
invariant_potCounterMonotonepotCount never decreases and pot is zero right after a win.

Test coverage

  • Unit suites covering every contract, both quote orientations (quoteIsCurrency0 true and false), 6- and 8-decimal quotes, the dust branch, partial fills, refunds and every guard error, asserted as exact wrapped hook errors.
  • Two invariant suites (Solvency, BlocksInvariant) driven by a handler that launches, buys, sells, claims and sweeps at random.
  • Fork suites against the public Arc RPC: one wires the full system on the canonical PoolManager, launches, buys and sweeps; one reads the live USDC and cirBTC feeds and paused() flags. Both skip when the RPC is unreachable.
  • Tests use isolate = true because the launch lock is transient storage.

Known limits

  • Not audited. No external audit has been performed. The design decision is that no mainnet marketing happens before one.
  • No fairness guarantee. Anti-Snipe bounds per-block volume and taxes early buys; it does not stop bots, MEV or splitting across addresses.
  • Oracle dependence at the edges. The opening price and Halt Guard depend on Chainlink and on the quote token's paused() flag. The AMM itself never reads the oracle.
  • Issuer-controlled quotes. A paused USDC or cirBTC cannot be transferred; a pool quoted in it is stuck until Circle unpauses, whatever Hookarc does. A blocklisted trader cannot receive their quote back either.
  • Trusted router attribution. If the owner marked a malicious router trusted, it could misattribute pot winners. Routers can be untrusted again; the pot is the only thing attribution affects.
read the code
  • contracts/test/invariant/Solvency.t.sol claims and balances
  • contracts/test/invariant/BlocksInvariant.t.sol supply and pot invariants
  • contracts/test/Guards.t.sol every guard error, exact-matched
  • contracts/test/fork/ArcFork.t.sol the live feeds, tokens and PoolManager on Arc
  • contracts/foundry.toml isolate, fail_on_revert, profiles, the arc RPC alias