Developers

Pools

View as Markdown

A pool on Giwater is a storage entry in CLPoolManager, not a contract. It is addressed by its PoolKey, and by the pool ID, the hash of that key. This page defines both, the fee fields as this deployment sets them, and how to list every pool from the chain.

The snippets use viem 2; only find-pools.ts talks to the chain. Addresses are on Contracts.

PoolKey

Currency, IHooks and IPoolManager are addresses in the ABI, so the key encodes as (address, address, address, address, uint24, bytes32).

solidity
struct PoolKey {
  Currency currency0;
  Currency currency1;
  IHooks hooks;
  IPoolManager poolManager;
  uint24 fee;
  bytes32 parameters;
}
  • currency0, currency1 — the two tokens, sorted: currency0 must be numerically lower than currency1, or initialize reverts with CurrenciesInitializedOutOfOrder. Native ETH is the zero address, so it is always currency0. WETH9 (0x4200…0006) is an ordinary ERC-20; a native-ETH pool and a WETH9 pool for the same pair are two different pools.
  • hooks — the hook contract, or the zero address for none. Every seeded pool is hookless, and the app trades and provides liquidity only in hookless pools.
  • poolManager — the CLPoolManager address. initialize reverts if it is not the manager being called.
  • fee — the LP fee in hundredths of a basis point (pips; 1_000_000 = 100%). It may not exceed 1_000_000. Bit 23 (0x800000) is the dynamic-fee flag: the pool starts with an LP fee of 0 and its hook sets the fee through updateDynamicLPFee. A hookless pool must not set it. No seeded pool uses it.
  • parameters — the tick spacing and the hook callback bitmap, packed into one word. See Parameters.

Change any field and the key names a different pool, or one that was never initialized.

Parameters

parameters is a bytes32 read as a bit field, bit 0 being the least significant (CLPoolParametersHelper):

  • Bits 0–15 — the hook registration bitmap, one bit per callback. With hooks set, it must equal the hook contract's getHooksRegistrationBitmap(); with hooks at the zero address, it must be 0. Either mismatch reverts with HookConfigValidationError.
  • Bits 16–39 — the tick spacing, 24 bits. initialize accepts 1 to 32767 (TickMath.MIN_TICK_SPACING to MAX_TICK_SPACING).
  • Bits 40–255 — unused, and must be zero.

The callback bits, from ICLHooks.sol:

  • 0 beforeInitialize · 1 afterInitialize
  • 2 beforeAddLiquidity · 3 afterAddLiquidity
  • 4 beforeRemoveLiquidity · 5 afterRemoveLiquidity
  • 6 beforeSwap · 7 afterSwap
  • 8 beforeDonate · 9 afterDonate
  • 10 beforeSwapReturnsDelta · 11 afterSwapReturnsDelta
  • 12 afterAddLiquidityReturnsDelta · 13 afterRemoveLiquidityReturnsDelta

For a hookless pool the word is just tickSpacing << 16: tick spacing 60 is 0x…003c0000.

parameters.ts
import { pad, toHex, type Hex } from "viem";

// Bit offsets of the CL hook callbacks in parameters bits 0-15 (ICLHooks.sol).
export const HOOK_OFFSETS = {
  beforeInitialize: 0,
  afterInitialize: 1,
  beforeAddLiquidity: 2,
  afterAddLiquidity: 3,
  beforeRemoveLiquidity: 4,
  afterRemoveLiquidity: 5,
  beforeSwap: 6,
  afterSwap: 7,
  beforeDonate: 8,
  afterDonate: 9,
  beforeSwapReturnsDelta: 10,
  afterSwapReturnsDelta: 11,
  afterAddLiquidityReturnsDelta: 12,
  afterRemoveLiquidityReturnsDelta: 13,
} as const;

// bits 0-15: hook callback bitmap, bits 16-39: tick spacing, bits 40-255: zero.
export function encodeParameters(tickSpacing: number, hookBitmap = 0): Hex {
  if (!Number.isInteger(tickSpacing) || tickSpacing < 1 || tickSpacing > 32_767) {
    throw new Error("tickSpacing must be 1 to 32767");
  }
  if (!Number.isInteger(hookBitmap) || hookBitmap < 0 || hookBitmap > 0xffff) {
    throw new Error("hookBitmap must fit in 16 bits");
  }
  return pad(toHex((BigInt(tickSpacing) << 16n) | BigInt(hookBitmap)), { size: 32 });
}

