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

Documentation · Integrate

Verify the deployment

A step-by-step procedure to confirm that a GeneHook deployment is the frozen RC4 source, with the committed Genome, the right PoolKey and a correct launch.

Steps marked Offline run against the frozen source, frozen on 2026-10-07, and can be performed today. Steps marked After deployment or After launch use the addresses listed on addresses.

All source references are to tag rc4-audit-candidate, commit aca5fcd3bbc9ecb1026de58bea431d406d8ecfff.

Tooling. Foundry's cast and forge, and Python 3. Set:

export RPC_URL="RPC_ENDPOINT"                          # your RPC endpoint
export PM=0x000000000004444c5dc75cB358380D2e3dE08A90   # Uniswap v4 PoolManager, the Deploy.s.sol default (Deploy.s.sol:34)
export HOOK_ADDRESS="HOOK_ADDRESS"                     # after deployment
export TOKEN_ADDRESS="TOKEN_ADDRESS"                   # after deployment
export LAUNCHER="LAUNCHER_ADDRESS"                     # after deployment
export SALT="CREATE2_SALT"                             # after deployment
export REPOSITORY_URL="REPOSITORY_URL"                 # once published

#Fixed inputs

Item Value Source
Uniswap v4 PoolManager 0x000000000004444c5dc75cB358380D2e3dE08A90 Deploy.s.sol:34
CREATE2 deployer 0x4e59b44847b379578588920cA78FbF26c0B4956C Deploy.s.sol:31-33
Source commit aca5fcd3bbc9ecb1026de58bea431d406d8ecfff, tag rc4-audit-candidate git rev-parse rc4-audit-candidate
Compiler solc 0.8.26, optimizer on, 800 runs, evm_version = cancun, bytecode_hash = none foundry.toml:11-15
Constructor GeneHook(IPoolManager manager, address launcher_) GeneHook.sol:224
Hook address rule low 14 bits equal 0x2AC0; first byte is not 0x91 GeneHook.sol:226-228
genomeHash (VERSION 4) 0x39c130f616c32abfb40f8ab51eb54544cffd3e0d99602c71dc336bc8083c36ae GenomeHash.t.sol:11; Step 1
GeneHook runtime, immutables zeroed 23,389 B, sha256 17e1418e1ec5689bcdce02aeec08aeaf89aba182136a917d728cd854e084b4d4 Step 2
GeneHook creation code, no arguments 28,065 B, sha256 50bd04f6e5475057641316afe353bd1091c40770589183e2dd7f5ef4342ef373 Step 2
GeneToken runtime, immutable zeroed 1,980 B, sha256 e0aa669a766a4fe730268afb74d26f02f9f1598ec7dcd19a7f5d261a475df00e Step 2
GeneToken creation code, no arguments 2,940 B, sha256 b1965b5ff5dd2892fc88a01e008a662007026eda19d5df824e5b968f28ff076a Step 2
GeneLens runtime 10,582 B, sha256 3752bd7dff722250e6e6334795b8994a7b6614689b0fc278886114c32d2782a4 Step 2
GeneLens creation code 10,610 B, sha256 d14ed67685d2ab52f47acf59f75d02abd9e8b72a76ff06654cf151a3948f8958 Step 2

#Step 1. Recompute the genome hash (Offline)

genomeHash is keccak256(Genome.encode()), computed in the constructor (GeneHook.sol:245-246). Genome.encode() concatenates nine abi.encode groups in a fixed order (Genome.sol:89-115). Every value is a static type, so each takes one 32-byte word: 43 words, 1,376 bytes.

Group Values, in order Genome.sol lines
Pool 1 VERSION, SUPPLY, LP_FEE, TICK_SPACING, GENESIS_TICK_UPPER, GENESIS_TICK_LOWER 12, 15-19, 95
Pool 2 EPOCH_BLOCKS, MIN_BLOCK_VOLUME, PATH_CLAMP, GAP, GRID, NEAR_WIDTH 22-29, 96
Caps 1 BID_CAP_CALL, BID_CAP_EPOCH, IMMUNITY_CAP_CALL, IMMUNITY_CAP_EPOCH, ASK_CAP_CALL, ASK_CAP_EPOCH 34-39, 102
Caps 2 REGEN_CAP_CALL, REGEN_CAP_EPOCH, MIN_DEPLOY_ETH, MIN_DEPLOY_GENE, COOLDOWN_BLOCKS, HARVEST_PER_CALL 40-45, 103
Caps 3 REGEN_PREMIUM_TICKS, REGEN_SHARE_BPS, GENE_COUNT, AUTO_EVOLVE_MAX 46, 49, 61, 54, 104
Genes 1 MET_MIN_EPOCH, MET_REVENUE, MET_OBSERVE 64-66, 110
Genes 2 REG_MIN_EPOCH, REG_REVENUE, REG_ACTIVE_BLOCKS, REG_OBSERVE, REG_ETH_COMMITTED 70-74, 111
Genes 3 IMM_MIN_EPOCH, IMM_REVENUE, IMM_PATH_TICKS, IMM_WINDOW, IMM_REQUIRED 77-81, 112
Genes 4 IMM_DEPLOY_REVENUE, IMM_CONFIRM 85-86, 113

