Pre-launch. RC4 code-frozen for external audit (rc4-audit-candidate, aca5fcd). Report pending. Status, updated 2026-10-08
GENESpecimen 001

Documentation · Reference

GeneHook

Every external and public function of GeneHook, with its signature, mutability, caller restrictions, behaviour, reverts and events.

GeneHook is the Uniswap v4 hook of the canonical GENE pool and holds the whole protocol: block-start price references, permanent epoch records, the gene state machines, fee reserves held as ERC-6909 claims at the PoolManager, and every protocol-owned liquidity position (GeneHook.sol:20-23). It has no proxy, no admin role and no upgrade path; no function checks the caller after launch() (GeneHook.sol:25-33).

All line references are to src/GeneHook.sol at tag rc4-audit-candidate (aca5fcd) unless another file is named. Constants are linked to Parameters instead of being restated.

#ABI pinning

The external ABI is exactly 27 functions. test/unit/HookAbi.t.sol reads methodIdentifiers from the forge build artifact and fails if any selector is added, removed or renamed (HookAbi.t.sol:19-62; invariant ABI). Selectors for every function are on Selectors.

Group Functions Count
Launch launch() 1
Keeper entry points metabolize(), regenerate(), evolve(uint256) 3
PoolManager callbacks unlockCallback(bytes), beforeInitialize, beforeAddLiquidity, beforeRemoveLiquidity, beforeSwap, afterSwap 6
Views REQUIRED_FLAGS, 5 immutables, launched, 6 storage getters, ethBudget, extsload, poolKey, immunityActive 17

There is no fallback and no receive. The five callbacks whose flag bits are clear (afterInitialize, afterAddLiquidity, afterRemoveLiquidity, beforeDonate, afterDonate) are not implemented, so a direct call to any of them reverts (GeneHook.sol:43-45). See Permissions and PoolKey.

#Constructor

constructor(IPoolManager manager, address launcher_)
Item Detail Source
Address check Reverts HookAddressInvalid() unless address(this) & Hooks.ALL_HOOK_MASK == REQUIRED_FLAGS (0x2AC0) GeneHook.sol:225-226
Prefix check Reverts HookAddressInvalid() if the top byte of the address is 0x91 GeneHook.sol:227-228
Argument checks Reverts HookAddressInvalid() if manager has no code or launcher_ is address(0) GeneHook.sol:229
Token Deploys GeneToken(address(this)), which mints the whole supply to the hook GeneHook.sol:233-234; GeneToken.sol:15-18
ERC-6909 id GENE_ID = uint256(uint160(token)) GeneHook.sol:235
PoolKey and poolId Builds the canonical PoolKey and stores poolId = key.toId() GeneHook.sol:236-244
Genome commitment genomeHash = keccak256(Genome.encode()) GeneHook.sol:245-246
Event Genesis(genomeHash, token, poolId) GeneHook.sol:247

Under CREATE2 through a deployer contract, msg.sender in the constructor is the deployer, which is why the launcher is a constructor argument (script/Deploy.s.sol:31-33).

#Launch

#launch()

function launch() external
Item Detail Source
Mutability nonpayable GeneHook.sol:252
Caller launcher only, once GeneHook.sol:253-255
Reverts NotLauncher() if msg.sender != launcher; AlreadyLaunched() on a second call; InvariantBroken() if the genesis add requires any ETH GeneHook.sol:253-254, GeneHook.sol:425

Behaviour, in order:

  1. Sets launched = true (GeneHook.sol:255).
  2. Calls PoolManager.initialize(poolKey(), sqrtPrice(GENESIS_TICK_UPPER)) (GENESIS_TICK_UPPER). The hook's own beforeInitialize is skipped because the hook is the caller (GeneHook.sol:257-258; v4 Hooks.sol:171-175).
  3. Opens an unlock with action ACTION_LAUNCH. Inside it, adds the genesis band [GENESIS_TICK_LOWER, GENESIS_TICK_UPPER] holding SUPPLY of GENE single-sided, transfers and settles the GENE, registers the band as position 0, adds its liquidity to massLiquidity and emits MassAdded(0, ...) (GeneHook.sol:259, GeneHook.sol:420-435).
  4. Burns any GENE left in the hook (dust) (GeneHook.sol:262-263).
  5. Sets launchBlock, seeds all six BlockState ticks to GENESIS_TICK_UPPER and records the block in EpochAcc.lastBlock (GeneHook.sol:265-267).
  6. Sets GENESIS to EXPRESSED with evidenceHash = keccak256(abi.encode(uint8(0), genomeHash, block.number)) (GeneHook.sol:268-270).

