PolicyPool is a Uniswap v4 Hook for pool covenants on X Layer. Each pool publishes the exact-input swap size and daily volume limits its liquidity will accept before execution. Over-cap flow either refuses in beforeSwap, or goes through PolicyPool Surge, where a trusted router donates a surge fee to in-range LPs before executing the swap inside the same v4 unlock.
Built for the OKX X Layer Hook the Future hackathon.
Fast links: Live app · Judge guide · Reviewer questions · Hook invariants · Adoption path · Security notes
- Open the live app and read the first screen:
Policy bends. LPs get paid. - Click the featured Surge proof or scroll to the proof ledger.
- Verify the Surge proof: the trusted surge router donates
40 mUSDC, then executes a5,000 mUSDCswap in the same v4 unlock. - Verify the spoof-guard proof: surge-looking
hookDatathrough the old router falls back toMAX_SWAP_EXCEEDED. - Verify the V1 covenant proofs: a
5,000 mUSDCexact-input order is accepted by the loose pool, refused by the strict pool, and the strict pool later refuses the third1,000 mUSDCfill withDAILY_CAP_EXCEEDED. - Run the one-command verifier:
node scripts/verify-all.mjsThe final proof section should include:
✓ loose pool accepts 5,000 mUSDC (5000 mUSDC)
✓ strict pool refuses 5,000 mUSDC by max-swap covenant (MAX_SWAP_EXCEEDED, attempted 5000 mUSDC, limit 1000 mUSDC)
✓ strict pool accepts first 1,000 mUSDC daily-cap fill (1000 mUSDC)
✓ strict pool accepts second 1,000 mUSDC daily-cap fill (1000 mUSDC)
✓ strict pool refuses third 1,000 mUSDC by daily-cap covenant (DAILY_CAP_EXCEEDED, attempted 3000 mUSDC, limit 2000 mUSDC)
PolicyPool proof verified on X Layer.
✓ surge hook deployment and policy verified
✓ surge swap donated 40 mUSDC and executed 5,000 mUSDC in one tx
✓ untrusted router hookData falls back to V1 max-swap refusal
PolicyPool Surge proof verified on X Layer.
PolicyPool makes the pool's execution limits explicit and enforceable. A pool creator can publish a small covenant that defines the maximum exact-input swap size and the rolling daily volume the pool will accept.
This keeps the v1 primitive narrow:
- The covenant belongs to the pool, not to a trader account.
- The Hook checks the covenant before swap execution.
- Accepted swaps emit
SwapAccepted. - Refused swaps revert with
PolicyBlocked. - The optional demo router catches refusals and emits
SwapBlockedCaughtfor easier indexing.
PolicyPool keeps the first submission deliberately narrow:
- One Hook:
PolicyPoolHook.sol - One covenant schema:
maxSwapAmount,dailyCap,spentToday,lastResetTimestamp - One callback:
beforeSwap - One proof pair:
MockUSDC / MockETH - Two v4 pools using the same Hook but different fee tiers, so they have different
PoolIds - Two live proofs:
- a
5,000 mUSDCexact-input swap passes the loose pool and fails the strict pool - two
1,000 mUSDCstrict-pool swaps pass, then the third fails the daily cap
- a
Cut from v1: slippage caps, asset allowlists, per-LP policy aggregation, governance, oracle checks, Pyth, and frontend swap execution.
Standard v4 pools accept any valid swap against available liquidity. PolicyPool moves one decision into the pool itself: before the swap executes, the pool checks whether the trader's requested input fits the covenant attached to that pool.
If the swap fits:
beforeSwapreturns successfully- the Hook emits
SwapAccepted(poolId, trader, amountIn) - the v4 swap continues
If the swap breaks the covenant:
beforeSwapreverts withPolicyBlocked(reason, attempted, limit)- no
SwapAcceptedevent is emitted - the v4 swap does not execute
Reverted logs do not persist onchain, so PolicyPoolDemoRouter.swapOrRecord can catch a failed strict-pool swap and emit SwapBlockedCaught. The Hook still enforces the refusal inside beforeSwap; the router event only makes the demo/indexer proof cleaner.
Most obvious v4 Hook ideas change the price of execution: dynamic fees, rebates, points, or routing incentives. PolicyPool changes the permission boundary of execution. The pool can refuse flow before liquidity is consumed.
That distinction is the primitive:
- A dynamic-fee Hook says: "you may swap, but the price changes."
- PolicyPool says: "this pool will not consume its liquidity for that swap."
- The refusal happens inside
beforeSwap, not in an offchain router or UI. - The proof is binary and onchain: accepted swaps emit
SwapAccepted; refused swaps are caught by the demo router with the originalPolicyBlockedreason.
PolicyPool is not trying to replace permissionless pools. It creates a second pool class for liquidity providers who want bounded flow:
- small teams seeding a new X Layer asset can cap early whale flow;
- treasuries can publish max-swap and daily-volume limits instead of monitoring liquidity manually;
- market makers can run strict and loose pools side by side and expose both policies publicly;
- protocols can prove that liquidity constraints are enforced by the Hook, not by a private backend.
The v1 covenant is intentionally small: maxSwapAmount and dailyCap. More complex policy belongs in later versions only after the core Hook is proven.
For the market-potential path, see docs/ADOPTION_PATH.md.
PolicyPool Surge is the one approved v2 upgrade: policy overrides pay LPs instead of silently bypassing the covenant.
Surge keeps the daily-cap gate intact. If an exact-input swap exceeds maxSwapAmount, PolicyPoolSurgeHook accepts the override only when the v4 sender is the authorized PolicyPoolSurgeRouter and hookData carries a sufficient surge amount. The router proves payment by calling PoolManager.donate(poolKey, 40 mUSDC, 0) before PoolManager.swap(...) inside the same unlock callback.
The spoof guard is part of the proof. Passing surge-looking hookData through the old demo router is treated as no surge and falls back to the V1 MAX_SWAP_EXCEEDED refusal.
PolicyPool is deployed on X Layer mainnet because the product needs cheap, repeatable swap proofs and an active onchain trading environment. The submission does not stop at deployment: it initializes v4 pools, runs accepted swaps, records max-swap refusal, records daily-cap refusal, records Surge execution, and verifies all of it from X Layer receipts.
src/
PolicyPoolHook.sol # beforeSwap covenant enforcement
PolicyPoolDemoRouter.sol # minimal PoolManager adapter for demo liquidity/swaps
PolicyHookDeployer.sol # CREATE2 helper for valid v4 Hook address bits
PolicyPoolSurgeHook.sol # v2 trusted-router surge path
PolicyPoolSurgeRouter.sol # donates surge fee then swaps in one unlock
PolicySurgeHookDeployer.sol # CREATE2 helper for surge hook address bits
PolicyTypes.sol # Policy struct, reasons, errors
mocks/
MockERC20.sol
MockUSDC.sol
MockETH.sol
script/
DeployHook.s.sol # mines BEFORE_SWAP hook address and deploys hook
DeployDemo.s.sol # deploys hook, router, mocks, pools, policies, demo swaps
DeploySurge.s.sol # deploys surge hook/router/pool and captures surge proofs
RunDailyCapProof.s.sol # runs the live strict-pool daily-cap proof
scripts/
verify-all.mjs # one-command local + live proof verifier, also used in CI
verify-live.mjs # recording-friendly live deployment + proof verifier
verify-proof.mjs # dependency-free verifier for live X Layer proof txs
verify-surge.mjs # dependency-free verifier for live Surge proof txs
verify-deployment.mjs # dependency-free verifier for live deployment state
test/
PolicyPoolHook.t.sol # policy unit tests
PolicyPoolDemoRouter.t.sol # demo router tests
PolicyPoolIntegration.t.sol # local v4 PoolManager integration tests
PolicyPoolSurgeRouter.t.sol # surge donate + swap and spoof-guard tests
docs/
ADOPTION_PATH.md
HOOK_INVARIANTS.md
JUDGE_GUIDE.md
POLICY_SCHEMA.md
SECURITY_NOTES.md
DEPLOYMENT_PLAN.md
web/
index.html # static judge proof page shell
PolicyPoolHook implements IHooks directly instead of depending on a periphery base hook. The contract exposes every required callback and only mutates state in beforeSwap.
Callback used:
beforeSwap(address sender, PoolKey key, SwapParams params, bytes hookData)
Callback behavior:
- Require caller is the configured Uniswap v4
PoolManager. - Compute
poolId = key.toId(). - Load policy for the pool.
- Reject missing policy.
- Reject exact-output swaps.
- Convert negative
amountSpecifiedinto exact-inputamountIn. - Reject if
amountIn > maxSwapAmount. - Reset
spentTodayif the 24-hour window elapsed. - Reject if
spentToday + amountIn > dailyCap. - Increment
spentToday. - Emit
SwapAccepted. - Return
IHooks.beforeSwap.selector.
Official Uniswap v4 deployment docs list X Layer mainnet chain 196 with:
PoolManager:0x360E68faCcca8cA495c1B759Fd9EEe466db9FB32PositionManager:0xCF1Eafc6928DC385A342E7C6491D371D2871458bStateView:0x76fd297E2D437cd7f76d50F01aFE6160f86e9990Permit2:0x000000000022D473030F116dDEE9F6B43aC78BA3
Source: https://developers.uniswap.org/docs/protocols/v4/deployments
Deployed on X Layer mainnet, chain 196.
| Artifact | Address / tx |
|---|---|
PoolManager |
0x360E68faCcca8cA495c1B759Fd9EEe466db9FB32 |
PolicyHookDeployer |
0x904eEf6AFB59754114F10182d1f0564F606d4F73 |
PolicyPoolHook |
0x7D676FA819D8CDF0A2BB73B944a3533870868080 |
PolicyPoolDemoRouter |
0xCD46b2C1e6dD9d0fd3Edd9B26F0137E02F3Fc29e |
MockUSDC |
0xBb856B7ce87315eaBF1005861B1b321826a6D33c |
MockETH |
0xEA76c34E0d6d43326c9AB98088536d129242d181 |
PolicySurgeHookDeployer |
0x10B48e541bC8eD94aC0106F1CA69Ffe255479dCB |
PolicyPoolSurgeHook |
0xf44d9C1f9efF1231E53C60EDB9A73761aa99c080 |
PolicyPoolSurgeRouter |
0xd05AAD5b86f6FFCc10872803bEdb5fa911e0E1fD |
Pool IDs:
- Loose pool:
0x1f03803fe744002a219a7d74646f3e355130b4afbd073c05afd3684bc70bbbf7 - Strict pool:
0x1c32ec3d512c6807ba73c5cd32bdf2fe6c3ab07dc3e820340378c728bb5711f7 - Surge pool:
0x1a024c08b90a1c3534b790c9e6c3c128d54fc9a3703d4882398f27a2d2ac068b
Proof txs:
The V1 and Surge contracts are verified on Sourcify with exact matches. The live Surge receipts are also checked by scripts/verify-surge.mjs.
Deployment steps:
- Deploy
PolicyHookDeployer. - Mine a salt so the Hook address has only
BEFORE_SWAP_FLAGset in the lower 14 bits. - Deploy
PolicyPoolHookthroughPolicyHookDeployer. - Deploy
PolicyPoolDemoRouter. - Deploy
MockUSDCandMockETH. - Initialize two v4 pools against the same Hook:
- Loose pool: fee
3000, tick spacing60 - Strict pool: fee
10000, tick spacing200
- Loose pool: fee
- Set policies:
- Loose:
maxSwapAmount = 10,000 mUSDC,dailyCap = 50,000 mUSDC - Strict:
maxSwapAmount = 1,000 mUSDC,dailyCap = 2,000 mUSDC
- Loose:
- Add small demo liquidity to both pools.
- Run the same
5,000 mUSDCexact-input swap against both.
Run the complete local and live verification path:
node scripts/verify-all.mjsThat command runs formatting, contract build, contract tests, web build, deployment-state verification, and proof-receipt verification.
The same command runs in GitHub Actions, so the public CI check verifies both local code quality and the live X Layer proof path.
For a shorter recording-friendly proof command that skips local build output and checks only the deployed state plus live receipts:
node scripts/verify-live.mjsVerify deployed contracts, Hook address bits, Hook permissions, PoolManager binding, and pool policy values:
node scripts/verify-deployment.mjsExpected result:
✓ connected to X Layer mainnet (196)
✓ Uniswap v4 PoolManager bytecode exists at 0x360e68faccca8ca495c1b759fd9eee466db9fb32
✓ PolicyPoolHook bytecode exists at 0x7d676fa819d8cdf0a2bb73b944a3533870868080
✓ PolicyPoolDemoRouter bytecode exists at 0xcd46b2c1e6dd9d0fd3edd9b26f0137e02f3fc29e
✓ MockUSDC bytecode exists at 0xbb856b7ce87315eabf1005861b1b321826a6d33c
✓ MockETH bytecode exists at 0xea76c34e0d6d43326c9ab98088536d129242d181
✓ Hook address bits enable BEFORE_SWAP only
✓ PolicyPoolHook is bound to official X Layer PoolManager
✓ getHookPermissions returns only beforeSwap=true
✓ loose pool policy is set (10000 / 50000 mUSDC)
✓ strict pool policy is set (1000 / 2000 mUSDC)
PolicyPool deployment verified on X Layer.
The verifier does more than check that transactions exist. It fetches the X Layer receipts, decodes the accepted
SwapAccepted events, unwraps the v4 WrappedError, decodes the inner PolicyBlocked error, and asserts the exact
attempted amount and covenant limit for both refusal paths.
node scripts/verify-proof.mjsIt currently verifies:
- the loose pool accepted a
5,000 mUSDCexact-input swap; - the strict pool refused the same
5,000 mUSDCswap withMAX_SWAP_EXCEEDED; - the strict pool accepted two
1,000 mUSDCdaily-cap fills; - the strict pool refused the third fill with
DAILY_CAP_EXCEEDED.
forge build
forge test -vv
node scripts/verify-all.mjs
node scripts/verify-live.mjs
node scripts/verify-deployment.mjs
node scripts/verify-proof.mjsEnvironment:
cp .env.example .envDeploy Hook on X Layer mainnet:
POOL_MANAGER=0x360E68faCcca8cA495c1B759Fd9EEe466db9FB32 \
PRIVATE_KEY=... \
forge script script/DeployHook.s.sol:DeployHook \
--rpc-url $XLAYER_RPC_URL \
--broadcastFull demo deploy on X Layer:
POOL_MANAGER=0x360E68faCcca8cA495c1B759Fd9EEe466db9FB32 \
PRIVATE_KEY=... \
forge script script/DeployDemo.s.sol:DeployDemo \
--rpc-url $XLAYER_RPC_URL \
--broadcastCurrent tests cover:
- policy storage and owner assignment
- invalid policy rejection
- owner-only policy update
- direct
beforeSwapcall protection - missing policy rejection
- exact-output rejection
- max-swap rejection
- daily-cap rejection
- daily-cap reset after 24 hours
- exact max-swap and exact daily-cap boundary acceptance
- owner-managed policy update behavior
- Hook permission flags
- local v4 PoolManager triggering
beforeSwap - loose pool accepted swap through v4 test router
- strict pool rejected swap through v4 test router
- demo router accepted swap
- demo router caught strict-pool refusal and emitted
SwapBlockedCaught - live X Layer proof verifier for accepted, max-swap refused, and daily-cap refused outcomes
The repo and live site are structured so judges can verify PolicyPool without a recorded walkthrough:
- Open the live hero:
Policy bends. LPs get paid. - Click the featured Surge proof and inspect the
Donate, HookSwapAccepted, and routerSurgeAcceptedlogs. - Click the spoof-guard proof and inspect the old-router refusal path.
- Return to the proof ledger and inspect the V1 covenant baseline: loose accepts
5,000 mUSDC, strict refuses the same exact-input amount, and daily cap refuses the third fill. - Run
node scripts/verify-live.mjsfor the concise live verifier ornode scripts/verify-all.mjsfor the full local + live proof path.
- Hook contract: implemented
- Mock tokens: implemented
- Policy unit tests: passing
- Local v4 integration tests: passing
- Hook deploy script: implemented
- Demo deploy script: implemented
- Pool initialization / liquidity scripts: implemented in
DeployDemo.s.sol - Static frontend shell: implemented
- X Layer deployment: complete
- Proof txs: captured
- Daily-cap proof txs: captured
- Live proof verifier: passing
