Reentrancy Bukan Cuma Masalah withdraw()
Reentrancy bukan sekadar contoh DAO lama. Gue jelaskan callback, external call, token hook, read-only reentrancy, dan kenapa pola Checks-Effects-Interactions masih penting.
0xNN · · 9 min read
Reentrancy itu pola, bukan nama satu fungsi
Contoh yang paling sering dipakai memang withdraw(): contract mengirim ETH, penerima menjalankan fallback, lalu memanggil withdraw() lagi sebelum saldo internal diperbarui.
Contoh itu penting, tetapi kalau lo menghafalnya sebagai “reentrancy = withdraw”, lo bisa melewatkan variasi lain. Intinya adalah external call memberi kesempatan kepada kode lain untuk masuk kembali sebelum invariant contract aman.
---
Versi paling sederhana
Kode rentan biasanya terlihat seperti ini:
function withdraw() external {
uint256 amount = balances[msg.sender];
(bool ok,) = msg.sender.call{value: amount}("");
require(ok);
balances[msg.sender] = 0;
}
Saldo baru diubah setelah external call. Contract penerima bisa memanggil fungsi lagi ketika saldo lama masih terlihat.
Pola Checks-Effects-Interactions membalik urutannya: cek kondisi, ubah state, baru panggil contract eksternal.
---
Reentrancy bisa muncul di tempat lain
Callback token. ERC-777, ERC-721, ERC-1155, dan token dengan hook dapat memanggil kode penerima. Contract yang mengira “transfer token sudah selesai” mungkin sebenarnya sedang memberi callback kesempatan masuk lagi.
External protocol call. DEX, lending market, oracle, dan vault saling memanggil contract lain. Invariant bisa rusak jika state internal belum selesai diperbarui sebelum call keluar.
Cross-function reentrancy. Attacker tidak harus memanggil fungsi yang sama. Ia bisa masuk lewat fungsi lain yang membaca state setengah diperbarui.
Read-only reentrancy. Fungsi view memang tidak mengubah state, tetapi protocol lain bisa membaca nilai sementara ketika callback berlangsung. Jika nilai itu dipakai untuk keputusan harga atau collateral, efeknya tetap berbahaya.
---
Checklist review
1. Tandai setiap call, delegatecall, transfer token, dan callback.
2. Catat state yang harus tetap konsisten sebelum dan sesudah call.
3. Periksa apakah state diperbarui sebelum interaksi eksternal.
4. Cari fungsi lain yang dapat dipanggil dari callback.
5. Periksa guard seperti ReentrancyGuard, tetapi jangan menganggap modifier sendirian cukup.
6. Uji dengan attacker contract, bukan hanya happy-path unit test.
7. Pastikan integrasi oracle dan vault tidak membaca state sementara.
ReentrancyGuard membantu, tetapi ia bukan pengganti desain invariant. Guard mungkin tidak melindungi fungsi lain, cross-contract path, atau asumsi yang salah tentang callback.
---
Kesimpulan
Reentrancy bukan mantra yang selesai setelah menambahkan satu modifier. Ini pertanyaan tentang kapan contract mempercayai kode eksternal dan kapan state internal dianggap final.
Kalau ada external call, gue selalu bertanya: apa yang bisa dipanggil kembali, state apa yang terlihat, dan apakah nilai yang terlihat masih konsisten?
---
Sumber
• Solidity: Security Considerations
• OpenZeppelin: ReentrancyGuard
• OWASP Smart Contract Security Weakness Enumeration
*Ditulis karena banyak checklist berhenti di “tambahkan ReentrancyGuard”, padahal bug-nya sering ada di asumsi antar-contract.*