# Deploy Uni Hook

> 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. Every deployed hook inherits a mandatory 10 bps AeonFee protocol fee.

- Skill: `aaronjmars/deploy-uni-hook` (Agent Skill, multi-file: 13 files)
- Install (CLI): `npx skillmds@latest add aaronjmars/deploy-uni-hook`
- Raw SKILL.md: https://api.skillmd.com/api/skills/aaronjmars/deploy-uni-hook/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: aaronjmars (https://skillmd.com/u/aaronjmars)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/aaronjmars/deploy-uni-hook

---


> **${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/` - `AeonFee.sol` (the mandatory 10 bps protocol-fee base every hook inherits), `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` |

## Mandatory AeonFee (every hook)

Every hook this skill deploys inherits `AeonFee` (`templates/AeonFee.sol`): a MANDATORY 10 bps (0.10%) protocol fee taken in `afterSwap` on the swap's unspecified (output) currency and routed to `AEON_FEE_RECIPIENT` (`0xF1E958db7D1e4C074377946018Ad645db4FB158e`). The rate and recipient are compile-time constants and `afterSwap` is NOT virtual, so no hook can lower, skip, or redirect it. A hook adds its own post-swap logic through `_afterSwapExtra` (which runs AFTER the fee), and any hook fee it charges stacks ON TOP of the 10 bps.

Because the fee is a return-delta `take()`, EVERY aeon hook's address carries `AFTER_SWAP + AFTER_SWAP_RETURNS_DELTA` (`0x44`) - so **no aeon hook is Uniswap Labs auto-routable**; every one needs the allowlist / a UniswapX filler (see Labs routing below). This is the deliberate trade for a fee that can never be bypassed.

## 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. Because AeonFee makes every hook an `afterSwapReturnsDelta` take, **no template auto-routes** - all are allowlist.

| Template | Flags | Labs classic router |
|---|---|---|
| `noop` | `0xC4` (beforeSwap + AeonFee `0x44`) | allowlist (`afterSwapReturnsDelta`) |
| freeform default (`_afterSwapExtra`) | `0x44` (AeonFee) | allowlist (`afterSwapReturnsDelta`) |
| `dynamic` | `0x10C4` (`0x10C0` + AeonFee `0x04`) + `DYNAMIC_FEE_FLAG` | allowlist (`dynamicFees` + `afterSwapReturnsDelta`) |
| `skim` | `0x44` (AeonFee + own skim, same bits) | allowlist (`afterSwapReturnsDelta`) |

**Game on a hook** (freeform): the 10 bps fee already runs in the base `afterSwap` via `take()` - a freeform body must NOT implement `afterSwap` (put extra logic in `_afterSwapExtra`).
1. The mandatory fee is always taken; a hook's own extra fee also goes in `_afterSwapExtra` and stacks on top.
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. The miner skips `0x91...` addresses.

## 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).
- 6. A "balance" / "skew" / "heavier-side" gate on the two virtual reserves is a raw-PRICE gate in disguise. `StateLibrary` gives `amount0 = L*2^96/sqrtP` and `amount1 = L*sqrtP/2^96`, so `amount0/amount1 = 1/price` and the liquidity `L` cancels exactly. Any test on the two reserves (`b0 >= b1`, a skew band, "which side is heavier") therefore reduces to comparing the pool's RAW price to an implicit `1.0`, and raw price is `token1/token0` in smallest units, so it is ~1.0 only for a same-decimals pair near parity. A USDC(6d)/WETH(18d) pool sits ~8 orders of magnitude off; two 18-dec tokens at price 2.0 are already outside a 10% band. Such a gate is permanently one-directional on every real pair: one whole leg reverts forever, and "every trade rebalances toward 50/50" is false (a full-range position is already 50/50 by value at any price). Fix: snapshot the pool's OWN reference (its `sqrtPriceX96` at `afterInitialize`, or an explicit target ratio from the brief) and gate the current price against THAT, never a hard-coded 1.0. This needs the `afterInitialize` callback (adds flag bit `0x1000`), so a price/balance/skew hook must include it in the callback set or it cannot know its own starting price.
- 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()`.
- A price / balance / skew gate MUST be asserted at a price away from 1:1. The scaffold's `setUp()` pool starts at 1:1 (`sqrtPriceX96 = 2^96`), the single price where a raw-price-vs-1.0 gate looks correct no matter how it is written. Call `_freshPoolAt(<non-1:1 sqrtPriceX96>)` (helper in `Hook.t.sol`) and assert BOTH legs there: the leg that must stay open is not rejected, the leg that must close reverts. A gate proven only at 1:1 is a false pass.



## 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`, replacing the `// --- AEON:BODY ... ---` region. Rules: keep the contract as `contract Hook is AeonFee` and the constructor as `constructor(IPoolManager _pm) AeonFee(_pm)`. Do NOT implement `afterSwap`, `poolManager`, `onlyPoolManager`, or `NotPoolManager` - they come from `AeonFee` and the mandatory 10 bps fee is taken automatically (the audit rejects a Hook that is not `is AeonFee`, and rejects a redeclared `afterSwap`). For post-swap logic override `_afterSwapExtra` (return 0, or an additional delta the hook itself `take`s). Implement any OTHER 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 your callbacks (plus the always-on AeonFee `afterSwap`/`afterSwapReturnsDelta` bits). If another 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**: empty `hookData` must succeed; a game must not revert a vanilla exact-in swap; an extra `take()` (in `_afterSwapExtra`) must charge magnitude (exact-in and exact-out) and never custody; a price/balance/skew gate must add the `afterInitialize` callback and anchor to the pool's own start price (Gates rule 6), never an implicit 1.0.
     - **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)`. For a gate whose decision depends on price or reserve balance, assert it through `_freshPoolAt(<non-1:1 sqrtPriceX96>)` (both legs, off parity) - `setUp()`'s pool is at 1:1, where such a gate always looks correct. 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`, is `is AeonFee` (mandatory fee) and does not redeclare `afterSwap`, has ≥1 callback or `_afterSwapExtra`, 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. **Publish to the public hook list (live mainnet deploys only).** If this was a real broadcast (not a dry-run) to a mainnet chain the registry supports (`ethereum base robinhood monad bnb arbitrum unichain`), list the hook on `aeonfun/univ4-hooks` so it appears on the aeon hook marketplace. Reuse the `submit-hook` skill's helper - it decodes the flags from the address, formats the entry, regenerates the registry, and opens a PR (issue fallback when there is no push access):
    ```bash
    python3 skills/submit-hook/submit-univ4.py \
      --address "$HOOK_ADDR" --chain "$CHAIN" \
      --name "$NAME" --category "$CATEGORY" --klass "$KLASS" \
      --template "$TEMPLATE" --stage deployed --source aeon --verified \
      --date "$(date -u +%F)" \
      --mechanic "$MECHANIC" --plain "$PLAIN" --rule "$RULE1" --rule "$RULE2"
    ```
    Derive `name`/`category`/`klass`/`mechanic`/`plain`/`rules` from the brief exactly as `skills/submit-hook/SKILL.md` describes. This is best-effort: a submission failure must NEVER fail a completed deploy - log it and continue. Skip entirely on a dry-run, a testnet, or an unsupported chain.

12. **Notify + exit.** Send a short notification (template, address, explorer link, `routing` class, dry-run vs live, and the hook-list PR URL when one was opened). 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. All inherit `AeonFee`, so all carry the return-delta bits and are allowlist (`dynamic` = 0x10C4; `noop` = 0xC4; `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.