Events: MassAdded kind 0 (GeneHook.sol:433), GeneTransition(0, DORMANT, EXPRESSED, 0, evidenceHash) (GeneHook.sol:271), Launched (GeneHook.sol:272). The PoolManager emits its own Initialize and ModifyLiquidity, and GeneToken emits Transfer for the settlement and for any dust burn.

#Keeper entry points

All three are callable by any address after launch and behave the same whatever the caller (GeneHook.sol:33; test/unit/HookAbi.t.sol:11-12). Each first runs _sync(), which calls _newBlock if this is the block's first touch (GeneHook.sol:728-730). At an epoch boundary that first touch writes the closed epoch's record and budgets, so any of the three can emit EpochClosed and FlowBudgets (GeneHook.sol:747-765).

#metabolize()

function metabolize() external returns (uint256 ethToBids, uint256 ethToImmunity, uint256 geneToAsks)
Item Detail Source
Mutability nonpayable GeneHook.sol:281
Preconditions Launched; METABOLISM is EXPRESSED; at least COOLDOWN_BLOCKS since lastMetabolizeBlock (no wait if it is 0) GeneHook.sol:282-285
Cooldown write lastMetabolizeBlock = block.number is written at the start of every call that passes the checks, whether or not it places anything GeneHook.sol:286
Returns ethToBids: ETH placed as METABOLISM bids plus the churn tier. ethToImmunity: ETH placed by IMMUNITY. geneToAsks: GENE placed as asks GeneHook.sol:476, GeneHook.sol:488, GeneHook.sol:504, GeneHook.sol:521, GeneHook.sol:549

Behaviour, in order:

  1. _sync(), then _evolve(AUTO_EVOLVE_MAX): folds up to AUTO_EVOLVE_MAX pending closed epochs into the gene state machines (GeneHook.sol:287-289).

  2. Opens an unlock with ACTION_METABOLIZE (GeneHook.sol:290). Inside it, reads spot, atlTick and athTick and resets the per-epoch counters if the epoch changed (GeneHook.sol:452-456, GeneHook.sol:794-800).

  3. Harvest. Pokes the genesis position plus up to HARVEST_PER_CALL registry positions round-robin with a zero-liquidity modifyLiquidity, which returns only accrued LP fees (GeneHook.sol:671-697). ETH fees go to ethReserve; once REGENERATION is EXPRESSED, REGEN_SHARE_BPS of them go to regenReserve instead. GENE fees go to geneReserve (GeneHook.sol:707-718).

  4. ETH placements, only when evolveCursor equals the current epoch: IMMUNITY first if _immunityActive(), then METABOLISM bids, then the churn tier (GeneHook.sol:465-469). Each draws on its own budget from closed epoch e-1 and its own caps:

    Placement Amount Band Source
    IMMUNITY (kind 2) min(ethReserve, IMMUNITY budget left, IMMUNITY_CAP_EPOCH - placed, IMMUNITY_CAP_CALL) lo = alignUp(max(spot, atlTick) + GAP, GRID), width NEAR_WIDTH GeneHook.sol:493-507
    METABOLISM bid (kind 1) min(ethReserve, ethBudget() - placed, BID_CAP_EPOCH - placed, BID_CAP_CALL) top = alignUp(atlTick + GAP, GRID), range from _bidRange(top): [top, top + NEAR_WIDTH] in every state the tests reach (top >= 178,200, GenePermanence.t.sol:101) GeneHook.sol:479-491, GeneHook.sol:782-786
    Churn tier (kind 4) min(ethReserve, revenue(e-1) - bid(e-1) - churn placed, BID_CAP_EPOCH - bids - churn, BID_CAP_CALL) lo = alignUp(atlTick + GAP, GRID), width NEAR_WIDTH GeneHook.sol:513-539

    A placement below MIN_DEPLOY_ETH is skipped (GeneHook.sol:483, GeneHook.sol:496, GeneHook.sol:515).

  5. Asks (kind 3), always attempted: min(geneReserve, ASK_CAP_CALL, ASK_CAP_EPOCH - placed), skipped below MIN_DEPLOY_GENE; band ends at alignDown(min(spot, athTick) - GAP, GRID) (GeneHook.sol:541-552, GeneHook.sol:788-792).

  6. Settle. Fees collected are credits and principal added is debt; the net is minted or burned against the hook's own ERC-6909 claims for ETH (id 0) and GENE (id GENE_ID) (GeneHook.sol:474-475, GeneHook.sol:720-725).