The values are listed on the Genome page. The script below writes them out in encoding order:

e(){ cast abi-encode "$@" | sed 's/^0x//'; }
P1=$(e "f(uint256,uint256,uint24,int24,int24,int24)" 4 1000000000000000000000000000 10000 60 178080 85920)
P2=$(e "f(uint256,uint256,uint24,int24,int24,int24)" 7200 50000000000000000 200 60 600 1200)
C1=$(e "f(uint256,uint256,uint256,uint256,uint256,uint256)" 3000000000000000000 10000000000000000000 1000000000000000000 1500000000000000000 10000000000000000000000000 30000000000000000000000000)
C2=$(e "f(uint256,uint256,uint256,uint256,uint256,uint256)" 500000000000000000 1000000000000000000 1000000000000000 1000000000000000000000 300 8)
C3=$(e "f(int24,uint256,uint8,uint256)" 100 2000 4 8)
G1=$(e "f(uint32,uint256,uint32)" 1 250000000000000000 2)
G2=$(e "f(uint32,uint256,uint32,uint32,uint256)" 14 250000000000000000 300 7 10000000000000000000)
G3=$(e "f(uint32,uint256,uint32,uint8,uint8)" 14 100000000000000000 3000 7 5)
G4=$(e "f(uint256,uint8)" 500000000000000000 2)
cast keccak 0x$P1$P2$C1$C2$C3$G1$G2$G3$G4
# expected: 0x39c130f616c32abfb40f8ab51eb54544cffd3e0d99602c71dc336bc8083c36ae

An independent cross-check is keccak256(GeneLens.genome()), because genome() returns Genome.encode() (GeneLens.sol:204-206).

After deployment:

cast call $HOOK_ADDRESS "genomeHash()(bytes32)" --rpc-url $RPC_URL     # must equal the expected hash

The constructor also emits Genesis(genomeHash, token, poolId) with genomeHash as topic 1 (GeneHook.sol:131, 247).

#Step 2. Rebuild and hash the bytecode (Offline)

git clone "$REPOSITORY_URL" gene && cd gene
git checkout rc4-audit-candidate          # aca5fcd3bbc9ecb1026de58bea431d406d8ecfff
git rev-parse HEAD
forge build
python3 - <<'PY'
import json, hashlib
for c in ("GeneHook", "GeneToken", "GeneLens"):
    a = json.load(open(f"out/{c}.sol/{c}.json"))
    for k in ("deployedBytecode", "bytecode"):
        b = bytes.fromhex(a[k]["object"][2:])
        print(c, k, len(b), hashlib.sha256(b).hexdigest())
PY

Every line must match the Fixed inputs table. In the artifact, the immutable slots are zero.

#Step 3. Recompute the init code hash and the CREATE2 address (After deployment)

The deployment sends salt ‖ initCode to the CREATE2 deployer, with initCode = creationCode ‖ abi.encode(poolManager, launcher) (Deploy.s.sol:44).

CC=$(python3 -c "import json;print(json.load(open('out/GeneHook.sol/GeneHook.json'))['bytecode']['object'])")
ARGS=$(cast abi-encode "c(address,address)" $PM $LAUNCHER | sed 's/^0x//')
IH=$(cast keccak ${CC}${ARGS})
cast compute-address 0x4e59b44847b379578588920cA78FbF26c0B4956C --salt $SALT --init-code-hash $IH
# must print $HOOK_ADDRESS

Also confirm that the deployment transaction's calldata is exactly salt ‖ creationCode ‖ abi.encode($PM, $LAUNCHER), and that sha256(creationCode) equals 50bd04f6…2ef373.

#Step 4. Compare the deployed runtime bytecode (After deployment)

