Snapper is a local Uniswap v4 playground showing programmable, policy‑driven swap fees via a custom v4 Hook, a minimal unlock‑callback router, a small HTTP API, an event indexer (SSE), and a Next.js UI.
- Deploys a Uniswap v4
PoolManager, two ERC‑20 tokens, aPolicyController, and anAdaptiveFeeHookthat computes a dynamic per‑swap fee. - Uses a minimal
Swapper(unlock‑callback router) to execute exact‑input swaps throughPoolManager.unlock → swap. - Emits
PolicyUsed(feeBps, bucket)on every swap; an indexer streams these events to the UI terminal in real time.
- The Hook extends
BaseHookand enablesbeforeSwap/afterSwappermissions. _beforeSwapsets the fee in v4 fee units (hundredths of a bip, 1e‑6) derived from policy bps;_afterSwapemitsPolicyUsed.- The router follows the v4 unlock pattern and settles input/output via the
CurrencySettlerhelpers.
contracts/(Foundry)src/AdaptiveFeeHook.sol— dynamic‑fee Hook emittingPolicyUsed.src/PolicyController.sol— stores base/max/slope policy (admin‑updatable).src/Swapper.sol— minimal unlock‑callback router for exact‑in swaps.script/DeployLocal.s.sol— deploys the stack and writesout/addresses.local.json.
backend/api-server/— HTTP API (Express + viem) on port 4000.indexer/— simple event streamer (SSE) on port 4002.
src/(Next.js UI)app/— pages and layout.components/Terminal.tsx— bottom console that shows live events and results.
- Node.js (tested with modern Node 20+)
- NPM (comes with Node)
- Foundry (for anvil/forge): https://book.getfoundry.sh/
- Start a local chain
cd contracts
anvil -p 8545- Deploy contracts
cd contracts
forge script script/DeployLocal.s.sol \
--rpc-url http://127.0.0.1:8545 \
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
--broadcastThis writes contracts/out/addresses.local.json.
- Start the API and indexer
cd backend
export RPC_URL=http://127.0.0.1:8545
export DEMO_PRIVKEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
# optional, defaults to ../contracts/out/addresses.local.json relative to backend/
# export ADDRESSES_JSON=/Users/<you>/path/to/contracts/out/addresses.local.json
npm run devYou should see: API server up on port 4000 and Indexer listening on 4002.
- Start the frontend
cd .. # repo root
npm run devOpen http://localhost:3000 and connect a wallet on Localhost (31337).
- Approve TOKEN0: the UI provides a button that sends ERC‑20
approveto authorize the router. - Simulate swap: prints baseline vs dynamic fee and estimated outputs in the terminal.
- Build Tx → Send: the API returns real calldata for
Swapper.swapExactIn; your wallet submits the tx. - Update Policy: adjust fee policy (base/slope/max) and observe a different
PolicyUsedon the next swap.
Faucet (optional): fund ERC‑20 balances to your wallet via the API
curl -X POST http://localhost:4000/api/faucet \
-H 'Content-Type: application/json' \
-d '{"token":"TOKEN0","to":"<YOUR_WALLET>","amount":"1000000000000000000"}'GET /api/addresses— returns deployed addresses.GET /api/diagnostics— chainId and presence of code for the hook/swapper.GET /api/getPoolState?pair=TOKEN0-TOKEN1— reads current fee from the hook.POST /api/simulateSwap—{ amountIn, tokenIn, tokenOut, slippageBps? }→ baseline vs dynamic.POST /api/buildApproveTx—{ token, owner, amount }→ ERC‑20 approve calldata.POST /api/buildTx—{ amountIn, tokenIn, tokenOut, recipient }→to/dataforswapExactIn.POST /api/updatePolicy— demo policy bump (requiresDEMO_PRIVKEY).
- If Anvil restarts, you must redeploy and restart the backend so addresses match the live chain.
- Verify the API sees contracts:
curl http://localhost:4000/api/diagnostics(look forhasHookCode: true). - If swaps revert, ensure you approved TOKEN0 to the router and your wallet has local ETH for gas.