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

Documentation · Integrate

Swapping

How to swap ETH and GENE in the canonical Uniswap v4 pool through the Universal Router, with the PoolKey to use and the fees a swap pays.

This page covers GENE's one canonical Uniswap v4 pool: native ETH against GENE, with GeneHook as the pool's hook. GENE is a plain ERC-20, so other pools can list it; their trades never reach GeneHook (GeneToken.sol:6-10). HOOK_ADDRESS and the token address are listed on addresses. The snippets also run against a fork or devnet where you deploy the frozen source.

#The PoolKey

Field Value Source
currency0 address(0), native ETH GeneHook.sol:237, 967
currency1 the GENE token, GeneHook.token() GeneHook.sol:238, 968
fee 10000 (1.00%), static GeneHook.sol:239; Genome.sol:16
tickSpacing 60 GeneHook.sol:240; Genome.sol:17
hooks HOOK_ADDRESS GeneHook.sol:241

Read it from the hook instead of typing it: GeneHook.poolKey() returns this key (GeneHook.sol:965-973), and poolId() is its hash (GeneHook.sol:243-244). Pass empty hookData. A buy of GENE is zeroForOne = true (ETH in); a sell is zeroForOne = false (GeneHook.sol:385).

beforeSwap reverts WrongPool for any key whose currencies, fee or tick spacing differ (GeneHook.sol:360-363). Since beforeInitialize always reverts, no other pool can use this hook (GeneHook.sol:332-334).

#Fees a swap pays

  • The 1% LP fee. Uniswap v4 charges the static 1% LP fee on the input token: ETH on buys, GENE on sells. It accrues to the hook's positions, because the hook is the pool's only liquidity provider (GeneHook.sol:337-343).
  • No hook fee. beforeSwap returns a zero delta and a fee override of 0; afterSwap returns a delta of 0 (GeneHook.sol:365, 389). The pool has no return-delta permission (GeneHook.sol:40-45). Nothing in GeneHook can change the fee: the key is static and the hook never calls updateDynamicLPFee (GeneHook.sol:25-27; invariants FEE1 and FEE2).
  • The Uniswap protocol fee is governed by Uniswap and is outside GENE's control. v4 core caps it at 0.1% per direction, and none of it reaches GENE's positions (ProtocolFeeLibrary.sol:8). Read the current value from slot0.protocolFee (read state).

#Routes tested

The fork suite runs against Ethereum mainnet state with the real PoolManager, Universal Router 2.0 and 2.1.2, Permit2, V4Quoter and multi-hop routes. Exact-in and exact-out buys and sells succeed, and a route that hits the pool twice in one unlock succeeds (invariants RT1 and RT2). The invariant suite also sells a small and a very large GENE balance after each step of its runs (GenePermanence.t.sol:167-185). GeneHook contains no rule that rejects a swap by size, direction or sender (GeneHook.sol:355-390). Audit scope note: revert paths in the swap callbacks across all states (see Security).

The two router versions take different parameter structs for v4 swaps: 2.1.2 adds a minHopPriceX36 field (GeneFork.t.sol:55-100). Use the struct that matches the router you call.

Measured gas for a swap through the Universal Router: 126,054 for the first swap in a block, 114,902 for a later swap in the same block, 203,942 for the swap that closes an epoch. The first swap of each block records the block-start price (GeneHook.sol:364).

Routing is not a GENE property. Whether the Uniswap interface, an aggregator or a solver routes trades to this pool is decided off-chain by those services, outside GENE's control. The constructor rejects hook addresses whose first byte is 0x91, citing a Uniswap routing policy (GeneHook.sol:227-228); that policy belongs to Uniswap. Calling the Universal Router directly, as below, does not depend on an interface or aggregator choosing this pool.

#Buy GENE with viem (exact input)

The command sequence matches the fork tests: one V4_SWAP command (0x10) with the actions SWAP_EXACT_IN_SINGLE (0x06), SETTLE_ALL (0x0c) and TAKE_ALL (0x0f), then SWEEP (0x04) for leftover ETH (GeneFork.t.sol:214-245). The parameter layout below is the Universal Router 2.0 layout.

// buy.mjs   (Node 18+, npm install viem)
// env: RPC_URL, PRIVATE_KEY, HOOK_ADDRESS (after deployment),
//      UNIVERSAL_ROUTER_ADDRESS and V4_QUOTER_ADDRESS (from Uniswap's deployment list)
import { createPublicClient, createWalletClient, http, parseAbi, parseEther, encodeAbiParameters } from "viem";
import { privateKeyToAccount } from "viem/accounts";

const { RPC_URL, PRIVATE_KEY, HOOK_ADDRESS, UNIVERSAL_ROUTER_ADDRESS, V4_QUOTER_ADDRESS } = process.env;
const ETH_IN = parseEther("0.1");
const SLIPPAGE_BPS = 100n; // 1% below the quote