Every placement goes through _deploy. If the liquidity would exceed v4's per-tick ceiling, it emits PlacementSkipped and consumes nothing (GeneHook.sol:612-627, GeneHook.sol:644-652). Fees auto-collected when adding to an existing band are credited like harvested fees, but are not included in FeesHarvested (GeneHook.sol:554-559, GeneHook.sol:688).

Reverts When Source
NotLaunched() Before launch() GeneHook.sol:282, GeneHook.sol:1000
GeneNotExpressed(1) METABOLISM is not EXPRESSED GeneHook.sol:283
Cooldown(readyAtBlock) Called before lastMetabolizeBlock + COOLDOWN_BLOCKS; the argument is that block GeneHook.sol:285
InvariantBroken() A poke returned principal, or an ETH placement needed GENE (or an ask needed ETH) GeneHook.sol:660, GeneHook.sol:694

Audit scope note: reachability of the [top, GENESIS_TICK_UPPER] branch of _bidRange, metabolize() reverting InvariantBroken while the price sits at or beyond the METABOLISM or churn bid band, and whether bids and churn together can exceed BID_CAP_EPOCH (see Security).

Events: EpochClosed, FlowBudgets (first touch at a boundary), GeneTransition (auto-evolve), FeesHarvested (GeneHook.sol:688), MassAdded kinds 1 to 4 (GeneHook.sol:626), PlacementSkipped (GeneHook.sol:620).

#regenerate()

function regenerate() external returns (uint256 ethSpent, uint256 geneBurned)
Item Detail Source
Mutability nonpayable GeneHook.sol:296
Preconditions Launched; REGENERATION is EXPRESSED; at least COOLDOWN_BLOCKS since lastRegenerateBlock (no wait if it is 0) GeneHook.sol:297-300
Cooldown write lastRegenerateBlock is written only when the call spent its whole allotment. A zero or partial spend starts no cooldown GeneHook.sol:302-307
Returns ETH spent on the buyback and GENE bought and burned GeneHook.sol:303, GeneHook.sol:308

Behaviour, in order:

  1. _sync(), then an unlock with ACTION_REGENERATE (GeneHook.sol:301-303). It does not run _evolve.
  2. Allotment: min(regenReserve, REGEN_CAP_CALL, REGEN_CAP_EPOCH - spent this epoch, REGEN_SHARE_BPS of _budgets[e-1].bid - spent this epoch). If it is below MIN_DEPLOY_ETH, returns (0, 0, false) (GeneHook.sol:562-571).
  3. Price limit: limitTick = atlTick - REGEN_PREMIUM_TICKS (REGEN_PREMIUM_TICKS). If spot <= limitTick, returns (0, 0, false) (GeneHook.sol:572-577).
  4. Swaps ETH for GENE through the PoolManager: zeroForOne, exact input of the allotment, price-limited at limitTick (GeneHook.sol:578-582). Because the hook is the swapper, v4 skips its beforeSwap and afterSwap (v4 Hooks.sol:253, Hooks.sol:293).
  5. Pays the ETH by burning the hook's ETH claims, takes the GENE to the hook, debits regenReserve, adds to the epoch's REGENERATION counter and emits Regenerated (GeneHook.sol:583-589).
  6. After the unlock, burns the GENE bought with GeneToken.burn (GeneHook.sol:308).
Reverts When Source
NotLaunched() Before launch() GeneHook.sol:297
GeneNotExpressed(2) REGENERATION is not EXPRESSED GeneHook.sol:298
Cooldown(readyAtBlock) Called before lastRegenerateBlock + COOLDOWN_BLOCKS GeneHook.sol:300

Events: Regenerated (GeneHook.sol:589), GeneToken Transfer to address(0) for the burn, and EpochClosed / FlowBudgets on a first touch at a boundary.

A buyback runs while the price is no more than about 1% above the all-time-low price (step 3). Audit scope note: reachability of that window with supply-faithful balances (see Security).

#evolve(uint256 maxEpochs)