The deployed runtime differs from the artifact only in the immutable slots. Zero those slots, using the offsets the compiler recorded in the artifact, then hash:

cast code $HOOK_ADDRESS --rpc-url $RPC_URL > hook.runtime
python3 - <<'PY'
import json, hashlib
a = json.load(open("out/GeneHook.sol/GeneHook.json"))
code = bytearray(bytes.fromhex(open("hook.runtime").read().strip()[2:]))
for refs in a["deployedBytecode"]["immutableReferences"].values():
    for r in refs:
        code[r["start"]:r["start"] + r["length"]] = bytes(r["length"])
print(len(code), hashlib.sha256(code).hexdigest())
# expected: 23389 17e1418e1ec5689bcdce02aeec08aeaf89aba182136a917d728cd854e084b4d4
PY

GeneHook declares six immutables (GeneHook.sol:161-166). Apply the same method to GeneToken, which has one immutable, GENESIS_HOLDER (GeneToken.sol:13); the expected result is 1980 e0aa669a…5df00e. Steps 5 to 8 check the values held in those slots.

#Step 5. Check the hook address bits (After deployment)

Check Expected Source
HOOK_ADDRESS & 0x3FFF 0x2AC0: beforeInitialize, beforeAddLiquidity, beforeRemoveLiquidity, beforeSwap, afterSwap GeneHook.sol:40-45, 148-149, 226; Hooks.sol:27-39
HOOK_ADDRESS >> 152 (first byte) not 0x91 GeneHook.sol:227-228
REQUIRED_FLAGS() 0x2AC0 GeneHook.sol:148-149
python3 -c "h=int('$HOOK_ADDRESS',16); print(hex(h & 0x3FFF), hex(h >> 152))"
cast call $HOOK_ADDRESS "REQUIRED_FLAGS()(uint160)" --rpc-url $RPC_URL

With the low 14 bits exactly 0x2AC0, no return-delta flag, no donate flag and no after-initialize or after-liquidity flag is set (GeneHook.sol:40-45). The constructor reverts HookAddressInvalid() at any other address (GeneHook.sol:226-229), so this check confirms that you are looking at the right contract.

#Step 6. Check the immutables and the token (After deployment)

cast call $HOOK_ADDRESS "poolManager()(address)" --rpc-url $RPC_URL      # == $PM
cast call $HOOK_ADDRESS "launcher()(address)" --rpc-url $RPC_URL         # == $LAUNCHER
cast call $HOOK_ADDRESS "token()(address)" --rpc-url $RPC_URL            # == $TOKEN_ADDRESS
cast compute-address $HOOK_ADDRESS --nonce 1                              # == $TOKEN_ADDRESS
cast call $TOKEN_ADDRESS "GENESIS_HOLDER()(address)" --rpc-url $RPC_URL  # == $HOOK_ADDRESS
cast call $TOKEN_ADDRESS "INITIAL_SUPPLY()(uint256)" --rpc-url $RPC_URL  # 1000000000000000000000000000
cast call $TOKEN_ADDRESS "decimals()(uint8)" --rpc-url $RPC_URL          # 18
  • The constructor deploys GeneToken with CREATE and passes its own address as the genesis holder (GeneHook.sol:233; GeneToken.sol:15-17). The token is the hook's first contract creation, at nonce 1.
  • The token mints the whole supply once, to the hook, in its constructor, and has no other mint function (GeneToken.sol:11-24).

Launcher. The launcher can call launch() once and has no other power (GeneHook.sol:33, 252-255). The chain accepts a launcher with code, so check it before launch:

cast code $LAUNCHER --rpc-url $RPC_URL      # must be 0x

#Step 7. Check the PoolKey and pool id (After deployment)

cast call $HOOK_ADDRESS "poolKey()((address,address,uint24,int24,address))" --rpc-url $RPC_URL
cast call $HOOK_ADDRESS "poolId()(bytes32)" --rpc-url $RPC_URL
cast keccak $(cast abi-encode "f(address,address,uint24,int24,address)" \
  0x0000000000000000000000000000000000000000 $TOKEN_ADDRESS 10000 60 $HOOK_ADDRESS)   # must equal poolId()
Field Expected Source
currency0 0x0000000000000000000000000000000000000000 (native ETH) GeneHook.sol:237, 967
currency1 $TOKEN_ADDRESS GeneHook.sol:238, 968
fee 10000 (1.00%, static; not the dynamic-fee flag) GeneHook.sol:239; Genome.sol:16
tickSpacing 60 GeneHook.sol:240; Genome.sol:17
hooks $HOOK_ADDRESS GeneHook.sol:241
poolId keccak256(abi.encode(key)) GeneHook.sol:243-244