const POOL_KEY = {
  type: "tuple",
  components: [
    { name: "currency0", type: "address" },
    { name: "currency1", type: "address" },
    { name: "fee", type: "uint24" },
    { name: "tickSpacing", type: "int24" },
    { name: "hooks", type: "address" },
  ],
};
const EXACT_IN_SINGLE = {
  type: "tuple",
  components: [
    { name: "poolKey", ...POOL_KEY },
    { name: "zeroForOne", type: "bool" },
    { name: "amountIn", type: "uint128" },
    { name: "amountOutMinimum", type: "uint128" },
    { name: "hookData", type: "bytes" },
  ],
};

const hookAbi = parseAbi(["function poolKey() view returns ((address currency0, address currency1, uint24 fee, int24 tickSpacing, address hooks))"]);
const quoterAbi = parseAbi([
  "function quoteExactInputSingle(((address currency0, address currency1, uint24 fee, int24 tickSpacing, address hooks) poolKey, bool zeroForOne, uint128 exactAmount, bytes hookData) params) returns (uint256 amountOut, uint256 gasEstimate)",
]);
const routerAbi = parseAbi(["function execute(bytes commands, bytes[] inputs, uint256 deadline) payable"]);

const account = privateKeyToAccount(PRIVATE_KEY);
const pub = createPublicClient({ transport: http(RPC_URL) });
const wallet = createWalletClient({ account, transport: http(RPC_URL) });

const key = await pub.readContract({ address: HOOK_ADDRESS, abi: hookAbi, functionName: "poolKey" });

const { result: [quotedOut] } = await pub.simulateContract({
  address: V4_QUOTER_ADDRESS, abi: quoterAbi, functionName: "quoteExactInputSingle",
  args: [{ poolKey: key, zeroForOne: true, exactAmount: ETH_IN, hookData: "0x" }],
});
const minOut = (quotedOut * (10_000n - SLIPPAGE_BPS)) / 10_000n;

const actions = "0x060c0f"; // SWAP_EXACT_IN_SINGLE, SETTLE_ALL, TAKE_ALL
const params = [
  encodeAbiParameters([EXACT_IN_SINGLE], [{ poolKey: key, zeroForOne: true, amountIn: ETH_IN, amountOutMinimum: minOut, hookData: "0x" }]),
  encodeAbiParameters([{ type: "address" }, { type: "uint256" }], [key.currency0, ETH_IN]), // SETTLE_ALL: ETH in
  encodeAbiParameters([{ type: "address" }, { type: "uint256" }], [key.currency1, minOut]), // TAKE_ALL: GENE out
];
const inputs = [
  encodeAbiParameters([{ type: "bytes" }, { type: "bytes[]" }], [actions, params]),
  encodeAbiParameters([{ type: "address" }, { type: "address" }, { type: "uint256" }], [key.currency0, account.address, 0n]), // SWEEP leftover ETH
];
const deadline = BigInt(Math.floor(Date.now() / 1000) + 300);

const hash = await wallet.writeContract({
  address: UNIVERSAL_ROUTER_ADDRESS, abi: routerAbi, functionName: "execute",
  args: ["0x1004", inputs, deadline], value: ETH_IN,
});
console.log("buy tx", hash, "min GENE out", minOut);

#Sell GENE

A sell uses the same actions with zeroForOne = false, SETTLE_ALL on currency1 (GENE) and TAKE_ALL on currency0 (ETH), no value and no SWEEP. The router pulls GENE through Permit2, so approve first (GeneFork.t.sol:229-233, 259-267):

// sell-approve.mjs, continues buy.mjs; env: PERMIT2_ADDRESS (from Uniswap's deployment list)
const erc20Abi = parseAbi(["function approve(address spender, uint256 amount) returns (bool)"]);
const permit2Abi = parseAbi(["function approve(address token, address spender, uint160 amount, uint48 expiration)"]);
const GENE_IN = parseEther("1000");
const expiration = Math.floor(Date.now() / 1000) + 3600;

await wallet.writeContract({ address: key.currency1, abi: erc20Abi, functionName: "approve", args: [process.env.PERMIT2_ADDRESS, GENE_IN] });
await wallet.writeContract({
  address: process.env.PERMIT2_ADDRESS, abi: permit2Abi, functionName: "approve",
  args: [key.currency1, UNIVERSAL_ROUTER_ADDRESS, GENE_IN, expiration],
});

Wait for each approval to be mined before sending the swap.

#Before you trade

  • The pool's only liquidity is protocol-owned Permanent Mass. Protocol bids sit at or below the all-time low, so in a falling market a sell near spot can meet no protocol depth; size the trade from the quote (known limitations).
  • Check slot0 and the quote at the block you trade; execution follows the pool state when your transaction is included.

Related: permissions and PoolKey, fee anatomy, pool and hook.

Sources (13)
  • src/GeneHook.sol:25-27
  • src/GeneHook.sol:40-45
  • src/GeneHook.sol:227-228
  • src/GeneHook.sol:236-244
  • src/GeneHook.sol:355-390
  • src/GeneHook.sol:965-973
  • src/Genome.sol:16-17
  • test/fork/GeneFork.t.sol:39-49
  • test/fork/GeneFork.t.sol:55-100
  • test/fork/GeneFork.t.sol:214-277
  • test/invariant/GenePermanence.t.sol:167-185
  • src/GeneToken.sol:6-10
  • lib/v4-core/src/libraries/ProtocolFeeLibrary.sol:8

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