function evolve(uint256 maxEpochs) external returns (uint256 processed)
Item Detail Source
Mutability nonpayable GeneHook.sol:312
Preconditions Launched. No cooldown GeneHook.sol:312-316
Behaviour _sync(), then processes closed epochs from evolveCursor up to (not including) the current epoch, at most maxEpochs of them, and advances evolveCursor GeneHook.sol:313-329
Returns The number of epochs processed GeneHook.sol:315
Reverts NotLaunched() GeneHook.sol:313
Events GeneTransition for each state change; EpochClosed / FlowBudgets on a first touch at a boundary GeneHook.sol:908, GeneHook.sol:915, GeneHook.sol:928, GeneHook.sol:931, GeneHook.sol:945-946

Each processed epoch reads its stored EpochRecord (an epoch never written reads as all zero), shifts the IMMUNITY deployment-stress register, and steps METABOLISM, REGENERATION and IMMUNITY (GeneHook.sol:879-894). A gene can count an epoch only if its predecessor is EXPRESSED, the predecessor's expressedEpoch is earlier than that epoch, and the epoch is at least the gene's minimum epoch (GeneHook.sol:896-899). Expression emits two GeneTransition events in one call, to EXPRESSIBLE and then to EXPRESSED, and stores the evidence hash (GeneHook.sol:937-947). The predicates are in Genes and the thresholds in Parameters.

#PoolManager callbacks

#unlockCallback(bytes data)

function unlockCallback(bytes calldata data) external returns (bytes memory)
Item Detail Source
Mutability nonpayable GeneHook.sol:395
Caller The PoolManager only GeneHook.sol:396
Behaviour Decodes the action from data and dispatches to the launch, metabolize or regenerate body GeneHook.sol:397-406
Reverts NotPoolManager() for any other caller; UnexpectedCallback() unless the transient ACTION_SLOT is non-zero and equals the decoded action GeneHook.sol:396, GeneHook.sol:403

The hook writes the action to transient storage immediately before poolManager.unlock and clears it afterwards (GeneHook.sol:409-418), so the callback accepts only the action the hook itself started.

#beforeInitialize, beforeAddLiquidity, beforeRemoveLiquidity

function beforeInitialize(address, PoolKey calldata, uint160) external pure returns (bytes4)
function beforeAddLiquidity(address, PoolKey calldata, ModifyLiquidityParams calldata, bytes calldata) external pure returns (bytes4)
function beforeRemoveLiquidity(address, PoolKey calldata, ModifyLiquidityParams calldata, bytes calldata) external pure returns (bytes4)

All three are pure and always revert Forbidden() (GeneHook.sol:332-352). v4 does not call a hook's initialize and liquidity callbacks when the hook itself is the caller (v4 Hooks.sol:171-175), so only GeneHook can initialize its pool or add liquidity to it. When the PoolManager calls one of these for another caller, v4 wraps the revert in WrappedError(address,bytes4,bytes,bytes) with HookCallFailed() (v4 Hooks.sol:131-137). See Errors.

#beforeSwap

function beforeSwap(address, PoolKey calldata key, SwapParams calldata, bytes calldata)
    external returns (bytes4, BeforeSwapDelta, uint24)
Item Detail Source
Mutability nonpayable GeneHook.sol:355-357
Caller The PoolManager only (NotPoolManager()) GeneHook.sol:359
Key check Reverts WrongPool() unless currency0 == address(0), currency1 == token, fee == LP_FEE and tickSpacing == TICK_SPACING GeneHook.sol:360-363
First swap of a block Runs _newBlock(spot): finalizes the previous touched block, closes the epoch at a boundary, and updates the epoch extremes, athTick and atlTick GeneHook.sol:364, GeneHook.sol:734-780
Returns The selector, ZERO_DELTA and fee override 0: the hook takes no fee and changes no swap amount GeneHook.sol:365

#afterSwap

function afterSwap(address, PoolKey calldata, SwapParams calldata params, BalanceDelta delta, bytes calldata)
    external returns (bytes4, int128)
Item Detail Source
Mutability nonpayable GeneHook.sol:368-370
Caller The PoolManager only (NotPoolManager()). No key check GeneHook.sol:372
Net flow Adds signed net GENE leaving the curve to netGeneMicro (1e12-wei units, saturating): on buys the GENE out, on sells the GENE in net of the LP fee GeneHook.sol:378-383
Volume Adds the swap's ETH amount, in gwei, to the block's blockVolumeGwei (saturating) GeneHook.sol:375-384
Revenue On zeroForOne swaps (buys) only, adds ETH amount x LP_FEE / 1e6, in gwei, to revenueGwei (saturating). This is the evidence metric, not the fees collected GeneHook.sol:385-387
Returns The selector and delta 0 GeneHook.sol:389

