Developers
Pools
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).
struct PoolKey {
Currency currency0;
Currency currency1;
IHooks hooks;
IPoolManager poolManager;
uint24 fee;
bytes32 parameters;
}currency0,currency1— the two tokens, sorted:currency0must be numerically lower thancurrency1, orinitializereverts withCurrenciesInitializedOutOfOrder. Native ETH is the zero address, so it is alwayscurrency0. 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.initializereverts 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 exceed1_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 throughupdateDynamicLPFee. 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
hooksset, it must equal the hook contract'sgetHooksRegistrationBitmap(); withhooksat the zero address, it must be 0. Either mismatch reverts withHookConfigValidationError. - Bits 16–39 — the tick spacing, 24 bits.
initializeaccepts 1 to 32767 (TickMath.MIN_TICK_SPACINGtoMAX_TICK_SPACING). - Bits 40–255 — unused, and must be zero.
The callback bits, from ICLHooks.sol:
0beforeInitialize ·1afterInitialize2beforeAddLiquidity ·3afterAddLiquidity4beforeRemoveLiquidity ·5afterRemoveLiquidity6beforeSwap ·7afterSwap8beforeDonate ·9afterDonate10beforeSwapReturnsDelta ·11afterSwapReturnsDelta12afterAddLiquidityReturnsDelta ·13afterRemoveLiquidityReturnsDelta
For a hookless pool the word is just tickSpacing << 16: tick spacing 60 is 0x…003c0000.
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).
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 auint24packing 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,
parameters0x…00010000, LP fee 67, protocol fee 32 (packed131104), total 99 pips. - 0.05% — tick spacing 10,
parameters0x…000a0000, LP fee 335, protocol fee 164 (packed671908), total 499 pips. - 0.30% — tick spacing 60,
parameters0x…003c0000, LP fee 2011, protocol fee 989 (packed4051933), 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.
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
- Next
- Integration