ZkPadHook
Source: contracts/src/hooks/ZkPadHook.sol, ZkPadHookBase.sol, CLBaseHook.sol ·
Interface: IZkPadHook · Port of Clanker v4 ClankerHookV2 + ClankerHookStaticFeeV2 to
PancakeSwap Infinity CL. GPL-2.0-or-later.
Purpose
For every zk-pad pool the hook:
- lets only the factory create pools (the hook calls
initializeitself, which Infinity does not call back into; every other initializer hitsbeforeInitializeand reverts); - lets only the pool's locker add liquidity, and not while the MEV module runs;
- on each swap: records the block's pre-swap tick range, pays out earlier swaps' protocol fees,
auto-collects LP fees through the locker, picks the fee (static per direction, raised by the
MEV module while it runs) and returns the LP fee as the
lpFeeOverride:lpFeeFor(F) = floor(0.75·F)on sells and the slightly higherbuyLpFeeFor(F, pcs)on buys (below); - charges the protocol fee in the paired token in the same four cases as Clanker (exact in/out × buy/sell), sized at 1/3 of the LP fee the positions actually receive, so protocol : beneficiary = 25 : 75;
- on sells, donates 75% of the part of the protocol fee above the LP fee's post-swap value back
to the positions (
ProtocolFeeRebated) and books the sell's reference value (lpFeeValueAccrued) for the locker's protocol top-up; - exempts the locker's own fee-conversion swaps, and records the LP fees fee-paying swaps earned
(
lpFeesAccrued), which caps what that exempt conversion may sell; - tells the locker whether a conversion may run now (
conversionAllowed): only at a price at least as good for it as every pre-swap price of the block.
Permissions
beforeInitialize, beforeAddLiquidity, beforeSwap, afterSwap, beforeSwapReturnDelta,
afterSwapReturnDelta. Pools use fee = DYNAMIC_FEE_FLAG (0x800000), tick spacing 200, and the
permission bitmap in PoolKey.parameters.
Pool configuration
struct PoolStaticConfigVars {
uint24 tokenFee; // total fee when the launched token is the input (sells), [1%, 5%]
uint24 pairedFee; // total fee when the paired token is the input (buys), [1%, 5%]
}
Protocol-fee math
The pool charges swapFee = pcs + lp − pcs·lp/1e6 of its gross input, of which
lpReceived = swapFee − pcs accrues to the LPs (PancakeSwap's own protocol fee pcs, read from
slot0 per direction, is on top of F).
- Buys (paired in): the protocol fee is
lpReceived / 3of the pool's paired input and comes off the trader's input first, so the buy LP fee is raised tobuyLpFeeFor(F, pcs) = ceil(0.75·F / (1 − 0.25·F·(1 − pcs))). Protocol + LP is then exactlyF·(1 − pcs)of the trader's gross input, 25:75. - Sells (token in): the LP fee is in the token. At the execution price it is worth
lpReceived / (1 − swapFee)of the paired output, and the protocol fee is a third of that (exact-in from the output inafterSwap, exact-out up front inbeforeSwap). The part above the LP fee's value at the post-swap price is split 25:75 again and 75% of it is donated to the in-range liquidity, which is only ever the locker's positions. A sell that ends with no in-range liquidity has no post-swap reference and rebates nothing. A seller therefore always pays the execution value; where the price ends only moves part of it between protocol and beneficiary, and the locker's top-up undoes that when the fees convert higher.
Details and tests: contracts/PORTING.md H-2, H-3, H-8 and L-9.
Functions
| Function | Access | Description |
|---|---|---|
initializePool(token, pairedToken, tickIfToken0IsToken, tickSpacing, locker, mevModule, poolData) returns (PoolKey) | factory | Create and initialize the pool |
initializeMevModule(PoolKey, bytes mevModuleData) | factory | Arm the MEV module after liquidity and dev buy |
mevModuleSetFee(PoolKey, uint24 fee) | pool's MEV module, during beforeSwap | Raise this swap's total fee (ignored unless higher than normal and ≤ MAX_MEV_FEE) |
mevModuleOperational(PoolId) returns (bool) | anyone | Whether the module still runs; expires it if older than MAX_MEV_MODULE_DELAY |
lowerFees(PoolKey, newTokenFee, newPairedFee) | token admin | Lower the per-direction fees; never raise, never below MIN_FEE |
claimProtocolFees(Currency) | anyone | Pay held protocol fees to the factory (normally automatic on the next swap) |
lpFeeFor(uint24 fee) pure | Sell-side LP fee (75%, rounded down) for a total fee | |
buyLpFeeFor(uint24 fee, uint16 pcsFee) pure | Buy-side LP fee for a total fee and PancakeSwap's buy-direction fee | |
lpFeesAccrued(PoolId) view returns (tokenFees, pairedFees) | Cumulative LP fees fee-paying swaps credited to the positions (wrapping counters); caps the locker's fee-exempt conversion | |
lpFeeValueAccrued(PoolId) view | Cumulative paired reference value of the token-side LP fees (for the protocol top-up) | |
conversionAllowed(PoolId, bool zeroForOne) view | Whether a fee conversion in that direction may run now (same-block price guard) | |
tokenFee(PoolId), pairedFee(PoolId), tokenIsToken0, locker, mevModule, mevModuleEnabled, poolCreationTimestamp | view | Per-pool state |
factory, MIN_FEE, MAX_FEE, MAX_MEV_FEE, MAX_MEV_MODULE_DELAY, PROTOCOL_SHARE_BPS | view | Constants |
Events
| Event | Meaning |
|---|---|
PoolCreatedFactory(pairedToken, token, poolId, tickIfToken0IsToken, tickSpacing, locker, mevModule) | Pool created |
PoolInitialized(poolId, tokenFee, pairedFee) | Static fee config |
FeesLowered(poolId, oldTokenFee, oldPairedFee, newTokenFee, newPairedFee) | Token admin lowered fees |
MevModuleDisabled(poolId) | Module disabled: when the module reports its decay is over, or when mevModuleOperational finds it older than MAX_MEV_MODULE_DELAY |
MevModuleSetFee(poolId, fee) | Module raised a swap's fee |
ClaimProtocolFees(token, amount) | Protocol fees paid to the factory |
ProtocolFeeClaimFailed(token, reason) | Payout failed; trading continues |
ProtocolFeeRebated(poolId, amount) | A sell's rebate (in the paired token) donated back to the positions |
LpLockerFeeClaimFailed(poolId, reason) | LP fee collection failed; trading continues |
Errors
ETHPoolNotAllowed, OnlyFactory, OnlyThis, OnlyLocker, UnsupportedInitializePath,
MevModuleEnabled, Unauthorized, NotInSwap, TokenFeeOutOfBounds(fee),
PairedFeeOutOfBounds(fee), FeeNotLowered, UnknownPool.
Notes
- Native-currency (address 0) pools are rejected (
ETHPoolNotAllowed); BNB pairs use WBNB. - Nothing depends on
hookData, so any Infinity router can swap. - Failed fee payouts or collections emit an event instead of reverting, so a frozen recipient can never block trading.