#Views

Function Returns Behaviour Source
REQUIRED_FLAGS() uint160 Constant 0x2AC0: the required low 14 address bits GeneHook.sol:148-149
poolManager() IPoolManager Immutable, set in the constructor GeneHook.sol:161, GeneHook.sol:231
token() GeneToken Immutable: the GENE token deployed by the constructor GeneHook.sol:162, GeneHook.sol:233-234
launcher() address Immutable: the only address that can call launch() GeneHook.sol:163, GeneHook.sol:232
genomeHash() bytes32 Immutable keccak256(Genome.encode()). RC4 value: 0x39c130f616c32abfb40f8ab51eb54544cffd3e0d99602c71dc336bc8083c36ae GeneHook.sol:164, GeneHook.sol:245-246
poolId() PoolId (bytes32) Immutable keccak256(abi.encode(poolKey)) GeneHook.sol:165, GeneHook.sol:243-244
launched() bool true after launch() GeneHook.sol:173
ethReserve() uint128 Harvested ETH fees not yet placed, wei GeneHook.sol:181
regenReserve() uint128 REGENERATION's ETH share, wei GeneHook.sol:182, GeneHook.sol:709-712
geneReserve() uint128 Harvested GENE fees not yet placed GeneHook.sol:183
ethCommitted() uint128 Cumulative ETH principal placed into kinds 1, 2 and 4 (monotonic) GeneHook.sol:184, GeneHook.sol:624
geneCommitted() uint128 Cumulative GENE principal placed into asks, kind 3 (monotonic; the genesis band is excluded) GeneHook.sol:185, GeneHook.sol:625
massLiquidity() uint256 Sum of liquidity units ever added by the hook, genesis included (monotonic) GeneHook.sol:186, GeneHook.sol:432, GeneHook.sol:665
ethBudget() uint256 METABOLISM bid budget of the current epoch e: _budgets[e-1].bid, reduced by REGEN_SHARE_BPS once REGENERATION is EXPRESSED; 0 in epoch 0. Not carried over GeneHook.sol:808-817
extsload(bytes32[] slots) bytes32[] One sload per requested slot; no calls, no writes. See Storage layout GeneHook.sol:950-963
poolKey() PoolKey The canonical PoolKey GeneHook.sol:965-973
immunityActive() bool true when IMMUNITY is EXPRESSED, the last IMM_CONFIRM processed epochs were each deployment-stressed, and evolveCursor equals the current epoch GeneHook.sol:802-805, GeneHook.sol:990-992

ethBudget() and immunityActive() describe the last touched epoch until the new epoch's first touch by a swap or an entry point (GeneHook.sol:747-765). immunityActive() is a read-only signal: whether metabolize() then places ETH also depends on caps, reserves and the IMMUNITY budget (GeneHook.sol:987-989). Only a mined MassAdded event with kind 2 records an IMMUNITY placement. Typed views of all other state are in GeneLens.

Sources (22)
  • src/GeneHook.sol:20-47
  • src/GeneHook.sol:52-62
  • src/GeneHook.sol:131-166
  • src/GeneHook.sol:172-186
  • src/GeneHook.sol:224-248
  • src/GeneHook.sol:251-273
  • src/GeneHook.sol:276-329
  • src/GeneHook.sol:332-390
  • src/GeneHook.sol:395-435
  • src/GeneHook.sol:451-591
  • src/GeneHook.sol:594-725
  • src/GeneHook.sol:728-805
  • src/GeneHook.sol:808-873
  • src/GeneHook.sol:879-947
  • src/GeneHook.sol:950-1005
  • src/Genome.sol:12-86
  • test/unit/HookAbi.t.sol:6-62
  • test/invariant/GenePermanence.t.sol:101
  • lib/v4-core/src/libraries/Hooks.sol:131-137
  • lib/v4-core/src/libraries/Hooks.sol:171-175
  • lib/v4-core/src/libraries/Hooks.sol:253
  • lib/v4-core/src/libraries/Hooks.sol:293

Paths are relative to the GENE repository at tag rc4-audit-candidate (aca5fcd).