| 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)
- 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.
- Simulate before every broadcast. If the simulation reverts, do not broadcast. Report the revert and exit
DEPLOY_HOOK_SIM_FAILED.
- Dry-run is the default. Broadcast only when
${var} starts with arm:.
- 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.
- 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
-
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.
-
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).
-
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).
-
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.
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.