Sejak Uniswap v4 diluncurkan di mainnet, mekanisme Hook telah menjadi salah satu inovasi DeFi yang paling banyak mendapat perhatian. Platform peluncuran memecoin Flaunch di Base chain menggunakan Hook untuk mewujudkan mekanisme harga pra-penjualan tetap dan peluncuran otomatis likuidasi; protokol likuiditas Bunni v2 menggunakan Hook untuk membangun model likuiditas yang dapat diprogram dan re-staking; tahun ini, token seperti SATO, uPEG (Unipeg), dan Slonks yang berkaitan dengan permainan Hook juga mencapai kenaikan puluhan kali lipat dalam waktu singkat.
Di sisi lain, seiring dengan kemakmuran ekosistem Hook, serangan terhadap kelemahan implementasi Hook juga meningkat signifikan. Artikel ini akan dimulai dengan mekanisme Hook Uniswap v4, kemudian menganalisis stack panggilan intinya secara bertahap, untuk membantu pemahaman proyek tentang kerentanan yang mungkin ada di dalamnya.
Keamanan Hook Uniswap v4
1. Pengenalan
Perubahan arsitektur paling signifikan dari Uniswap v4 dibandingkan v3 adalah diperkenalkannya mekanisme Hook (kaitan): memungkinkan pengembang memasang kontrak kustom ke peristiwa siklus hidup pool likuiditas, dan menyuntikkan logika arbitrer pada node seperti swap, menambah/kurangi likuiditas, inisialisasi, dan lainnya.
Beberapa perubahan kunci v4 adalah sebagai berikut:
- Mode Singleton: Status semua pool dikelola secara terpusat oleh satu kontrak PoolManager, tidak lagi men-deploy kontrak independen untuk setiap pool.
- Flash accounting: Perubahan saldo sementara selama proses transaksi hanya dicatat dalam penyimpanan transient, dan diselesaikan secara seragam hanya saat transaksi berakhir.
- Mekanisme Hook: Setiap pool dapat mengikat satu kontrak Hook, dan PoolManager akan melakukan callback ke kontrak tersebut pada node kunci (seperti beforeInitialize, beforeSwap, afterAddLiquidity, dll.).
- Hook Tidak Dapat Diganti: Setelah pool diinisialisasi, alamat Hook yang terikat menjadi tetap secara permanen (Alamat Hook yang terikat pada pool tidak dapat diubah, tetapi apakah kontrak Hook itu sendiri dapat di-upgrade bergantung pada cara implementasinya).
Di era v3, pengembang hanya perlu mempercayai protokol Uniswap itu sendiri; sedangkan di era v4, keamanan setiap pool bergantung pada Hook yang terikat padanya. Hook mengubah AMM dari sebuah primitif keuangan yang tetap, menjadi infrastruktur keuangan yang dapat diprogram, tetapi model keamanannya juga menjadi terfragmentasi dari level "protokol" ke level "pool".
2. Arsitektur Hook
2.1 PoolManager dan model unlock/callback
Kontrak inti v4 adalah PoolManager yang bersifat singleton. Setiap operasi perubahan status pool (swap, menambah/mengurangi likuiditas) harus terlebih dahulu memanggil PoolManager.unlock(), untuk mendapatkan izin callback satu kali, kemudian menyelesaikan tindakan spesifik dalam unlockCallback(). Di akhir seluruh proses, PoolManager akan memverifikasi apakah buku besar seimbang:
Ketika NonzeroDeltaCount != 0, transaksi akan langsung direvert. Ini adalah kendala inti dari flash accounting v4. Setiap Hook dapat membuat buku besar tidak seimbang sementara selama eksekusi, tetapi sebelum transaksi berakhir harus menyelesaikannya sendiri, jika tidak seluruh transaksi akan di-rollback.
Setiap pool diidentifikasi secara unik oleh struktur PoolKey, yang berisi field hooks:
PoolId dihitung dari keccak256(PoolKey), oleh karena itu alamat hooks yang berbeda akan menghasilkan pool yang berbeda. Ini sekaligus berarti, PoolManager tidak akan memverifikasi apakah suatu alamat Hook pernah digunakan untuk pool lain; kontrak Hook yang sama dapat diikat oleh beberapa pool secara bersamaan.
2.2 Bit izin Hook yang dikodekan dalam alamat
Salah satu desain kontra-intuitif v4 adalah: Izin Hook tidak ditentukan oleh variabel tertentu di dalam kontrak, tetapi ditentukan oleh alamat deployment kontrak Hook.
PoolManager memeriksa 14 bit rendah dari alamat Hook untuk menentukan apakah Hook perlu dipanggil pada titik siklus hidup tertentu:
Misalnya BEFORE_SWAP_FLAG = 1 << 7. Jika bit ke-7 dari alamat Hook adalah 1, PoolManager akan memanggil beforeSwap() dari Hook tersebut sebelum swap; sebaliknya, bahkan jika kontrak Hook mengimplementasikan beforeSwap(), ia tidak akan pernah dipanggil oleh PoolManager.
Ini berarti saat deployment Hook, alamat harus dihitung melalui CREATE2 + salt, sehingga menghasilkan alamat yang bit rendahnya sepenuhnya konsisten dengan izin target. Uniswap secara resmi menyediakan alat HookMiner untuk tujuan ini:
Ketika bit izin tidak konsisten dengan implementasi fungsi, akan muncul dua jenis masalah:
(1) Mengimplementasikan fungsi hook tertentu, tetapi alamat tidak mengkodekan bit izin yang sesuai – PoolManager tidak akan pernah memanggil fungsi tersebut, logika menjadi tidak efektif.
(2) Alamat mengkodekan bit izin tertentu, tetapi hook tidak mengimplementasikan fungsi yang sesuai – Saat PoolManager melakukan callback, mungkin terjadi revert yang mengakibatkan DOS atau kegagalan validasi nilai kembali, sehingga operasi terkait tidak dapat dieksekusi.
Ini sekaligus menjadi hambatan alami untuk upgrade Hook: Jika Hook dapat di-upgrade melalui proxy, alamat deployment tidak berubah saat upgrade, sehingga setelah upgrade hanya dapat mengubah implementasi fungsi hook yang sudah ada, dan tidak dapat menambahkan jenis hook baru. Untuk menyediakan kemampuan ekspansi di masa depan, semua bit izin yang mungkin digunakan harus ditambang terlebih dahulu pada deployment awal.
2.3 BaseHook dan perangkap kontrol akses yang sering diabaikan
Kontrak abstrak BaseHook yang disediakan oleh versi lama periphery Uniswap v4 memungkinkan pengembang menginherit-nya untuk mengimplementasikan Hook kustom. Salah satu peran penting BaseHook adalah menyediakan modifier onlyPoolManager untuk fungsi unlockCallback():
Namun – ada perangkap desain yang sangat mudah diabaikan di sini – versi awal BaseHook hanya menambahkan onlyPoolManager untuk unlockCallback, dan tidak memberikan perlindungan apa pun terhadap fungsi callback hook lainnya (seperti beforeSwap, afterSwap, beforeAddLiquidity, dll.). Kontrol akses untuk fungsi-fungsi ini harus ditambahkan secara eksplisit oleh pengembang Hook.
3. Tinjauan Kode Siklus Hidup Hook
Mengambil contoh satu kali swap exact-input, di bawah ini dianalisis stack panggilan lengkap dari pengguna memulai transaksi hingga penyelesaian.
3.1 Inisialisasi Pool dan Pengikatan Hook
Siapa pun dapat memanggil PoolManager.initialize() untuk membuat pool baru:
isValidHookAddress hanya memverifikasi kompatibilitas bit izin alamat dengan field fee, tidak memverifikasi apakah Hook telah digunakan di pool lain, dan tidak memverifikasi apakah Hook tersebut "bersedia" menerima PoolKey ini. Jika Hook tidak dirancang untuk menambahkan logika whitelist atau pengikatan single-pool dalam beforeInitialize, siapa pun dapat membuat pool baru menggunakan Hook yang sama, tetapi dengan pasangan token yang arbitrer, dan memicu semua callback Hook selanjutnya.
3.2 beforeSwap dan BeforeSwapDelta
Pintu masuk alur swap adalah PoolManager.swap(), yang sebelum mengeksekusi logika swap inti akan memanggil Hooks.beforeSwap():
Nilai kembali beforeSwap adalah triple (bytes4, BeforeSwapDelta, uint24):
- bytes4: Harus sama dengan IHooks.beforeSwap.selector, jika tidak PoolManager langsung revert.
- BeforeSwapDelta: Penyesuaian delta Hook terhadap specified token dan unspecified token dalam swap ini.
- uint24: Nilai override biaya LP dinamis (hanya berlaku ketika pool mengaktifkan biaya dinamis).
BeforeSwapDelta adalah alias untuk int256, 128 bit tinggi adalah delta dari specified token (yaitu token yang jumlahnya ditentukan pengguna), dan 128 bit rendah adalah delta dari unspecified token:
Perlu diperhatikan bahwa semantik BeforeSwapDelta adalah: Hook yang memungut biaya seharusnya mengembalikan nilai positif, dan Hook yang mengembalikan token seharusnya mengembalikan nilai negatif. Pengembang sangat mudah membalikkan tanda; sekaligus, hubungan korespondensi antara specified dan unspecified bergantung pada params.zeroForOne dan tanda positif/negatif amountSpecified, kesalahan penulisan sedikit saja dapat menyebabkan token tertukar posisi.
PoolManager akan menambahkan specifiedDelta yang dikembalikan oleh beforeSwap langsung ke amountToSwap:
Baris ini menyiratkan semantik kunci: Hook dapat menahan jumlah swap. Ketika hookDeltaSpecified sama dengan -params.amountSpecified, amountToSwap langsung menjadi nol, yang setara dengan Hook sepenuhnya mengambil alih swap ini – inilah yang disebut sebagai Async Hook atau Custom Curve Hook.
Async Hook adalah salah satu pola desain dengan risiko tertinggi di v4: Pada dasarnya, ia menggantikan logika swap Uniswap dengan logika Hook sendiri. Jika Hook memiliki kerentanan atau pada dasarnya jahat, dana pengguna tidak lagi dibatasi oleh logika penetapan harga asli Uniswap, tetapi terutama bergantung pada kebenaran implementasi Hook itu sendiri.
3.3 Penyelesaian Delta dan NonzeroDeltaCount
Delta yang dikembalikan oleh beforeSwap dan afterSwap tidak akan segera memicu transfer, tetapi dicatat ke dalam buku besar internal PoolManager:
Setiap kali delta kumulatif suatu token berubah dari nol menjadi bukan nol, NonzeroDeltaCount bertambah; ketika kembali menjadi nol, berkurang. Seperti dijelaskan di 2.1, jika pada akhir unlock() NonzeroDeltaCount != 0, seluruh transaksi akan direvert.
Hook menyeimbangkan delta-nya melalui dua tindakan: settle() (mentransfer ke PoolManager) dan take() (mengambil dari PoolManager):
Semantik keamanan yang dibawa oleh mekanisme ini jelas: Semua orang pada akhirnya harus menyeimbangkan buku besar. Namun, ini hanya menjamin "kekekalan buku besar", bukan "kebenaran buku besar". Jika Hook mengembalikan delta yang dibangun dengan jahat dalam beforeSwap, PoolManager akan setia mencatat delta ini, asalkan akhirnya diselesaikan dengan seimbang, transaksi berhasil – bahkan jika ini berarti Hook dapat memalsukan status bisnis, membuat sistem secara salah menganggap penyerang memiliki beberapa hak aset, dan PoolManager tidak dapat mengenali kesalahan pada level bisnis ini.
Insiden keamanan Cork Protocol sebelumnya terjadi karena Hook-nya memiliki kerentanan, dan sebelum diserang telah diaudit oleh empat perusahaan audit. Setelah ditinjau kembali, kami menemukan:
- Dari empat audit, tiga di antaranya tidak memasukkan kontrak CorkHook dalam scope.
- Satu-satunya yang mengaudit CorkHook mengidentifikasi beberapa masalah kode dan mengajukan saran perbaikan, tetapi tidak sepenuhnya mencakup masalah kontrol akses.
- Ada satu auditor lain yang secara eksplisit menyarankan dalam laporannya: "an interesting follow-up engagement would be to prove the invariants for the CorkHook functions that are being invoked by different components verified within the scope of this engagement". Dari sudut pandang pasca-kejadian, saran ini sangat relevan.
Ini mengungkapkan area buta audit baru di era Hook v4: Ledakan kompleksitas protokol menyebabkan penentuan scope itu sendiri menjadi keputusan keamanan. Rantai interaksi antara Hook dan kontrak protokol lainnya sangat panjang, mengaudit kontrak Hook secara terpisah tidak cukup untuk menemukan masalah kombinasi lintas kontrak; sebaliknya, mengaudit kontrak sekitar tanpa memasukkan Hook dalam scope, akan melewatkan vektor serangan terbesar di era v4.
4. Refleksi
Melihat mekanisme protokol dan tinjauan ulang serangan Cork secara berdampingan, dapat disimpulkan beberapa poin inti model keamanan Hook v4:
(1) Jika fungsi callback Hook bergantung pada konteks panggilan yang disediakan oleh PoolManager, harus secara eksplisit membatasi hanya dapat dipanggil oleh PoolManager. BaseHook tidak melakukan ini untuk pengembang, ini adalah perangkap desain yang paling mudah bertentangan dengan pengalaman audit kontrak umum di v4.
(2) Hubungan pengikatan Hook dengan pool tidak dibatasi oleh PoolManager. Pengembang harus mengimplementasikan whitelist pool atau pengikatan single-pool secara mandiri dalam beforeInitialize.
(3) Bit izin alamat Hook harus konsisten sepenuhnya dengan implementasi fungsi. Alamat yang dihitung harus memasukkan semua bit izin yang mungkin diperluas di masa depan secara terlebih dahulu.
(4) Async / Custom Curve Hook pada dasarnya adalah implementasi swap yang sepenuhnya kustom. Ini tidak memiliki perlindungan apa pun pada tingkat protokol Uniswap, dan harus diaudit dengan standar "kontrak keuangan yang sepenuhnya otonom".
(5) "Kekekalan" akuntansi Delta tidak sama dengan "kebenaran". NonzeroDeltaCount == 0 hanya dapat menjamin buku besar akhirnya seimbang, tidak dapat menjamin isi buku besar belum dimanipulasi dengan jahat.
(6) Kebingungan jenis token lintas pasar adalah vektor serangan baru di era v4. Ketika protokol memungkinkan pengguna membuat pasar, validasi semantik token adalah wajib, tidak boleh hanya bergantung pada pemeriksaan antarmuka.
Setiap Hook adalah domain kepercayaan yang independen, keamanan setiap pool ditentukan oleh Hook yang terikat padanya. Kompleksitas audit keamanan Hook karena itu bukan lagi "mengaudit satu kode", tetapi "mengaudit sub-protokol yang lengkap" – perubahan ini berarti peningkatan metodologis bagi pihak proyek dan auditor.
Lihat artikel asli



















