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:
- Sets
launched = true(GeneHook.sol:255). - Calls
PoolManager.initialize(poolKey(), sqrtPrice(GENESIS_TICK_UPPER))(GENESIS_TICK_UPPER). The hook's ownbeforeInitializeis skipped because the hook is the caller (GeneHook.sol:257-258; v4Hooks.sol:171-175). - 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 tomassLiquidityand emitsMassAdded(0, ...)(GeneHook.sol:259, GeneHook.sol:420-435). - Burns any GENE left in the hook (dust) (GeneHook.sol:262-263).
- Sets
launchBlock, seeds all sixBlockStateticks toGENESIS_TICK_UPPERand records the block inEpochAcc.lastBlock(GeneHook.sol:265-267). - 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:
_sync(), then_evolve(AUTO_EVOLVE_MAX): folds up to AUTO_EVOLVE_MAX pending closed epochs into the gene state machines (GeneHook.sol:287-289).Opens an unlock with
ACTION_METABOLIZE(GeneHook.sol:290). Inside it, reads spot,atlTickandathTickand resets the per-epoch counters if the epoch changed (GeneHook.sol:452-456, GeneHook.sol:794-800).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 toethReserve; once REGENERATION is EXPRESSED, REGEN_SHARE_BPS of them go toregenReserveinstead. GENE fees go togeneReserve(GeneHook.sol:707-718).ETH placements, only when
evolveCursorequals 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), widthNEAR_WIDTHGeneHook.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), widthNEAR_WIDTHGeneHook.sol:513-539 A placement below MIN_DEPLOY_ETH is skipped (GeneHook.sol:483, GeneHook.sol:496, GeneHook.sol:515).
Asks (kind 3), always attempted:
min(geneReserve, ASK_CAP_CALL, ASK_CAP_EPOCH - placed), skipped below MIN_DEPLOY_GENE; band ends atalignDown(min(spot, athTick) - GAP, GRID)(GeneHook.sol:541-552, GeneHook.sol:788-792).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:
_sync(), then an unlock withACTION_REGENERATE(GeneHook.sol:301-303). It does not run_evolve.- 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). - Price limit:
limitTick = atlTick - REGEN_PREMIUM_TICKS(REGEN_PREMIUM_TICKS). Ifspot <= limitTick, returns(0, 0, false)(GeneHook.sol:572-577). - Swaps ETH for GENE through the PoolManager:
zeroForOne, exact input of the allotment, price-limited atlimitTick(GeneHook.sol:578-582). Because the hook is the swapper, v4 skips itsbeforeSwapandafterSwap(v4Hooks.sol:253,Hooks.sol:293). - 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 emitsRegenerated(GeneHook.sol:583-589). - 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).