Generate, simulate, audit, and deploy a Uniswap v4 hook + test pool from a brief, on any Uniswap v4 chain (every testnet and mainnet) - pre-audited templates or a from-scratch freeform hook (flags auto-derived; static audit + dangerous-pattern scan + a behavioral forge test + fork sim gate the deploy). Dry-run by default; explicit arm: to broadcast; testnet default, mainnet behind a double opt-in; records the deploy to main.
Scanned 9/3/2026
Install to Claude Code
npx -y skills add aeonfun/aeon --skill deploy-uni-hook --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Deploy Uni Hook?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/aeonfun-deploy-uni-hook)More formats (shields.io, HTML) on the badges page.
---
name: deploy-uni-hook
description: "Generate, simulate, audit, and deploy a Uniswap v4 hook + test pool from a brief, on any Uniswap v4 chain (every testnet and mainnet) - pre-audited templates or a from-scratch freeform hook (flags auto-derived; static audit + dangerous-pattern scan + a behavioral forge test + fork sim gate the deploy). Dry-run by default; explicit arm: to broadcast; testnet default, mainnet behind a double opt-in; records the deploy to main."
metadata:
title: Deploy Uni Hook
category: crypto
var: "arm: to broadcast (default is a dry-run), template:dynamic|noop|skim to force a mode, chain:<name> to pick a chain (default base-sepolia), then the hook brief. Empty prints the grammar."
tags:
- crypto
- dev
- onchain
requires:
- HOOK_DEPLOYER_PRIVATE_KEY?
- ALCHEMY_API_KEY?
- ETHERSCAN_API_KEY?
capabilities:
- onchain_writes
- writes_external_host
- sends_notifications
---
> **${var}** — the hook brief. Grammar: `[arm:][template:<name>] [chain:<name>] <brief>`
> - `` (empty) → print help and exit `DEPLOY_HOOK_EMPTY`.
> - `<brief>` → **dry-run**: generate, compile, mine, and simulate. Never broadcasts. *[default — no prefix]*
> - `arm:<brief>` → **broadcast**: do the full dry-run first, then deploy for real if the simulation passes.
> - `template:<name>` → force a mode: `dynamic` | `noop` | `skim` (pre-audited templates) or `freeform` (build a whole hook from the prompt). Omit to auto-pick: a brief that matches a template uses it; anything else → `freeform`.
> - `chain:<name>` → any Uniswap v4 chain in `chains.tsv` (run `./hook-deploy.sh chains` to list). Default `base-sepolia`. Testnets: `base-sepolia`, `unichain-sepolia`, `arbitrum-sepolia`. Mainnets (`testnet: false`, e.g. `base`, `ethereum`, `unichain`, `arbitrum`, `optimism`, `polygon`, `bnb`, `avalanche`, ...) require BOTH `arm:` and an explicit `chain:` — the skill never targets mainnet by default. `base-mainnet` is accepted as an alias for `base`.
Today is ${today}. This skill turns a one-line brief into a live Uniswap v4 hook. It is built to be safe: it simulates every deploy before it broadcasts, it defaults to a dry-run on testnet, and it needs an explicit `arm:` to move on-chain.
## Why this design
A hook binding is immutable and a bad hook can brick a pool or steal funds. So the gates sit BEFORE the deploy: two of them (`dry-run` then `arm:`), a mandatory simulation, and idempotent state. Everything after the broadcast is just recording what already happened — appended to `memory/state/hook-deploys.json` on `main`, no PR (there is nothing left to review). The Foundry flow is the proven one — mine a CREATE2 salt so the address carries the right hook-flag bits, deploy, initialize the pool, add liquidity, run one swap.
## Safety contract (do not skip)
1. **Mainnet needs a triple lock.** Never target a `testnet: false` chain unless `${var}` has BOTH `arm:` AND an explicit `chain:<mainnet-name>` — AND the instance has `HOOK_MAINNET_OK=1` set as a **repo variable** (a third, operator-level lock enforced inside `hook-deploy.sh`, exit 7; store it as a variable, not a secret - a secret value of `1` masks every `1` in the run log, so tx hashes and links print as `***`). An instance that never authorized mainnet cannot broadcast there even if an armed message asks it to. This skill must only run on an instance whose inbound path is owner-gated (`TELEGRAM_ALLOWED_USER_ID` / the multi-channel allowlist) — a mainnet deploy spends real gas, so an untrusted sender must never be able to dispatch it. On a mainnet chain, first read the deployer balance with `cast balance` and abort (`DEPLOY_HOOK_UNDERFUNDED`) if it cannot cover the simulation's `Estimated amount required`; `hook-deploy.sh` independently enforces a funding floor (exit 8), an optional `MAX_GAS_GWEI` gas-price ceiling (exit 9), and warns if the deployer holds more than `HOOK_MAX_FLOAT_ETH` (default 0.25) — a deploy key must hold gas float only, never LP or treasury capital. Log a clear `MAINNET` warning in the output.
2. **Simulate before every broadcast.** If the simulation reverts, do not broadcast. Report the revert and exit `DEPLOY_HOOK_SIM_FAILED`.
3. **Dry-run is the default.** Broadcast only when `${var}` starts with `arm:`.
4. **Key hygiene.** The deployer key is a burner. Never print it. Never put it on a shell command line — always go through `./hook-deploy.sh`, which reads it from the env inside the script.
5. **Idempotency.** Before broadcasting, read `memory/state/hook-deploys.json`. If an identical brief already deployed within the last hour, do not re-deploy. The deploy script is also idempotent at the address level: it deploys to the *canonical* address (the first flag-matching CREATE2 salt for this exact `(creationCode, flags, PoolManager)`). If that address already holds code, an identical hook is already live, so the script logs `ALREADY_DEPLOYED <addr>` and does nothing — the runner reports the existing address instead of deploying a duplicate. (HookMiner itself skips occupied addresses, so without this check a re-run would silently deploy another copy at a new address.)
## Inputs and config
- **Templates:** `skills/deploy-uni-hook/templates/` — `DynamicFeeHook.sol`, `NoOpHook.sol`, `HookFeeHook.sol` (pre-audited), `Hook.sol` + `Hook.t.sol` + `hook.env.example` (freeform scaffold, behavioral-test gate, manifest), plus `DeployHook.s.sol`, `MockERC20.sol`, `foundry.toml`, `chains.tsv`.
- **Chain config:** `skills/deploy-uni-hook/templates/chains.tsv` is the single source of truth — TAB-separated `name chainId testnet poolManager stateView rpc explorer alchemy`, one row per Uniswap v4 chain (staged next to `hook-deploy.sh`, which reads it). `memory/uni-deployments.md` mirrors it for humans. To add a chain, append a row to `chains.tsv`.
- **Authenticated RPC:** the `rpc` column is a public endpoint. When `ALCHEMY_API_KEY` is set and the row has an `alchemy` slug, `hook-deploy.sh` uses `https://<slug>.g.alchemy.com/v2/$ALCHEMY_API_KEY` instead — a trusted RPC matters for the mainnet sim + broadcast (a lying public RPC can fake a clean sim). Precedence: `RPC_URL` (override, for testing) > Alchemy key + slug > public `rpc`. The RPC path (where the key lives) is never printed — logs show host only.
- **Deploy helper:** `skills/deploy-uni-hook/hook-deploy.sh` — the only sanctioned broadcast path (hides the key).
- **State:** `memory/state/hook-deploys.json` — idempotency + the deploy ledger.
### Template picker (when `template:` is not given)
| Brief mentions | Mode |
|---|---|
| fee, volatility, dynamic, surge | `dynamic` |
| skim, hook fee, take a cut, revenue | `skim` |
| "minimal" / "starter" / "empty" | `noop` |
| game, leaderboard, points, crown, loyalty | `freeform` (game rules in Labs routing) |
| anything else (novel logic the templates don't cover) | `freeform` |
## Labs routing
Uniswap Labs auto-routes a hooked pool unless the address starts with `0x91`, or the hook uses `beforeSwapReturnsDelta`, `afterSwapReturnsDelta`, or `dynamicFees`. Anything in that set needs the [allowlist form](https://www.notion.so/uniswaplabs/1aec52b2548b80f78dbef8d2f0d7183e) or a UniswapX filler. A swap `take()` cannot auto-route: it requires `afterSwapReturnsDelta`.
| Template | Flags | Labs classic router |
|---|---|---|
| `noop` | `0x80` | auto-route |
| freeform default (afterSwap, delta 0) | `0x40` | auto-route |
| `dynamic` | `0x10C0` + `DYNAMIC_FEE_FLAG` | allowlist (`dynamicFees`) |
| `skim` | `0x44` | allowlist (`afterSwapReturnsDelta`) |
**Game + fee on one hook** (freeform):
1. Fee always runs in `afterSwap` via `take()`. Set `HOOK_RETURNS_DELTA=afterSwap`. This is allowlist, never auto-route.
2. Game runs only when `hookData` names a player. Empty `hookData` (Labs Universal Router) = paid swap, no game, no revert.
3. Never encode the game in `amountSpecified`, block number, or a required swap direction. Those revert the router and collect nothing.
4. `sender` is the router, not the user. Do not key game state off `sender`.
Do not generate amount-suffix / block-echo / exact-out-only / direction-gate hooks unless the brief explicitly asks for a revert-gate. Those are not Labs-routable. The miner skips `0x91...` addresses so an otherwise auto-route hook is not accidentally gated.
## Fleet audit rules (from aeon.fun hook audits)
These are standing defects measured on the live fleet. Freeform MUST NOT recreate them. The `skim` template is already patched.
**Fee / `take()`:**
- Charge the MAGNITUDE of the unspecified delta. Exact-out makes that delta negative. `if (unspecifiedAmount <= 0) return` silently skips the fee on every exact-output swap (shared-base F1).
- Widen to `int256` before negating. `-type(int128).min` panics and bricks that swap.
- `poolManager.take(..., feeRecipient, ...)` to an immutable recipient. NEVER `address(this)`. No `withdraw()`. Custody was the CrownClash/LegacyLedger HIGH.
- An extra skim helper must not copy a `<= 0` early return (second copy of the sign guard).
**Gates** (only if the brief demands a revert-gate):
- 1a. Value that moves on its own (`block.number`): a `view` helper answered at head N is wrong at execution N+1. Target the execution block.
- 1b. Value that moves when someone swaps (price low byte): exact match + zero tolerance is a contention DoS. Need a band, or do not gate.
- 1c. Shared counter an attacker can park, not advanced on failure: griefing primitive.
- 2. A contract in the `unlock` frame can satisfy the predicate; a signed tx cannot. That binds the wrong party.
- 5. Never compare raw `amountSpecified` to a token-denominated constant. The caller picks the specified currency via exact-in vs exact-out. Use a dimensionless bound (tick move / liquidity fraction).
- Never `balanceOf(poolManager)`: that is the v4 singleton's global inventory, not this pool. Use `StateLibrary`.
- `sender` is the router. Do not treat it as the trader.
**Tests:**
- A fee hook must assert the take on exact-in AND exact-out.
- A gate needs a hookless negative control (`hooks = address(0)`).
- Do not cache `block.number` across `vm.roll` (via-ir folds it). Use `vm.getBlockNumber()`.
## Steps
1. **Parse `${var}`.** Extract the `arm:` flag, the optional `template:`, the optional `chain:`, and the free-text brief. Empty brief → exit `DEPLOY_HOOK_EMPTY` with the grammar.
2. **Resolve the chain.** The chain name resolves in `chains.tsv` (default `base-sepolia`); `hook-deploy.sh` maps it to the official `PoolManager` + RPC, so you pass the NAME, not the address. Run `./hook-deploy.sh chains` to see the list, or read `chains.tsv`. If the name is not in the registry, exit `DEPLOY_HOOK_BAD_CHAIN`. Look up the row's `testnet` column: if it is `false` (mainnet), enforce the double opt-in — require BOTH `arm:` and an explicit `chain:` in `${var}`, else exit `DEPLOY_HOOK_BAD_CHAIN`. Every Uniswap v4 chain is supported (Base, Ethereum, Unichain, Arbitrum, Optimism, Polygon, BNB, Avalanche, Robinhood, Worldchain, Ink, Soneium, Celo, X Layer + their testnets).
3. **Confirm the staged toolchain + project.** The workflow pre-stages everything before this run (`scripts/stage-deploy-uni-hook.sh`): Foundry on `$PATH`, a pre-built v4 project at `$HOOKBUILD_DIR` (default `$HOME/hookbuild`) holding all three templates + `MockERC20.sol` + `DeployHook.s.sol` + the v4 libraries, and `./hook-deploy.sh` copied to the repo root. Do **not** install Foundry or clone the libs in-run — the sandbox blocks that. Check `command -v forge` and that `$HOOKBUILD_DIR` exists; if either is missing, degrade to `DEPLOY_HOOK_NO_TOOLCHAIN` (emit the generated source + plan).
4. **Build the hook (brief-driven).**
- **Template mode** (`dynamic` / `noop` / `skim`): in `$HOOKBUILD_DIR/src/<Hook>.sol`, edit ONLY the region between `// --- AEON:LOGIC START ---` and `// --- AEON:LOGIC END ---`. Keep the callback signatures and flag set unchanged. If the default already fits the brief, leave it.
- **Freeform mode** (anything else): write the whole hook into `$HOOKBUILD_DIR/src/Hook.sol` — replace the `// --- AEON:BODY ... ---` region. Rules: keep the contract name `Hook` and `constructor(IPoolManager)`; implement whichever v4 callbacks the prompt needs, each with the EXACT `IHooks` signature, `onlyPoolManager`, and the right selector return. Do NOT hand-set flags — they are auto-derived from which callbacks you implement. If a callback returns a non-zero delta, set `HOOK_RETURNS_DELTA` in `$HOOKBUILD_DIR/hook.env`; for a fee-override hook set `HOOK_POOL_FEE=dynamic` there. Follow **Labs routing** and **Fleet audit rules** above: empty `hookData` must succeed; a game must not revert a vanilla exact-in swap; a `take()` must declare `HOOK_RETURNS_DELTA`, charge magnitude (exact-in and exact-out), and never custody.
- **Also write the behavioral test.** In `$HOOKBUILD_DIR/test/Hook.t.sol`, replace the `// --- AEON:ASSERT ... ---` region with `test_*` functions that assert the hook's SPECIFIC intended behavior — not just "does not revert". For every rule in the brief write at least one positive and one negative case: a swap the hook must REJECT as `_expectSwapRevert(zeroForOne, amount, Hook.SomeError.selector)` (this helper unwraps v4's `WrappedError` for you — do NOT use bare `vm.expectRevert`, it won't match the wrapper); a swap it must ALLOW as a plain `_swap(...)`; any getter/accounting as `assertEq(hook.someGetter(...), expected)`. Do NOT edit `setUp()` or the helpers — only the `AEON:ASSERT` region. If the brief has no rejectable behavior, still assert the observable state the hook changes.
5. **Simulate + audit (always).** Pass mode, kind, and chain (chain omitted = `base-sepolia`):
```bash
./hook-deploy.sh simulate <kind> <chain>
```
For `freeform` this runs, in order, three gates before any deploy:
1. **Static audit** — derives the flags from the callbacks; checks the contract is named `Hook`, has ≥1 callback, every callback carries `onlyPoolManager`, `test/Hook.t.sol` has ≥1 `test_` function, and scans for dangerous patterns (`selfdestruct`/`delegatecall` are hard fails; `tx.origin`/raw value-call/inline `assembly` print a warning to review). A failure exits `DEPLOY_HOOK_AUDIT_FAILED` (never deploy).
2. **Behavioral test** — `forge test --fork-url <chain> --match-contract HookBehaviorTest` runs the agent-written assertions on a fork. A failing OR non-compiling test exits `DEPLOY_HOOK_TEST_FAILED` (never deploy). This proves the hook does what the prompt asked.
3. **Fork simulation** — `forge script` compiles, mines the salt, deploys in-memory, initializes the pool, adds liquidity, and runs one swap against a fork of the target chain.
On a compile error, fix and retry (max 3). On a sim revert, exit `DEPLOY_HOOK_SIM_FAILED`. Capture the mined hook address, the derived flags, and the `Estimated amount required`. On mainnet, compare that estimate to the deployer balance (`cast balance <addr> --rpc-url <rpc>`) and exit `DEPLOY_HOOK_UNDERFUNDED` if it will not cover it.
For a freeform hook, also **read the generated `Hook.sol` and reason about safety** before arming: does any callback let a caller steal funds, brick the pool (unconditional revert), or reenter? If unsure, stop at the dry-run and report the concern.
6. **Dry-run stop.** If `${var}` did NOT start with `arm:`, STOP here. Report: template, mined address (with its flag bits), the receipt `routing` line (auto-route vs allowlist + reason), the pool key, and the simulation result. Exit `DEPLOY_HOOK_DRY_RUN`.
7. **Arm checks (only if `arm:`).**
- Confirm `HOOK_DEPLOYER_PRIVATE_KEY` is set (it is injected via `requires:`). If not, degrade to the dry-run report and exit `DEPLOY_HOOK_NO_KEY`.
- Read `memory/state/hook-deploys.json`. If the same `(chain, template, brief)` deployed in the last hour, exit `DEPLOY_HOOK_IDEMPOTENT` with the prior address.
8. **Broadcast.**
```bash
./hook-deploy.sh broadcast <kind> <chain>
```
The runner prints a **deploy receipt** (hook address, decoded flag names, explorer deep-link, tx hashes) and, when `ETHERSCAN_API_KEY` is set on an Etherscan-family chain, **auto-verifies** the hook source on the explorer (best-effort — a failed verify never fails a completed deploy). If it printed `ALREADY_DEPLOYED`, treat the reported address as the result (no new deploy). Read the hook address and the transaction hashes from the receipt or `$HOOKBUILD_DIR/broadcast/DeployHook.s.sol/<chainId>/run-latest.json`.
9. **Verify.** With `cast`, read the pool back through `StateView.getSlot0(poolId)` on the RPC. Confirm the pool exists and the hook address low bits equal the template's flags. Confirm the swap emitted the hook event.
10. **Record the deploy.** The deploy already happened on-chain — this is append-only history, not a change to review, so DO NOT open a PR or a branch. Just write the record into the working tree on `main`; the workflow's post-run commit lands it. Write:
- `memory/state/hook-deploys.json` — append this deploy (chain, template, brief, hook address, flags, tx hashes, timestamp, poolId, poolKey).
- `output/hooks/<hook-address>.sol` — copy the deployed source from `$HOOKBUILD_DIR/src/<Hook>.sol`.
- For freeform, also `output/hooks/<hook-address>.t.sol` — copy `$HOOKBUILD_DIR/test/Hook.t.sol` (the behavioral test that gated the deploy).
Do NOT stage the root `./hook-deploy.sh` or `./chains.tsv` (runtime copies; both gitignored).
11. **Notify + exit.** Send a short notification (template, address, explorer link, `routing` class, dry-run vs live). Exit `DEPLOY_HOOK_OK` (or `DEPLOY_HOOK_DRY_RUN`).
## Degrade rules
- No key → dry-run report, `DEPLOY_HOOK_NO_KEY`. Never fail hard.
- Foundry or the staged project missing (`command -v forge` fails or `$HOOKBUILD_DIR` absent) → emit the generated source + plan, `DEPLOY_HOOK_NO_TOOLCHAIN`. Do not try to install in-run (the sandbox blocks it).
- Bad/missing chain, or mainnet without the double opt-in → `DEPLOY_HOOK_BAD_CHAIN`.
- Mainnet chain but the instance did not set `HOOK_MAINNET_OK=1` (`hook-deploy.sh` exit 7) → `DEPLOY_HOOK_MAINNET_NOT_AUTHORIZED` (never broadcast).
- Mainnet balance below the simulation estimate, or the deployer is unfunded (`hook-deploy.sh` exit 8) → `DEPLOY_HOOK_UNDERFUNDED` (never broadcast).
- Gas price above `MAX_GAS_GWEI` (`hook-deploy.sh` exit 9) → `DEPLOY_HOOK_GAS_TOO_HIGH` (never broadcast; retry when fees drop).
- Freeform static audit fails (bad name / no callback / missing `onlyPoolManager` / no `test_` / `selfdestruct` / `delegatecall`) → `DEPLOY_HOOK_AUDIT_FAILED` (never deploy).
- Freeform behavioral test fails or does not compile → `DEPLOY_HOOK_TEST_FAILED` (never deploy).
- Simulation revert → `DEPLOY_HOOK_SIM_FAILED` (never broadcast after a failed sim).
## Notes
- The three templates are pre-validated: each compiles and simulates a full deploy + swap on Base Sepolia (`dynamic` = 0x10C0 flags, allowlist; `noop` = 0x80, auto-route; `skim` = 0x44, allowlist).
- **Freeform** builds an arbitrary hook from the prompt into `src/Hook.sol` and its behavioral test into `test/Hook.t.sol`. Flags are auto-derived from the callbacks (never hand-set). Three gates run before any deploy: a static audit (name/callbacks/`onlyPoolManager`/test-present/dangerous-pattern scan), the agent-written `forge test` behavioral assertions on a fork, then the fork simulation. The agent also reads the generated source for steal/brick/reentrancy risk before arming. Prefer a matching template when one fits (they are audited); use freeform for novel logic.
- Every deploy — template or freeform — always simulates on the target chain's fork first, so "does it work" is checked before any broadcast.
- **Any Uniswap v4 chain works.** `chains.tsv` carries every official v4 deployment (Base, Ethereum, Unichain, Arbitrum, Optimism, Polygon, BNB, Avalanche, Robinhood, Worldchain, Ink, Soneium, Celo, X Layer + the Sepolia testnets), each verified to hold the PoolManager. The same flow runs on all of them — only the `PoolManager`/RPC differ, resolved by name. The CREATE2 deployer (`0x4e59…4956C`) is required for the mined address; if a chain lacks it the fork simulation fails closed before any broadcast.
- **Mainnet is gas-only.** The deploy mints its own `MockERC20` tokens to itself (free) and seeds the demo pool with those mock tokens — a mainnet broadcast risks GAS ONLY, never real capital. The deployed pool is a MockA/MockB demo; the reusable hook contract is the real deliverable. The deployer key must be a funded burner holding gas float only (the runner warns above `HOOK_MAX_FLOAT_ETH`); mainnet also needs the `HOOK_MAINNET_OK=1` operator lock. A future version can add the keyless Base MCP `send_calls` rail so no key sits in the runner.
- **Authenticated RPC + receipt + verify.** On mainnet the runner prefers an Alchemy endpoint (`ALCHEMY_API_KEY` + the chain's `alchemy` slug) over the public RPC, so a lying public node can't fake a clean sim. After a broadcast it prints a receipt (address, decoded flags, explorer link, tx hashes) and, with `ETHERSCAN_API_KEY` on an Etherscan-family chain, auto-verifies the source (best-effort). All of this is opt-in: with no keys set the skill still runs on public RPCs, unverified.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!