Uniswap v4入門|PoolManager・hooks・flash accountingを実装検証
Uniswap v4のsingleton PoolManager、PoolKey、hook permission、unlock callback、delta settlement、native ETH、dynamic feeをFoundryで検証します。

Uniswap v4のhookを実装したいが、v3までのRouterとPoolを前提にすると、どこでtokenを支払い、いつ残高が確定するのか分からない。hook addressの末尾bitは合っているのにswapがrevertする。callback途中のdeltaを最終残高と読み違える。このような開発者・integrator・reviewer向けに、PoolManager、PoolKey、hook permission、flash accountingを一つのlocal検証で確認します。
結論は、PoolKeyを毎回同じ5項目で特定し、unlock → callback → pool操作 → delta確認 → sync / settle / take → deltaゼロを一つのtransactionとして追うことです。hook addressのpermission bitは呼ばれる関数を示すだけで、そのコードが安全、正確、または有利であることを保証しません。
この記事はnpm公開版の@uniswap/v4-core 1.0.2と@uniswap/v4-periphery 1.0.3、Solidity 0.8.26、Cancun EVM、Foundry 1.8.0を固定したlocal contractだけで検証します。外部RPC、mainnet fork、wallet接続、署名、公開transaction、実資産のswapやLP depositは行いません。
v2・v3のPoolごとのcontractからsingletonへ
Uniswap v2ではpairごと、v3ではpoolごとにcontractがあり、Routerなどのperipheryがそれらを呼びます。v4では、複数poolのstateとtoken残高を一つのPoolManagerが管理します。
| 世代 | pool stateの主な置き場所 | 複数hopの中間transfer | 拡張点 |
|---|---|---|---|
| v2 | pairごとのcontract | pair間でtokenを移す | pair / Routerの固定ロジック |
| v3 | poolごとのcontract | pool間でtokenを移す | concentrated liquidity、periphery |
| v4 | singleton PoolManager |
中間はdeltaへ集約し、最後に精算できる | poolごとのhook |
singletonは「poolが一つになった」という意味ではありません。一つのcontract内に、異なるPoolIdを持つ多数のpool stateがあります。integratorはPoolManagerへ直接曖昧なtoken pairを渡すのではなく、完全なPoolKeyを渡します。
PoolKeyの5項目がpoolを特定する
固定したcore sourceのPoolKeyは次の5項目です。
struct PoolKey {
Currency currency0;
Currency currency1;
uint24 fee;
int24 tickSpacing;
IHooks hooks;
}
| 項目 | 確認すること | 間違えた場合 |
|---|---|---|
currency0 |
addressの数値順で小さいcurrency | 順序が逆ならinitializeがrevert |
currency1 |
addressの数値順で大きいcurrency | 別tokenなら別pool |
fee |
static feeまたはdynamic fee flag | 値が違えば別pool |
tickSpacing |
tickの許容間隔 | 値が違えば別pool |
hooks |
0 addressまたはpool固有hook | addressが違えば別pool |
PoolIdは、この構造体をencodeしたhashです。PoolManagerはPoolKey自体をstorageへ保存しないため、呼び出し側が毎回正しいkeyを組み立てます。今回の検証ではtick spacingだけを変えたkeyでswapし、初期化済みpoolとは別IDになるためPoolNotInitializedでrevertすることを確認します。
native ETHはaddress(0)のCurrencyです。address順では必ず先頭になるため、ETH/token pairではETHがcurrency0になります。
hookはpool lifecycleへ差し込む外部contract
各poolは、初期化時に0または1つのhook addressを固定します。一つのhook contractを複数poolが共有することはできます。
PoolManagerはhook addressの下位14 bitを見て、どのcallbackを呼ぶか判断します。
| bit | hex | callbackまたは能力 |
|---|---|---|
| 13 | 0x2000 |
beforeInitialize |
| 12 | 0x1000 |
afterInitialize |
| 11 | 0x0800 |
beforeAddLiquidity |
| 10 | 0x0400 |
afterAddLiquidity |
| 9 | 0x0200 |
beforeRemoveLiquidity |
| 8 | 0x0100 |
afterRemoveLiquidity |
| 7 | 0x0080 |
beforeSwap |
| 6 | 0x0040 |
afterSwap |
| 5 | 0x0020 |
beforeDonate |
| 4 | 0x0010 |
afterDonate |
| 3 | 0x0008 |
beforeSwapがdeltaを返す |
| 2 | 0x0004 |
afterSwapがdeltaを返す |
| 1 | 0x0002 |
afterAddLiquidityがdeltaを返す |
| 0 | 0x0001 |
afterRemoveLiquidityがdeltaを返す |
return-delta bitは、対応するcallback bitと組み合わせる必要があります。たとえばbeforeSwapとafterSwapを使う単純なcounter hookなら、下位maskは0x00c0です。
permission bitは安全性の証明ではない
permission bitが合っていることから分かるのは、PoolManagerがそのcallbackを呼ぶという一点です。
- callback内のaccess controlが正しいか
- external callやoracle readが操作されないか
- return deltaとfeeが意図どおりか
- callbackが正しいselectorと長さを返すか
- state changeが別poolや別利用者へ影響しないか
- upgrade、admin、pause、withdraw権限があるか
これらはcode、constructor、storage、権限、動作検証、auditを別々に確認します。意図的にrevertするhookは正しいbeforeSwap bitを持つためpoolを初期化できますが、callbackがrevertするためswap全体は失敗します。bitが正しいことと、動作が安全であることは別です。
hook内で外部価格を読む場合は、return値だけでなくfeed identity、updatedAt、L2 sequencer、TWAP windowを分けます。DeFi Oracleのdecimals・staleness・TWAP検証にconsumer側の停止条件をまとめています。
unlock callbackとflash accounting
swap、modifyLiquidity、donateなどの残高を変える操作は、原則としてPoolManagerをunlockした範囲で行います。poolのinitializeはbalanceを変えないため、unlock外で実行できます。
integration / router
→ PoolManager.unlock(data)
→ PoolManagerが呼出元のunlockCallback(data)を呼ぶ
→ swap / modifyLiquidityなどを実行する
→ caller・currencyごとのdeltaを累積する
→ 負deltaを支払い、正deltaを受け取る
→ 全deltaが0ならlockしてreturnする
callbackは外部公開関数です。次の検査がなければ、第三者が任意dataで直接呼び出せます。
if (msg.sender != address(poolManager)) revert UnauthorizedCallback();
local検証ではcallbackを直接呼び、PoolManager以外ならrevertすることを確認します。また、unlock中に再度unlockするとAlreadyUnlockedです。callback入口の認証と、hook自身のonlyPoolManagerは別の境界として確認します。
BalanceDeltaの符号をcaller視点で読む
PoolManagerはunlock中、(caller, currency)ごとに差額を累積します。
| delta | caller視点 | 終了までに行うこと |
|---|---|---|
| 負 | PoolManagerへ支払う債務 | sync、token支払い、settle |
| 正 | PoolManagerから受け取る債権 | take、ERC-6909化、または限定的なclear |
| 0 | 精算済み | 追加操作なし |
swapを2回行えば、1回目の出力を2回目の入力へdelta上で相殺できます。途中tokenをpool間でtransferせず、最後のnet balanceだけを実際に動かすのがflash accountingです。
callbackがreturnした時点でnon-zero deltaが一つでも残れば、PoolManagerはCurrencyNotSettledでtransaction全体をrevertします。tokenを一時的にtakeすることはできますが、unlock終了までに反対側の操作や支払いで相殺しなければなりません。
ERC-20とnative ETHのsettlement
固定したperipheryのDeltaResolverに合わせ、今回のrouterはcurrencyごとに次の順序で支払います。
| currency | 負deltaの精算 | 正deltaの受取 |
|---|---|---|
| ERC-20 | sync(currency) → transferFrom(payer, PoolManager, amount) → settle() |
take(currency, recipient, amount) |
| native ETH | periphery 1.0.3ではsync(address(0)) → settle{value: amount}() |
take(address(0), recipient, amount) |
ERC-20のsyncは、transfer前のPoolManager残高をtransient storageへcheckpointします。settle()は、その後に実際に増えた残高をdeltaへ反映します。transferしてからsyncすると増加前の値を失うため、順番を入れ替えません。
native ETHはmsg.valueから支払額を決めます。coreのIPoolManager上はERC-20残高のcheckpointが不要なのでsyncを省略できますが、固定したperiphery 1.0.3のDeltaResolverはnativeでも先にsyncします。今回の検証実装はperiphery準拠pathとcore上許される省略pathを別関数で再現し、どちらもvalue付きsettleとtakeの後にdeltaが0になることを確認します。余ったmsg.valueはcallback終了後にpayerへ返します。
clearは受取ではなく放棄
clear(currency, amount)は正deltaをtoken transferなしで0へします。amountは現在の正deltaと完全一致しなければrevertします。
clearした分は利用者へ届かず、PoolManager内から回収できなくなります。今回の検証では正delta 9,969をexact amountでclearし、recipient残高が増えないことを確認します。これは危険境界を示す専用の確認であり、通常pathは正deltaをtakeします。通常の受取をclearへ置き換えず、放棄してよいdustと完全一致するamountに限定します。
static fee・dynamic fee・hook独自収支を分ける
v4では「fee」という語が複数の層に現れます。
| 種類 | どこで決まるか | 変更の境界 |
|---|---|---|
| static LP fee | PoolKey.fee |
feeを変えると別PoolKey・別pool |
| dynamic LP fee | PoolKey.fee = 0x800000 |
hookだけがstored feeを更新できる |
| per-swap override | dynamic poolのbeforeSwap return |
`0x400000 |
| protocol fee | PoolManagerのprotocol設定 | hook feeやLP feeとは別 |
| hook独自収支 | hookのreturn deltaやcustom accounting | hook codeとpermissionを個別review |
LP feeの単位は1 basis pointの100分の1です。3000は0.30%、500は0.05%です。最大は1_000_000、つまり100%です。
local検証のdynamic fee hookは、初期stored fee 0のpoolを次のswapのbeforeSwapで4,000(0.40%)へ更新します。これはstored fee更新の検証であり、0x400000 | feeを返すper-swap overrideの検証ではありません。
dynamicであること自体はfeeの妥当性を保証しません。fee入力のsource、更新者、更新頻度、操作可能性、上限、failure時の挙動を確認します。
local環境で再現する正常系と失敗系
同一の実物PoolManagerへERC-20/ERC-20 poolとnative ETH/ERC-20 poolを初期化し、三つのhookを分けて確認します。
| case | 確認する証拠 | 期待結果 |
|---|---|---|
| simple hook | permission mask、before/after count、swap delta | beforeとafterが各1回、最終delta 0 |
| dynamic fee hook | dynamic flag、beforeSwap、stored fee | 0から0.40%へ更新、最終delta 0 |
| revert hook | 正しいbeforeSwap bit、revert selector | pool初期化は成功、swap全体はrevert |
| ERC-20 settlement | callback中の負delta、sync、transfer、settle | PoolManager残高が増え、delta 0 |
| native settlement | periphery syncあり / core sync省略、value付きsettle | 両pathでETHとtokenを精算し、delta 0 |
| clear | swap出力の正delta、recipient残高 | exact deltaだけ0、recipientは受け取らない |
| 未清算 | callback return直前のnon-zero delta | CurrencyNotSettledで全体revert |
| 不正callback | callbackの直接呼出元 | UnauthorizedCallbackでrevert |
| wrong PoolKey | tick spacingだけを変更 | PoolNotInitializedでrevert |
実行traceは次の順序を確認します。
PoolManager.unlock
→ SettlementRouter.unlockCallback
→ PoolManager.swap
→ ERC-20ならPoolManager.sync → token transferFrom
→ ERC-20はsettle、native ETHはperiphery pathでsync → value付きsettle
→ 正deltaをPoolManager.take
→ non-zero delta count = 0
固定したローカル検証のERC-20 exact-inputでは、swap直後のdeltaが(-10,000, +9,969)、精算後が(0, 0)でした。native ETHでは入力上限0.01 ETHに対し6,035,841,794,200,769 weiをsettleし、token側の正delta5,981,737,760,509,662をtakeして、未使用valueを返しました。精算を省いたcaseはcallback終了直前に(-10,000, +9,969)が残り、CurrencyNotSettledで全体がrevertします。数値、検証項目、source pin、制約は公開検証JSONへ固定しています。
revertしたtransactionのeventとstate changeは残りません。失敗caseは、残ったeventを証拠にせず、期待したerror selectorとrevert後の残高・counter不変を証拠にします。
固定した検証環境
再現に使ったversionと12個の検証項目に加え、ERC-20、native ETHの2経路、clear、未清算deltaの詳細trace要約を公開検証JSONへ記録しています。
| component | 固定値 |
|---|---|
@uniswap/v4-core |
npm 1.0.2 / gitHead 59d3ecf53afa9264a16bba0e38f4c5d2231f80bc |
@uniswap/v4-periphery |
npm 1.0.3 / gitHead 60cd93803ac2b7fa65fd6cd351fd5fd4cc8c9db5 |
| Foundry / Forge | 1.8.0 / commit 61ae26af36320d4fa1020f7db53785885e29eeb5 |
| Solidity / EVM | 0.8.26 / Cancun |
| optimizer | via IR、runs 200 |
v4-coreの公式releaseは確認時点でv4.0.0、tag commitはe50237c43811bd9b526eff40f26772152a42dabaです。検証ではrelease tagを曖昧に追従せず、npm packageのversion・integrity・gitHeadを固定します。v4-peripheryは確認時点でGitHub releaseがないため、同様にnpm packageとgitHeadを正本にします。
transient storageはtransactionが終わるまで共有される
v4 coreはunlock状態、currency delta、non-zero delta count、syncしたreserveなどにEIP-1153のTSTORE / TLOADを使います。
transient storageはtransaction終了時に破棄され、revertにも追従します。しかし「callbackがreturnした瞬間に自動で消えるstorage」ではありません。同じtransaction内のnested callやreentrancyからも、そのtransactionに属する状態が見えます。
したがって、flash accountingで最終deltaが0になることだけではhookの安全性を証明できません。外部callの前後、callback sender、pool key、hook state、return delta、reentrancy guard、資産flowを合わせてreviewします。transient storage単体の挙動はTSTORE・TLOADのlocal検証で確認できます。
実装・review checklist
Poolとhook
currency0 < currency1か- fee、tick spacing、hookを含む完全なPoolKeyを固定したか
- hook addressの下位14 bitと実装callbackが一致するか
- permission bitを安全性評価へ流用していないか
- hookがPoolManager以外からのcallbackを拒否するか
- dynamic feeのsource、上限、更新条件を確認したか
callbackと精算
unlockCallbackがPoolManagerだけを許可するか- operation直後にcurrency別deltaを記録したか
- ERC-20はsyncより後にtransferしているか
- 負deltaはsettle、正deltaはtakeしているか
- clearするamountと放棄理由を明示したか
- callback終了前に全currency deltaが0か
利用者向け表示
- quote時のPoolKey、fee、hook、blockを表示または記録できるか
- callback途中のdeltaをwallet最終残高として見せていないか
- minimum output、deadline、route、price impactを実行条件へ結びつけたか
- receiptと最終token balanceをtransaction後に読み直すか
LPの価格変動と集中流動性の前提はインパーマネントロスの計算、quoteと実行差はPrice impact・slippage・MEVの違いで確認できます。vaultのasset/share会計とpoolのcurrency deltaは別モデルなので、ERC-4626のshare計算と混同しないでください。
まとめ
Uniswap v4は、hookを追加できるだけのv3ではありません。複数poolを持つsingleton PoolManager、5項目のPoolKey、transaction中のdelta、unlock callback、最後のsettlementが一つの設計です。
調査時は、PoolKey → hook permissionとcode → callback sender → operation delta → sync / settle / take → deltaゼロ → receiptと最終残高の順に対応させます。permission bit、dynamic fee、動作検証の通過のどれか一つを、hookやpool全体の安全性評価へ置き換えないことが重要です。
この記事には特定poolへのswap、LP deposit、token購入を促す導線はありません。将来developer toolやaudit serviceを紹介する場合も、広告表記とlocal検証結果を分離します。
確認した一次情報
- Uniswap v4 protocol overview確認日: 2026/08/31
- Uniswap v4 architecture確認日: 2026/08/31
- Uniswap v4 PoolManager確認日: 2026/08/31
- Uniswap v4 hooks確認日: 2026/08/31
- Uniswap v4 flash accounting確認日: 2026/08/31
- Uniswap v4 dynamic fees確認日: 2026/08/31
- Uniswap v4-core PoolManager pinned source確認日: 2026/08/31
- Uniswap v4-core Hooks pinned source確認日: 2026/08/31
- Uniswap v4-core PoolKey pinned source確認日: 2026/08/31
- Uniswap v4-periphery BaseHook 1.0.3 pinned source確認日: 2026/08/31
- Uniswap v4-periphery DeltaResolver 1.0.3 pinned source確認日: 2026/08/31
- EIP-1153 transient storage確認日: 2026/08/31
- Foundry v1.8.0確認日: 2026/08/31



