Uniswap v4 Hooks, Flash Accounting, MEV
도입
Uniswap v4 hook은 AMM을 더 유연하게 만든다. pool lifecycle의 특정 지점에 외부 contract가 붙어 dynamic fee, custom accounting, liquidity rule, oracle update 같은 동작을 넣을 수 있다. 하지만 확장성이 커진 만큼 audit surface도 커진다.
hook을 "새 기능을 붙이는 플러그인"으로만 보면 위험하다. hook은 swap 전후, liquidity 변경 전후 같은 민감한 지점에서 실행된다. 잘못된 hook은 가격 계산, fee, reserve accounting, 사용자 minOut을 흔들 수 있다. flash accounting도 같은 transaction 안의 net settlement를 전제로 하므로 invariant가 끝에서 맞아야 한다.
학습 목표
- hook이 pool lifecycle을 확장하는 지점과 위험을 설명한다.
- flash accounting을 "공짜 자금"이 아니라 transaction-level net settlement로 이해한다.
- MEV, JIT liquidity, dynamic fee hook을 release gate로 검토한다.
개념 설명
beforeSwap price guard
완화 장치를 정의한다.
afterSwap accounting drift
완화 장치를 정의한다.
JIT liquidity capture
완화 장치를 정의한다.
routing exclusion
완화 장치를 정의한다.
| Hook 지점 | 가능한 기능 | 주요 위험 | 검증 증거 |
|---|---|---|---|
| beforeSwap | dynamic fee, allowlist | quote 변경과 minOut 훼손 | fee bounds |
| afterSwap | rebate, custom accounting | balance drift | net settlement invariant |
| beforeAddLiquidity | KYC, vault rule | LP censorship | policy event |
| afterRemoveLiquidity | fee accounting | withdrawal mismatch | position diff |
코드로 확인하기
export function boundedDynamicFee({ volatilityBps, depthUsd, baseFeeBps}: { volatilityBps: number; depthUsd: number; baseFeeBps: number;}) { const volatilityPremium = Math.min(50, Math.floor(volatilityBps / 20)); const depthPenalty = depthUsd < 1_000_000 ? 30 : depthUsd < 5_000_000 ? 10 : 0; return Math.min(100, Math.max(baseFeeBps, baseFeeBps + volatilityPremium + depthPenalty));}// SPDX-License-Identifier: MITpragma solidity ^0.8.24;contract HookReleaseGate { uint16 public constant MAX_FEE_BPS = 100; mapping(address => bool) public approvedHook; function assertHook(address hook, uint16 feeBps) external view { require(approvedHook[hook], "hook not approved"); require(feeBps <= MAX_FEE_BPS, "fee too high"); }}두 예시는 hook 자체 구현이 아니라 release guard다. 교육의 핵심은 "hook을 만들 수 있다"보다 "어떤 hook을 pool에 붙여도 되는가"다. dynamic fee는 volatility와 depth를 반영할 수 있지만, 상한과 event logging이 없으면 사용자 quote를 예측하기 어렵게 만든다.
강의 포인트
| 관점 | 확인할 질문 | 증거로 남길 것 |
|---|---|---|
| Lifecycle | hook은 어느 지점에서 실행되는가 | before/after 함수 목록 |
| Permission | 어떤 pool에 붙을 수 있는가 | hook allowlist |
| Accounting | transaction 끝의 net balance가 맞는가 | invariant test |
| Routing | 사용자가 실제로 접근 가능한가 | frontend route support |
실무 예시
컨트랙트[OPS] 어떤 팀이 volatility가 높을 때 fee를 올리는 hook pool을 출시하려 한다. 제품 문구는 "LP 보호"라고 말하지만, 사용자는 swap 직전에 fee가 바뀌는 경험을 할 수 있다. 따라서 quote에는 current fee, max fee, fee update reason이 보여야 한다. backend는 hook address allowlist와 fee bounds를 관리해야 한다.
MEV 측면에서는 JIT liquidity도 봐야 한다. 특정 bot이 큰 swap 직전에 liquidity를 넣고 직후 제거하면 일반 LP의 fee capture가 낮아질 수 있다. hook이 이를 막을 수도 있지만, 잘못된 hook은 더 큰 privilege surface가 된다.
흔한 오해와 실패 시나리오
| 오해 | 실패 시나리오 | 교정 방식 |
|---|---|---|
| hook은 앱 플러그인이다 | swap accounting을 직접 흔든다 | lifecycle별 invariant를 둔다 |
| dynamic fee는 항상 LP에게 좋다 | 사용자의 quote 예측 가능성을 낮춘다 | max fee와 사유를 노출한다 |
| flash accounting은 flash loan이다 | transaction 끝 net settlement가 핵심인데 중간 balance를 오해한다 | final balance invariant를 테스트한다 |
| hook pool은 자동으로 route된다 | frontend나 aggregator가 배제할 수 있다 | route support를 출시 조건에 넣는다 |
실습 과제
- Hook 위험 매트릭스 만들기: beforeSwap, afterSwap, addLiquidity, removeLiquidity hook별 권한과 실패 상태를 표로 작성한다.
- Dynamic fee guard 작성하기: volatilityBps와 poolDepthUsd를 받아 허용 가능한 feeBps를 제한하는 함수를 작성한다.
완료 기준
- beforeSwap, afterSwap, liquidity hook 중 최소 3개 지점의 위험을 설명했다.
- hook allowlist, fee bounds, invariant check를 포함한 release gate를 작성했다.
근거 자료
- 03 DEX and AMM
- Uniswap v4 Hooks
- Uniswap v4 Flash Accounting