GeneHook contains no call to updateDynamicLPFee (GeneHook.sol:26-27).

#Step 8. Check the pre-launch state (After deployment, before launch)

Check Expected Source
launched() false GeneHook.sol:173
token.balanceOf($HOOK_ADDRESS) 1000000000000000000000000000 GeneToken.sol:17
token.totalSupply() 1000000000000000000000000000 GeneToken.sol:12, 17
PoolManager slot0 for poolId zero: the pool is not initialized only the hook can initialize it (GeneHook.sol:258, 332-334)

Raw slot0 read on the PoolManager:

POOL_ID=$(cast call $HOOK_ADDRESS "poolId()(bytes32)" --rpc-url $RPC_URL)
cast call $PM "extsload(bytes32)(bytes32)" \
  $(cast keccak $(cast concat-hex $POOL_ID 0x0000000000000000000000000000000000000000000000000000000000000006)) --rpc-url $RPC_URL

The pool-state slot is keccak256(poolId ‖ POOLS_SLOT) with POOLS_SLOT = 6 (StateLibrary.sol:11). In the word, sqrtPriceX96 is bits 0-159, tick 160-183, protocolFee 184-207 and lpFee 208-231 (Slot0.sol:33-38).

#Step 9. Check the launch (After launch)

launch() initializes the pool at the opening tick and adds the genesis band holding the whole supply, in one transaction (GeneHook.sol:252-273, 420-435). Read the Launched(poolId, tickLower, tickUpper, liquidity, geneSeeded, dustBurned) log (GeneHook.sol:132, 272). Its block is launchBlock (GeneHook.sol:265).

Check Expected Source
Launched.tickLower / tickUpper 85920 / 178080 GeneHook.sol:272; Genome.sol:18-19
Launched.geneSeeded + Launched.dustBurned 1000000000000000000000000000 GeneHook.sol:262-263, 426
token.totalSupply() right after launch 1e27 - dustBurned GeneHook.sol:263
token.balanceOf($HOOK_ADDRESS) 0 GeneHook.sol:262-263; Launch.s.sol:41
launched() true GeneHook.sol:255
GeneLens launchBlock(hook), or slot 2 bits 0-39 the Launched block GeneHook.sol:265; GeneLens.sol:109-111
PoolManager slot0 tick at the end of the launch transaction 178080 GeneHook.sol:257-258; Launch.s.sol:39
PoolManager slot0 lpFee 10000 GeneHook.sol:239; Launch.s.sol:40
getLiquidity(poolId) at the opening tick 0: the band's upper tick equals the opening tick, so the band is out of range Pool.sol:209-234
immunityActive() false GeneHook.sol:802-805
GENESIS gene, slot 10 byte 0 3 (EXPRESSED) GeneHook.sol:268-269
GENESIS evidence, slot 11 keccak256(abi.encode(uint8(0), genomeHash, launchBlock)) GeneHook.sol:270
Slot 0 (BlockState) at launch every tick field 178080, epoch 0 GeneHook.sol:266

The launch is meant to go through a private bundle or private RPC: GENE has no anti-snipe surcharge, so transaction ordering is the only launch-block protection (Launch.s.sol:4-10). This is an operational requirement, not an on-chain property that can be checked.

#Step 10. Check the genesis position (After launch)

Check Expected Source
GeneLens positionCount(hook), or slot 18 >= 1 GeneLens.sol:89-91
GeneLens positionAt(hook, 0) lower = 85920, upper = 178080 GeneHook.sol:431; GeneLens.sol:93-98
PoolManager getPositionInfo(poolId, $HOOK_ADDRESS, 85920, 178080, bytes32(0)) liquidity at least Launched.liquidity (equal at launch; never decreases) GeneHook.sol:594-608
massLiquidity() right after launch Launched.liquidity GeneHook.sol:432

Raw storage decoding: storage layout.

#Step 11. Ongoing checks (After launch)

Liquidity is add-only. _modifyLiquidity is GeneHook's only call to PoolManager.modifyLiquidity, and its liquidity argument is a uint128 cast to a non-negative int256 (GeneHook.sol:594-608). For every registry position, PoolManager liquidity never decreases, and massLiquidity() never decreases (GeneHook.sol:432, 665). Every addition emits MassAdded. No other address can add or remove liquidity in this pool: beforeAddLiquidity and beforeRemoveLiquidity always revert, and only the hook's own calls skip them (GeneHook.sol:41-43, 337-352).

