The hook lifecycle
A Uniswap v4 hook has a stricter life than an ordinary contract. Its address encodes its permissions, its pools choose it at birth and can never change it, and it only ever runs inside somebody else’s transaction. Here is that lifecycle end to end.
The five stages
- Mine an address. v4 reads permissions from the low bits of the hook address, so the address has to be ground out before deployment.
- Deploy to it. Through CREATE2, with the salt that produces that exact address.
- Initialise pools that name it. A pool commits to its hook at
initializeand never again. - Accrue. Every swap through such a pool calls
afterSwapand leaves a share behind. - Sweep. The accrued value is pushed into the factory as payout liquidity.
1. The address is the permission set
Uniswap v4 does not ask a hook which callbacks it wants. It reads them from the low 14 bits of the hook’s own address. A contract deployed to the wrong address is not a misconfigured hook, it is a hook that v4 silently never calls.
Sitowise needs two flags: afterSwap, and afterSwapReturnDelta because taking value from the pool means returning a non-zero delta. Together they are 0x44.
uint160(address(hook)) & 0x3FFF == 0x44
AFTER_SWAP_FLAG = 1 << 6 // 0x40
AFTER_SWAP_RETURNS_DELTA_FLAG = 1 << 2 // 0x04The constructor calls Hooks.validateHookPermissions against its own address, so a deployment to a wrong address reverts instead of quietly producing a dead hook. That check costs one deployment and removes an entire class of silent failure.
2. Deploying to a mined address
Only CREATE2 gives a predictable address, so the deploy script grinds salts locally until the resulting address carries the right low bits, then deploys through the standard deterministic factory at 0x4e59b44847b379578588920cA78FbF26c0B4956C and asserts that the deployed address equals the mined one.
export FACTORY_ADDRESS=0x... # the SitowiseFactory
export OWNER_ADDRESS=0x...
export HOOK_SHARE_BPS=25 # optional, cap 500
forge script script/DeployHook.s.sol:DeployHook \
--rpc-url $RPC_URL --private-key $DEPLOYER_PRIVATE_KEY --broadcastPOOL_MANAGER defaults to the Robinhood Chain v4 PoolManager, which is verified on the explorer under that name. The v4 PositionManager at 0x58daec3116aae6D93017bAAea7749052E8a04fA7 reports the same address from poolManager(), which is how it was confirmed rather than assumed.
| Contract | Address on Robinhood Chain |
|---|---|
| Uniswap v4 PoolManager | 0x8366a39CC670B4001A1121B8F6A443A643e40951 |
| Uniswap v4 PositionManager | 0x58daec3116aae6D93017bAAea7749052E8a04fA7 |
| CREATE2 deterministic factory | 0x4e59b44847b379578588920cA78FbF26c0B4956C |
3. Pools choose their hook once
PoolKey, which is fixed when the pool is initialised. There is no function to attach a hook to a pool afterwards, and none can exist: changing the key would make it a different pool. The only way Sitowise earns from swap flow is for pools to be created with PoolKey.hooks set to the hook address, and for traders to use them.This is the single fact that decides whether the protocol is swap funded or not, which is why it appears on this page, on How accrual works, in the contract’s own comments, and on Risks. Until those pools exist and carry volume, rewards are funded by Sitowise.
4. Accrual, per swap
Once a pool is live on the hook, every swap through it runs the same short path: measure the unspecified side, compute the share, take it from the manager, add it to the per-currency cumulative total, emit SwapAccrued, return the delta. It adds a few thousand gas to a swap and cannot revert the swap for any reason other than a genuine accounting failure.
5. Sweeping into the factory
Accrued native value would sit on the hook until somebody swept it. sweepNative() is permissionless and always sends to the factory through fund(), so the value lands as unattached contract balance and is booked with a Funded event. A sweep credits nothing by itself: balances only ever move through creditBatch, which is a separate, payable call. See Settlement.
# permissionless, destination fixed in code
cast send $HOOK "sweepNative()" --private-key $ANY_KEY
# owner only, for ERC-20 accruals the factory cannot hold
cast send $HOOK "sweepToken(address)" $TOKEN --private-key $OWNER_KEYAfter the sweep, accrued is unchanged: it is a lifetime total, not a balance. The difference between it and the hook’s current balance is what has already been swept.
What the hook owner can change
| Call | Effect | Bound |
|---|---|---|
setShareBps | Changes the share taken per swap | Cannot exceed MAX_SHARE_BPS, which is a constant |
setFactory | Changes where sweepNative sends value | Cannot be the zero address |
setSweepRecipient | Changes where token accruals are sent | Cannot be the zero address |
There is no owner call that pulls native value out of the hook to an arbitrary address. Native value has exactly one exit, and it leads to the factory. The equivalent analysis for the factory is on Security model.