Uniswap V4 Hooks — Panduan untuk Developer Indonesia
1.1 Pendahuluan
1.1.1 Apa itu Uniswap V4?
Uniswap adalah DEX (Decentralized Exchange) terbesar di ekosistem Ethereum. Sejak versi pertama diluncurkan tahun 2018, Uniswap terus berevolusi:
| Versi | Tahun | Fitur Utama |
|---|---|---|
| V1 | 2018 | AMM pertama, hanya pair ETH ↔ ERC20 |
| V2 | 2020 | Pair ERC20 ↔ ERC20, Flash Swaps, TWAP Oracle |
| V3 | 2021 | Concentrated Liquidity, LP bisa pilih price range |
| V4 | 2025 | Hooks, Singleton Design, Flash Accounting, ERC-6909 |
Uniswap V4 (diluncurkan 31 Januari 2025) membawa perubahan arsitektur besar-besaran. Salah satu fitur paling revolusioner adalah Hooks — smart contract yang bisa kamu tulis untuk memodifikasi perilaku liquidity pool.
1.1.2 Apa itu Hooks?
Hooks adalah smart contract yang bisa "menempel" ke liquidity pool dan menjalankan logika custom di titik-titik penting dalam alur operasi pool. Analoginya mirip dengan React hooks (useState, useEffect) yang memungkinkan kamu "plug in" ke lifecycle sebuah komponen.
Dengan hooks, kamu bisa membuat DEX turunan di atas Uniswap tanpa perlu fork seluruh codebase. Ini membuka peluang besar untuk inovasi:
- Onchain orderbook (limit orders)
- Custom pricing curve (misalnya untuk stablecoin)
- Dynamic fees yang menyesuaikan kondisi pasar
- Proteksi dari MEV (sandwich attacks)
- Auto-compounding LP fees
- KYC'd pools untuk enterprise
- Dan masih banyak lagi!
1.1.3 Prasyarat
Panduan ini ditujukan untuk developer yang sudah memiliki:
- Pengalaman menulis smart contract dalam Solidity
- Familiar dengan Foundry (forge, cast, anvil), khususnya menulis test
- Pemahaman dasar tentang AMM dan konsep xy = k (Uniswap V2)
- Ketertarikan dalam perkembangan DeFi
1.1.4 Tujuan
Di akhir panduan ini, kamu akan mampu:
- Memahami arsitektur Uniswap V4 (Singleton, Flash Accounting, ERC-6909)
- Memahami jenis-jenis hook yang tersedia dan kapan menggunakannya
- Membangun hook sederhana (Points Hook) dari nol
- Menulis test suite untuk hook menggunakan Foundry
- Memahami konsep Dynamic Fees dan Limit Orders
- Mengenal Return Delta hooks untuk custom pricing curve
- Memahami pertimbangan keamanan saat membangun hooks
1.2 Arsitektur Uniswap V4
1.2.1 Singleton Design
Di Uniswap V3, setiap pool adalah contract terpisah yang di-deploy melalui Factory contract. Ini mahal untuk multi-hop swap karena perlu transfer token antar contract.
Uniswap V4 menggunakan Singleton Design — satu contract PoolManager mengelola semua pool.
V3: Factory → Pool A (contract sendiri)
→ Pool B (contract sendiri)
→ Pool C (contract sendiri)
V4: PoolManager (satu contract berisi semua pool)
├── Pool A (library call)
├── Pool B (library call)
└── Pool C (library call)
Pool di V4 diimplementasikan sebagai Solidity library, bukan contract terpisah:
// Pool sebagai Library
library Pool {
function initialize(State storage self, ...) { ... }
function swap(State storage self, ...) { ... }
function modifyPosition(State storage self, ...) { ... }
}
// PoolManager menggunakan library
contract PoolManager {
using Pools for *;
mapping(PoolId id => Pool.State) internal pools;
function swap(PoolId id, ...) {
pools[id].swap(...); // Library call, bukan external call
}
}
Keuntungan Singleton Design:
- Tidak perlu deploy contract baru untuk setiap pool
- Multi-hop swap lebih murah (tidak perlu transfer token antar contract)
- Memungkinkan Flash Accounting
1.2.2 Flash Accounting
Di V3, setiap swap (termasuk multi-hop) harus melakukan transfer token ERC-20 di setiap langkah. Ini mahal karena setiap transfer() adalah external call ke contract token.
Dengan Flash Accounting di V4, token hanya ditransfer di awal dan akhir transaksi. Langkah-langkah di tengah hanya menghitung "utang" (balance delta) tanpa transfer nyata.
Contoh: Swap ETH → DAI via USDC (multi-hop)
V3 (3x transfer):
1. ETH masuk ke pool ETH/USDC
2. USDC keluar dari pool ETH/USDC, masuk ke pool USDC/DAI
3. DAI keluar dari pool USDC/DAI ke user
V4 (2x transfer saja):
1. ETH masuk ke PoolManager
2. PoolManager hitung output USDC (tanpa transfer)
3. PoolManager hitung output DAI dari USDC (tanpa transfer)
4. DAI keluar ke user
1.2.3 Mekanisme Unlock (Locking)
Untuk menjamin atomicity, V4 menggunakan mekanisme unlock. Semua operasi penting (swap, modifikasi likuiditas) harus dilakukan dalam konteks "unlock":
function unlock(bytes calldata data) external returns (bytes memory result) {
if (Lock.isUnlocked()) revert AlreadyUnlocked();
Lock.unlock();
// Callback ke pemanggil untuk melakukan operasi
result = IUnlockCallback(msg.sender).unlockCallback(data);
// Pastikan semua saldo sudah diselesaikan
if (NonZeroDeltaCount.read() != 0) revert CurrencyNotSettled();
Lock.lock();
}
Alur unlock:
- Periphery contract memanggil
unlock()pada PoolManager - PoolManager membuka kunci dan memanggil
unlockCallback()pada periphery - Di dalam callback, periphery melakukan swap/modifikasi likuiditas
- Setelah callback selesai, PoolManager memastikan tidak ada saldo yang belum diselesaikan
- PoolManager mengunci dirinya kembali
1.2.4 Transient Storage (EIP-1153)
V4 menggunakan Transient Storage untuk efisiensi gas. Ini adalah lokasi penyimpanan baru di EVM (selain storage, memory, calldata) yang:
- Hanya bertahan selama satu transaksi
- Sangat murah untuk baca/tulis dibanding storage biasa
- Sempurna untuk data sementara seperti status lock dan balance delta
library Lock {
uint256 constant IS_UNLOCKED_SLOT = uint256(keccak256("Unlocked")) - 1;
function unlock() internal {
assembly { tstore(IS_UNLOCKED_SLOT, true) }
}
function lock() internal {
assembly { tstore(IS_UNLOCKED_SLOT, false) }
}
function isUnlocked() internal view returns (bool unlocked) {
assembly { unlocked := tload(IS_UNLOCKED_SLOT) }
}
}
1.2.5 ERC-6909 Claim Tokens
ERC-6909 adalah standar multi-token sederhana. Di V4, ini digunakan untuk claim tokens — representasi kepemilikan token yang disimpan di PoolManager.
Trader yang sering melakukan swap bisa memilih untuk tidak menarik token dari PoolManager. Sebagai gantinya, PoolManager menerbitkan ERC-6909 claim token yang mewakili saldo mereka. Swap berikutnya cukup burn/mint claim token tanpa transfer ERC-20 yang mahal.
Keuntungan:
- Tidak perlu external call ke contract ERC-20
- Gas cost konstan tanpa terpengaruh logika custom token (misalnya blacklist USDC)
- Cocok untuk trader high-frequency
1.3 Hooks dalam Uniswap V4
1.3.1 Jenis-jenis Hook
Hook bisa mengimplementasikan subset dari fungsi-fungsi berikut:
| Hook Function | Kapan Dipanggil |
|---|---|
beforeInitialize | Sebelum pool diinisialisasi |
afterInitialize | Setelah pool diinisialisasi |
beforeAddLiquidity | Sebelum likuiditas ditambahkan |
afterAddLiquidity | Setelah likuiditas ditambahkan |
beforeRemoveLiquidity | Sebelum likuiditas dihapus |
afterRemoveLiquidity | Setelah likuiditas dihapus |
beforeSwap | Sebelum swap dilakukan |
afterSwap | Setelah swap dilakukan |
beforeDonate | Sebelum donasi ke LP |
afterDonate | Setelah donasi ke LP |
beforeSwapReturnDelta | beforeSwap yang bisa bypass swap logic |
afterSwapReturnDelta | afterSwap yang bisa modifikasi output |
afterAddLiquidityReturnDelta | afterAddLiquidity dengan delta kustom |
afterRemoveLiquidityReturnDelta | afterRemoveLiquidity dengan delta kustom |
Donate adalah fungsi untuk langsung memberikan tip ke LP yang sedang aktif (in-range). Hook bisa memanfaatkan ini untuk mekanisme distribusi reward.
1.3.2 Hook Address Bitmap
PoolManager mengetahui fungsi hook mana yang diimplementasikan berdasarkan bit tertentu di address contract hook.
Setiap hook function punya flag yang merepresentasikan bit tertentu di address. Misalnya:
Address: 0x...0090
Binary (8 bit terakhir): 1001 0000
Bit ke-5 = 1 → AFTER_DONATE_FLAG aktif
Bit ke-8 = 1 → BEFORE_SWAP aktif
Saat deploy hook ke network sungguhan, kamu perlu mining address yang bit-bitnya sesuai dengan fungsi yang kamu implementasikan. Untuk testing lokal, Foundry punya cheat code deployCodeTo yang memungkinkan deploy ke address manapun.
Daftar lengkap flags: Hooks.sol di v4-core
1.3.3 Alur Swap dengan Hooks
1. User → Periphery (SwapRouter)
2. Periphery → PoolManager.unlock()
3. PoolManager → Periphery.unlockCallback()
4. Periphery → PoolManager.swap()
5. PoolManager → Hook.beforeSwap() (jika aktif)
6. PoolManager menjalankan swap logic
7. PoolManager → Hook.afterSwap() (jika aktif)
8. BalanceDelta dikembalikan ke Periphery
9. Periphery menyelesaikan saldo (transfer token)
10. PoolManager memastikan tidak ada saldo pending, lalu lock
1.3.4 BalanceDelta
BalanceDelta adalah dua nilai int128 yang merepresentasikan perubahan saldo amount0 dan amount1 dari perspektif user:
- Nilai positif = PoolManager berhutang ke user (user menerima token)
- Nilai negatif = User berhutang ke PoolManager (user mengirim token)
Saldo bisa diselesaikan dengan dua cara:
- Transfer token ERC-20 secara langsung
- Mint/burn ERC-6909 claim token
1.4 Ticks dan sqrtPriceX96
1.4.1 Apa itu Tick?
Uniswap V3/V4 menggunakan concentrated liquidity dengan pricing curve yang terbatas (finite). Curve ini dipecah menjadi titik-titik diskrit yang disebut ticks.
Setiap tick merepresentasikan harga spesifik:
Harga pada tick i = 1.0001^i
Contoh:
- Tick = 0: harga = 1.0001^0 = 1 (kedua token bernilai sama)
- Tick = 10: harga = 1.0001^10 ≈ 1.001 (Token 0 sedikit lebih mahal)
- Tick = -10: harga = 1.0001^-10 ≈ 0.999 (Token 1 sedikit lebih mahal)
Angka 1.0001 dipilih karena setiap tick bergerak 1 basis point (0.01%) — satuan yang umum dalam dunia keuangan.
Token 0 vs Token 1: Ditentukan berdasarkan sorting lexicographic dari address contract. Address yang lebih kecil menjadi Token 0. Native token (ETH) selalu menjadi Token 0 karena direpresentasikan oleh address(0).
Tick spacing: Jarak minimum antar tick yang bisa digunakan. Misalnya jika tick spacing = 60, maka trade hanya bisa terjadi di tick ..., -120, -60, 0, 60, 120, ...
1.4.2 sqrtPriceX96 (Q64.96)
Solidity tidak mendukung floating point. Untuk perhitungan harga di dalam contract, Uniswap menggunakan Q64.96 notation — format fixed-point yang menggunakan 64 bit untuk integer dan 96 bit untuk pecahan.
Nilai Q64.96 = Nilai desimal × 2^96
Contoh: angka 1.000234 tidak bisa disimpan di Solidity secara langsung. Tapi dalam Q64.96:
1.000234 × 2^96 = 79246701904292675448540839620 (bisa disimpan sebagai uint256)
Kenapa hook developer perlu tahu ini?
Parameter sqrtPriceLimitX96 dalam SwapParams menggunakan format ini untuk menentukan batas slippage:
struct SwapParams {
int24 tickSpacing;
bool zeroForOne;
int256 amountSpecified;
uint160 sqrtPriceLimitX96; // Batas slippage dalam format Q64.96
}
Saat membangun hook seperti onchain orderbook, kamu perlu mengkonversi antara harga yang user-friendly dan sqrtPriceLimitX96.
1.5 Membangun Hook Pertama: Points Hook
1.5.1 Deskripsi
Kita akan membuat hook sederhana yang memberikan POINTS token (ERC-1155) ke user sebagai insentif saat mereka membeli TOKEN dengan ETH.
Aturan:
- Pool: ETH ↔ TOKEN
- Ketika user swap ETH → TOKEN, mereka dapat POINTS = 20% dari jumlah ETH yang diswap
- POINTS diterbitkan sebagai ERC-1155 token
Repository: github.com/haardikk21/points-hook
1.5.2 Kenapa afterSwap?
Kita perlu mengetahui berapa ETH yang benar-benar dihabiskan user. Ada dua jenis swap:
- Exact Input for Output: User tentukan jumlah ETH yang mau dijual (
amountSpecified < 0) - Exact Output for Input: User tentukan jumlah TOKEN yang mau dibeli (
amountSpecified > 0)
Untuk kasus ke-2, kita tidak tahu pasti berapa ETH yang dihabiskan sampai swap selesai. afterSwap menyediakan BalanceDelta yang berisi jumlah pasti, sementara beforeSwap tidak.
1.5.3 Setup Foundry
forge init points-hook
cd points-hook
forge install https://github.com/Uniswap/v4-periphery
forge remappings > remappings.txt
rm ./**/Counter*.sol
Konfigurasi foundry.toml:
solc_version = '0.8.26'
evm_version = "cancun"
optimizer_runs = 800
via_ir = false
ffi = true