Claims and reserves. Each metabolize() unlock settles against the hook's own ERC-6909 claims (GeneHook.sol:720-725). The invariant suite asserts that the hook's ETH claim balance equals ethReserve + regenReserve, and its GENE claim balance equals geneReserve, plus any claims a third party transferred to the hook (GenePermanence.t.sol:139-151). So on chain, each claim balance must be at least the matching reserve sum:

cast call $PM "balanceOf(address,uint256)(uint256)" $HOOK_ADDRESS 0 --rpc-url $RPC_URL                             # >= ethReserve() + regenReserve()
cast call $PM "balanceOf(address,uint256)(uint256)" $HOOK_ADDRESS $(cast to-dec $TOKEN_ADDRESS) --rpc-url $RPC_URL   # >= geneReserve()

The GENE claim id is uint256(uint160(token)) (GeneHook.sol:235). Audit scope note: the claims-to-reserves relation on every path, including rounding in the fee split (see Security).

Gene evidence. For each expressed gene, recompute the evidence hash from stored records and compare it with storage, the GeneTransition logs and GeneLens. The procedure is in verify evidence hashes.

#Step 12. Verify source on Etherscan (After deployment)

forge verify-contract $HOOK_ADDRESS src/GeneHook.sol:GeneHook --chain mainnet \
  --compiler-version v0.8.26 --num-of-optimizations 800 --evm-version cancun \
  --constructor-args $(cast abi-encode "constructor(address,address)" $PM $LAUNCHER)
forge verify-contract $TOKEN_ADDRESS src/GeneToken.sol:GeneToken --chain mainnet \
  --compiler-version v0.8.26 --num-of-optimizations 800 --evm-version cancun \
  --constructor-args $(cast abi-encode "constructor(address)" $HOOK_ADDRESS)

Compiler settings: foundry.toml:11-15. Constructor signatures: GeneHook.sol:224, GeneToken.sol:15. GeneLens is optional, takes no constructor arguments and is never called by GeneHook (GeneLens.sol:12-16); a GeneLens address is not part of the protocol.

#Step 13. Reproduce the launch on a fork (Offline)

test/rehearsal/LaunchRehearsal.t.sol deploys and launches the frozen source on a mainnet fork at block 26,123,400 and asserts the parameters above. It reads GENE_FORK_RPC with vm.envString, so every test fails, rather than skips, when the variable is unset (LaunchRehearsal.t.sol:11, 124). Rehearsal addresses are fork-only and are never GENE addresses.

#Not available before deployment

Item Status
GeneHook, GeneToken and pool id Listed on addresses after deployment
Launcher address, CREATE2 salt, init code hash with arguments Chosen at deployment (Deploy.s.sol:36-46)
Deployment and launch transactions, launchBlock Recorded at deployment and launch
Etherscan verification links Added after Step 12
Public repository location (REPOSITORY_URL) Listed here once published
Sources (30)
  • foundry.toml:11-15
  • script/Deploy.s.sol:31-46
  • script/Launch.s.sol:4-10
  • script/Launch.s.sol:38-42
  • src/Genome.sol:12-115
  • src/GeneHook.sol:25-45
  • src/GeneHook.sol:131-132
  • src/GeneHook.sol:148-149
  • src/GeneHook.sol:161-186
  • src/GeneHook.sol:224-273
  • src/GeneHook.sol:332-352
  • src/GeneHook.sol:420-435
  • src/GeneHook.sol:594-608
  • src/GeneHook.sol:665
  • src/GeneHook.sol:707-725
  • src/GeneHook.sol:802-805
  • src/GeneHook.sol:965-982
  • src/GeneToken.sol:11-24
  • src/GeneLens.sol:12-16
  • src/GeneLens.sol:89-111
  • src/GeneLens.sol:133-147
  • src/GeneLens.sol:204-206
  • test/unit/GenomeHash.t.sol:11
  • test/invariant/GenePermanence.t.sol:139-151
  • test/rehearsal/LaunchRehearsal.t.sol:11
  • test/rehearsal/LaunchRehearsal.t.sol:124
  • lib/v4-core/src/libraries/Hooks.sol:27-39
  • lib/v4-core/src/libraries/StateLibrary.sol:11
  • lib/v4-core/src/types/Slot0.sol:33-38
  • lib/v4-core/src/libraries/Pool.sol:209-234

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