deploy-uni-hook
GitHub用于根据简要描述生成、模拟审计并部署Uniswap v4 Hook及测试池。支持预审计模板或自由构建,默认干跑并在通过安全门禁后广播至指定链,确保资金与操作安全。
Trigger Scenarios
Install
npx skills add aeonfun/aeon --skill deploy-uni-hook -g -y
SKILL.md
Frontmatter
{
"name": "deploy-uni-hook",
"metadata": {
"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"
],
"title": "Deploy Uni Hook",
"category": "crypto",
"requires": [
"HOOK_DEPLOYER_PRIVATE_KEY?",
"ALCHEMY_API_KEY?",
"ETHERSCAN_API_KEY?"
],
"capabilities": [
"onchain_writes",
"writes_external_host",
"sends_notifications"
]
},
"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."
}
${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) orfreeform(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 inchains.tsv(run./hook-deploy.sh chainsto list). Defaultbase-sepolia. Testnets:base-sepolia,unichain-sepolia,arbitrum-sepolia. Mainnets (testnet: false, e.g.base,ethereum,unichain,arbitrum,optimism,polygon,bnb,avalanche, ...) require BOTHarm:and an explicitchain:— the skill never targets mainnet by default.base-mainnetis accepted as an alias forbase.
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: falsechain unless${var}has BOTHarm:AND an explicitchain:<mainnet-name>— AND the instance hasHOOK_MAINNET_OK=1set as a repo variable (a third, operator-level lock enforced insidehook-deploy.sh, exit 7; store it as a variable, not a secret - a secret value of1masks every1in 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 withcast balanceand abort (DEPLOY_HOOK_UNDERFUNDED) if it cannot cover the simulation'sEstimated amount required;hook-deploy.shindependently enforces a funding floor (exit 8), an optionalMAX_GAS_GWEIgas-price ceiling (exit 9), and warns if the deployer holds more thanHOOK_MAX_FLOAT_ETH(default 0.25) — a deploy key must hold gas float only, never LP or treasury capital. Log a clearMAINNETwarning 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 witharm:. - 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 logsALREADY_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), plusDeployHook.s.sol,MockERC20.sol,foundry.toml,chains.tsv. - Chain config:
skills/deploy-uni-hook/templates/chains.tsvis the single source of truth — TAB-separatedname chainId testnet poolManager stateView rpc explorer alchemy, one row per Uniswap v4 chain (staged next tohook-deploy.sh, which reads it).memory/uni-deployments.mdmirrors it for humans. To add a chain, append a row tochains.tsv. - Authenticated RPC: the
rpccolumn is a public endpoint. WhenALCHEMY_API_KEYis set and the row has analchemyslug,hook-deploy.shuseshttps://<slug>.g.alchemy.com/v2/$ALCHEMY_API_KEYinstead — 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 > publicrpc. 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 thearm:flag, the optionaltemplate:, the optionalchain:, and the free-text brief. Empty brief → exitDEPLOY_HOOK_EMPTYwith the grammar. -
Resolve the chain. The chain name resolves in
chains.tsv(defaultbase-sepolia);hook-deploy.shmaps it to the officialPoolManager+ RPC, so you pass the NAME, not the address. Run./hook-deploy.sh chainsto see the list, or readchains.tsv. If the name is not in the registry, exitDEPLOY_HOOK_BAD_CHAIN. Look up the row'stestnetcolumn: if it isfalse(mainnet), enforce the double opt-in — require BOTHarm:and an explicitchain:in${var}, else exitDEPLOY_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.shcopied to the repo root. Do not install Foundry or clone the libs in-run — the sandbox blocks that. Checkcommand -v forgeand that$HOOKBUILD_DIRexists; if either is missing, degrade toDEPLOY_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. - Freeform mode (anything else): write the whole hook into
$HOOKBUILD_DIR/src/Hook.sol— replace the// --- AEON:BODY ... ---region. Rules: keep the contract nameHookandconstructor(IPoolManager); implement whichever v4 callbacks the prompt needs, each with the EXACTIHookssignature,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, setHOOK_RETURNS_DELTAin$HOOKBUILD_DIR/hook.env; for a fee-override hook setHOOK_POOL_FEE=dynamicthere.- Also write the behavioral test. In
$HOOKBUILD_DIR/test/Hook.t.sol, replace the// --- AEON:ASSERT ... ---region withtest_*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'sWrappedErrorfor you — do NOT use barevm.expectRevert, it won't match the wrapper); a swap it must ALLOW as a plain_swap(...); any getter/accounting asassertEq(hook.someGetter(...), expected). Do NOT editsetUp()or the helpers — only theAEON:ASSERTregion. If the brief has no rejectable behavior, still assert the observable state the hook changes.
- Also write the behavioral test. In
- Template mode (
-
Simulate + audit (always). Pass mode, kind, and chain (chain omitted =
base-sepolia):./hook-deploy.sh simulate <kind> <chain>For
freeformthis runs, in order, three gates before any deploy:- Static audit — derives the flags from the callbacks; checks the contract is named
Hook, has ≥1 callback, every callback carriesonlyPoolManager,test/Hook.t.solhas ≥1test_function, and scans for dangerous patterns (selfdestruct/delegatecallare hard fails;tx.origin/raw value-call/inlineassemblyprint a warning to review). A failure exitsDEPLOY_HOOK_AUDIT_FAILED(never deploy). - Behavioral test —
forge test --fork-url <chain> --match-contract HookBehaviorTestruns the agent-written assertions on a fork. A failing OR non-compiling test exitsDEPLOY_HOOK_TEST_FAILED(never deploy). This proves the hook does what the prompt asked. - Fork simulation —
forge scriptcompiles, 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, exitDEPLOY_HOOK_SIM_FAILED. Capture the mined hook address, the derived flags, and theEstimated amount required. On mainnet, compare that estimate to the deployer balance (cast balance <addr> --rpc-url <rpc>) and exitDEPLOY_HOOK_UNDERFUNDEDif it will not cover it. For a freeform hook, also read the generatedHook.soland 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.
- Static audit — derives the flags from the callbacks; checks the contract is named
-
Dry-run stop. If
${var}did NOT start witharm:, STOP here. Report: template, mined address (with its flag bits), the pool key, and the simulation result. ExitDEPLOY_HOOK_DRY_RUN. -
Arm checks (only if
arm:).- Confirm
HOOK_DEPLOYER_PRIVATE_KEYis set (it is injected viarequires:). If not, degrade to the dry-run report and exitDEPLOY_HOOK_NO_KEY. - Read
memory/state/hook-deploys.json. If the same(chain, template, brief)deployed in the last hour, exitDEPLOY_HOOK_IDEMPOTENTwith the prior address.
- Confirm
-
Broadcast.
./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_KEYis 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 printedALREADY_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. -
Verify. With
cast, read the pool back throughStateView.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. -
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.shor./chains.tsv(runtime copies; both gitignored). -
Notify + exit. Send a short notification (template, address, explorer link, dry-run vs live). Exit
DEPLOY_HOOK_OK(orDEPLOY_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 forgefails or$HOOKBUILD_DIRabsent) → 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.shexit 7) →DEPLOY_HOOK_MAINNET_NOT_AUTHORIZED(never broadcast). - Mainnet balance below the simulation estimate, or the deployer is unfunded (
hook-deploy.shexit 8) →DEPLOY_HOOK_UNDERFUNDED(never broadcast). - Gas price above
MAX_GAS_GWEI(hook-deploy.shexit 9) →DEPLOY_HOOK_GAS_TOO_HIGH(never broadcast; retry when fees drop). - Freeform static audit fails (bad name / no callback / missing
onlyPoolManager/ notest_/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.soland its behavioral test intotest/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-writtenforge testbehavioral 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.tsvcarries 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 thePoolManager/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
MockERC20tokens 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 aboveHOOK_MAX_FLOAT_ETH); mainnet also needs theHOOK_MAINNET_OK=1operator lock. A future version can add the keyless Base MCPsend_callsrail so no key sits in the runner. - Authenticated RPC + receipt + verify. On mainnet the runner prefers an Alchemy endpoint (
ALCHEMY_API_KEY+ the chain'salchemyslug) 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, withETHERSCAN_API_KEYon 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.
Version History
-
c648040
Current 2026-08-27 12:27
修复 Windows Connect OAuth URL 截断、Setup Token 超时、配置行折叠导致解析错误、Foundry 安装静默失败及主网标志变量掩码问题。
- fc05537 2026-08-05 22:13


