---
name: Deploy Uni Hook
slug: deploy-uni-hook
category: DevOps
description: Deploy Uni Hook generates, simulates, audits, and deploys a Uniswap v4 hook and test pool from a brief. It defaults to dry-run on testnets and requires explicit arm and chain settings for broadcasts, especially on mainnet.
github: "https://github.com/aeonfun/aeon/tree/main/skills/deploy-uni-hook"
language: TypeScript
stars: 679
forks: 242
install: "npx degit https://github.com/aeonfun/aeon/tree/main/skills/deploy-uni-hook ~/.claude/skills/deploy-uni-hook"
installs_to: ~/.claude/skills/deploy-uni-hook
source_path: skills/deploy-uni-hook/SKILL.md
collection_size: 25
category_size: 798
collection_url: "https://dirskills.com/collections/aeonfun/aeon"
added: 2026-08-24T05:16:30.456Z
last_synced: 2026-08-24T05:16:30.456Z
canonical_url: "https://dirskills.com/skills/deploy-uni-hook"
---

# Deploy Uni Hook

Deploy Uni Hook generates, simulates, audits, and deploys a Uniswap v4 hook and test pool from a brief. It defaults to dry-run on testnets and requires explicit arm and chain settings for broadcasts, especially on mainnet.

**Install:**

```bash
npx degit https://github.com/aeonfun/aeon/tree/main/skills/deploy-uni-hook ~/.claude/skills/deploy-uni-hook
```

## README

> **${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` |
| anything else (novel logic the templates don't cover) | `freeform` |

## 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.
     - **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 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, 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, `noop` = 0x80, `skim` = 0x44).
- **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.