export function decodeParameters(parameters: Hex) {
  const word = BigInt(parameters);
  if (word >> 40n !== 0n) throw new Error("bits 40-255 must be zero");
  const hookBitmap = Number(word & 0xffffn);
  return {
    tickSpacing: Number((word >> 16n) & 0xffffffn),
    hookBitmap,
    callbacks: Object.entries(HOOK_OFFSETS)
      .filter(([, offset]) => (hookBitmap >> offset) & 1)
      .map(([name]) => name),
  };
}

encodeParameters(60); // 0x…003c0000
decodeParameters("0x00000000000000000000000000000000000000000000000000000000003c0000");
// { tickSpacing: 60, hookBitmap: 0, callbacks: [] }

Pool ID

The pool ID is keccak256(abi.encode(key)): the six fields as six 32-byte words, 192 bytes, hashed. CLPoolManager stores every pool under this ID, takes it in getSlot0, getLiquidity and the other reads, emits it as the indexed id of Initialize, ModifyLiquidity, Swap and Donate, and maps it back to the key with poolIdToPoolKey(id).

pool-id.ts
import { encodeAbiParameters, keccak256, type Address, type Hex } from "viem";

export type PoolKey = {
  currency0: Address;
  currency1: Address;
  hooks: Address;
  poolManager: Address;
  fee: number;
  parameters: Hex;
};

const poolKeyAbi = [
  {
    type: "tuple",
    components: [
      { name: "currency0", type: "address" },
      { name: "currency1", type: "address" },
      { name: "hooks", type: "address" },
      { name: "poolManager", type: "address" },
      { name: "fee", type: "uint24" },
      { name: "parameters", type: "bytes32" },
    ],
  },
] as const;

// PoolId = keccak256(abi.encode(PoolKey)): six 32-byte words, hashed.
export const getPoolId = (key: PoolKey): Hex => keccak256(encodeAbiParameters(poolKeyAbi, [key]));

// currency0 is the numerically lower address; native ETH (address 0) always sorts first.
export const sortCurrencies = (a: Address, b: Address): [Address, Address] =>
  BigInt(a) < BigInt(b) ? [a, b] : [b, a];

const [currency0, currency1] = sortCurrencies(
  "0xC7aA29e2FcB71c6396650F8Cc76dDA20714A5814", // USDC
  "0x1f6e6ffb0fC879CEC28830531C6Ec710F6110A7E", // BTC
);
console.log(
  getPoolId({
    currency0,
    currency1,
    hooks: "0x0000000000000000000000000000000000000000",
    poolManager: "0x1B24bDE0c2b7fd77450A48C4f4b5db0eA4AD6bDc",
    fee: 2011,
    parameters: "0x00000000000000000000000000000000000000000000000000000000003c0000",
  }),
); // 0xacbcd92a14745494c9d3302c3622b784d985c860202643d1f644b6348832b491 (BTC/USDC 0.30%)

Fee Tiers on This Deployment

A swap pays two fees, both in pips:

  • LP fee — the key's fee, paid to liquidity providers.
  • Protocol fee — set per pool by CLProtocolFeeController when the pool is initialized, as a share of the total swap fee (protocolFeeSplitRatio, currently 33%, capped at 0.4%). CLPoolManager stores it as a uint24 packing two 12-bit values: bits 0–11 for 0→1 swaps, bits 12–23 for 1→0. The controller's owner can change it later.

The protocol fee is taken first and the LP fee applies to the rest, so the total is protocolFee + lpFee − protocolFee × lpFee / 1_000_000, not the plain sum. The seeded pools use three tiers:

  • 0.01% — tick spacing 1, parameters 0x…00010000, LP fee 67, protocol fee 32 (packed 131104), total 99 pips.
  • 0.05% — tick spacing 10, parameters 0x…000a0000, LP fee 335, protocol fee 164 (packed 671908), total 499 pips.
  • 0.30% — tick spacing 60, parameters 0x…003c0000, LP fee 2011, protocol fee 989 (packed 4051933), total 2999 pips.

To create a pool at another tier, take the LP fee for a target total from CLProtocolFeeController.getLPFeeFromTotalFee(totalFee). Pool creation is permissionless, so other combinations may exist on chain; the app lists only these three. Read a pool's actual fees from getSlot0, which returns the current protocolFee and lpFee.

Finding Pools

CLPoolManager emits one Initialize event per pool, with the ID and both currencies indexed and the rest of the key in the data. Scan it from the deployment's start block, 33156261, to rebuild the full key of every pool. The poolManager field is not in the event; it is the address that emitted it.

The public RPC serves eth_getLogs over at most 10,000 blocks per call, so the scan walks the range in chunks. To find one pair, filter on the indexed currency0 and currency1 (args in viem) instead of scanning everything.

readPool reads a pool's live state: getSlot0 for the price (sqrtPriceX96, Q64.96), current tick and fees, and getLiquidity(id) for the liquidity in range at the current tick. A pool that was never initialized reads as all zeros. For the tick-level state, use getPoolTickInfo and CLTickLens.getPopulatedTicksInWord.

find-pools.ts
import { createPublicClient, http, parseAbi, type Address, type Hex } from "viem";
import { giwaSepolia } from "viem/chains";

const publicClient = createPublicClient({ chain: giwaSepolia, transport: http() });

const CL_POOL_MANAGER = "0x1B24bDE0c2b7fd77450A48C4f4b5db0eA4AD6bDc";
const START_BLOCK = 33_156_261n; // the deployment's first block
const MAX_RANGE = 10_000n; // the public RPC rejects wider eth_getLogs ranges

const poolManagerAbi = parseAbi([
  "event Initialize(bytes32 indexed id, address indexed currency0, address indexed currency1, address hooks, uint24 fee, bytes32 parameters, uint160 sqrtPriceX96, int24 tick)",
  "function getSlot0(bytes32 id) view returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee)",
  "function getLiquidity(bytes32 id) view returns (uint128 liquidity)",
]);

type Pool = {
  id: Hex;
  key: {
    currency0: Address;
    currency1: Address;
    hooks: Address;
    poolManager: Address;
    fee: number;
    parameters: Hex;
  };
};

// Every pool ever initialized, oldest first, from CLPoolManager's Initialize events.
export async function findPools(): Promise<Pool[]> {
  const latest = await publicClient.getBlockNumber();
  const pools: Pool[] = [];
  for (let fromBlock = START_BLOCK; fromBlock <= latest; fromBlock += MAX_RANGE) {
    const end = fromBlock + MAX_RANGE - 1n;
    const logs = await publicClient.getLogs({
      address: CL_POOL_MANAGER,
      event: poolManagerAbi[0],
      fromBlock,
      toBlock: end < latest ? end : latest,
      strict: true,
    });
    for (const { args } of logs) {
      pools.push({
        id: args.id,
        key: {
          currency0: args.currency0,
          currency1: args.currency1,
          hooks: args.hooks,
          poolManager: CL_POOL_MANAGER,
          fee: args.fee,
          parameters: args.parameters,
        },
      });
    }
  }
  return pools;
}

// A pool's live price, fees and in-range liquidity.
export async function readPool(id: Hex) {
  const [[sqrtPriceX96, tick, protocolFee, lpFee], liquidity] = await Promise.all([
    publicClient.readContract({
      address: CL_POOL_MANAGER,
      abi: poolManagerAbi,
      functionName: "getSlot0",
      args: [id],
    }),
    publicClient.readContract({
      address: CL_POOL_MANAGER,
      abi: poolManagerAbi,
      functionName: "getLiquidity",
      args: [id],
    }),
  ]);
  return { sqrtPriceX96, tick, protocolFee, lpFee, liquidity };
}

Seeded Pools

The pools the deployment created and seeded with liquidity, from its manifest. Each pairs a test token with USDC, is hookless, and has currency0 = the test token and currency1 = USDC.

  • BTC/USDC 0.01% fee 67 · tick spacing 1

    0xadcd53a3f0073e0ba22616164984977bdfed3f54b0b8725f3cfc0dcf1f93a116

  • BTC/USDC 0.05% fee 335 · tick spacing 10

    0x55a43cc4160fc5d86a96757d13d3609460e7e5a3459c342cae9999d4364e965f

  • BTC/USDC 0.30% fee 2011 · tick spacing 60

    0xacbcd92a14745494c9d3302c3622b784d985c860202643d1f644b6348832b491

  • AAPL/USDC 0.30% fee 2011 · tick spacing 60

    0x26fd02bf143e669903b4c459f13f3e10417a08713b8c8d944f7becd3f2b1b1db

  • SPCX/USDC 0.30% fee 2011 · tick spacing 60

    0xe7f088000fe753330605e620d04198c29028ccb067041b24b26104dd7a2c6a7e

  • SKHY/USDC 0.30% fee 2011 · tick spacing 60

    0x13260cdead351c67beda47265ad9cf9d87dabb9fb9d95657f5ee5278deb8f3e9

These are a subset of what findPools returns: anyone can initialize further pools through CLPositionManager or CLPoolManager.

Next: Positions for minting and managing liquidity in these pools, Math for ticks and prices, and Concentrated Liquidity for the model.

Previous
Contracts