Kerentanan smart contract — reentrancy, overflow, front-running
Bug di backend biasa berarti hotfix dan deploy ulang. Bug di smart contract yang sudah live di mainnet bisa berarti dana hilang permanen dalam hitungan detik — tidak ada rollback, tidak ada 'undo'. Tiga kelas kerentanan ini wajib dipahami sebelum menyentuh kontrak produksi.
Intisari
- Reentrancy: kontrak eksternal 'memanggil balik' fungsimu sebelum state selesai diperbarui — pola serangan yang membobol The DAO senilai ~$60 juta tahun 2016.
- Pertahanannya: pola Checks-Effects-Interactions (ubah state sebelum memanggil kontrak luar) dan modifier
ReentrancyGuarddari OpenZeppelin. - Integer overflow/underflow sudah otomatis dicegah compiler sejak Solidity 0.8.0 (Fase 1) — tapi kode yang sengaja pakai blok
uncheckedtetap rentan kalau tidak dianalisis cermat. - Front-running/MEV: transaksimu terlihat di mempool sebelum masuk block — pihak lain bisa menyisipkan transaksi mereka sendiri di depan milikmu untuk mengambil keuntungan.
tx.originuntuk otorisasi adalah bug klasik — selalu pakaimsg.sender, karenatx.originbisa dieksploitasi lewat kontrak perantara yang menipu pengguna.
Reentrancy — kasus yang mendefinisikan seluruh bidang keamanan Solidity
Ini kerentanan paling terkenal di sejarah Ethereum: The DAO, tahun 2016, kehilangan ~$60 juta karena satu pola bug yang sekarang jadi pelajaran pertama siapa pun yang belajar keamanan smart contract.
// RENTAN — jangan ditiru
mapping(address => uint256) public saldo;
function tarikDana() external {
uint256 jumlah = saldo[msg.sender];
require(jumlah > 0, "Saldo kosong");
// BAHAYA: memanggil alamat eksternal SEBELUM saldo di-nol-kan
(bool sukses, ) = msg.sender.call{value: jumlah}("");
require(sukses, "Transfer gagal");
saldo[msg.sender] = 0; // terlambat — penyerang sudah menarik berkali-kali
}
Kalau msg.sender adalah kontrak jahat, fungsi receive()/fallback()-nya
bisa memanggil balik tarikDana() sebelum baris terakhir sempat jalan — karena
saldo[msg.sender] masih belum di-nol-kan, penarikan bisa terjadi berkali-kali dalam satu
transaksi, menguras seluruh saldo kontrak.
Pertahanan 1: Checks-Effects-Interactions
// AMAN — state diperbarui SEBELUM memanggil pihak luar
function tarikDana() external {
uint256 jumlah = saldo[msg.sender]; // Checks
require(jumlah > 0, "Saldo kosong");
saldo[msg.sender] = 0; // Effects — dulu duluan
(bool sukses, ) = msg.sender.call{value: jumlah}(""); // Interactions — terakhir
require(sukses, "Transfer gagal");
}
Urutannya sengaja: Checks (validasi), Effects (ubah state internal),
Interactions (panggil kontrak/address lain). Dengan urutan ini, meski penyerang memanggil
balik, saldo[msg.sender] sudah 0 — penarikan kedua akan gagal di baris require.
Pertahanan 2: ReentrancyGuard
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract Bendahara is ReentrancyGuard {
function tarikDana() external nonReentrant {
// modifier ini menolak panggilan berulang ke fungsi yang sama
// (atau fungsi nonReentrant lain) selama eksekusi masih berjalan
}
}
Pakai keduanya, bukan salah satu. Checks-Effects-Interactions adalah kebiasaan menulis kode
yang seharusnya berlaku di semua fungsi. ReentrancyGuard adalah jaring pengaman untuk
kasus di mana urutan yang benar sulit dijamin (mis. banyak external call di satu fungsi).
Tim produksi memakai keduanya sekaligus, bukan menganggap satu cukup untuk menggantikan yang lain.
Front-running: mempool itu ruang publik
Sebelum masuk block, transaksi menunggu di mempool — dan mempool bisa dilihat siapa saja, termasuk bot otomatis. Kalau transaksimu (mis. membeli token di harga tertentu) terlihat menguntungkan bagi pihak lain, mereka bisa mengirim transaksi serupa dengan gas price lebih tinggi supaya masuk block lebih dulu dari milikmu — mengambil keuntungan sebelum transaksimu sempat dieksekusi.
| Mitigasi | Cara kerja |
|---|---|
| Slippage protection | Fungsi menerima parameter "jumlah minimum yang diterima" — revert kalau harga sudah berubah terlalu jauh |
| Commit-reveal | Kirim hash komitmen dulu (harga/pilihan disembunyikan), baru ungkap nilai sungguhannya di transaksi berikutnya |
| Private mempool (mis. Flashbots) | Transaksi tidak melewati mempool publik sebelum masuk block |
tx.origin vs msg.sender — jebakan otorisasi klasik
// RENTAN — jangan ditiru
function tarikDanaAdmin() external {
require(tx.origin == owner, "Bukan owner"); // BAHAYA
// ...
}
tx.origin selalu merujuk ke akun yang memulai rantai panggilan, bahkan lewat berlapis
kontrak perantara. Kalau owner tergoda memanggil kontrak jahat (mis. lewat link phising), dan
kontrak jahat itu memanggil tarikDanaAdmin(), pengecekan tx.origin == owner tetap
lolos — padahal pemanggil langsungnya (msg.sender) adalah kontrak jahat, bukan
transaksi resmi dari owner. Selalu pakai msg.sender untuk otorisasi.
Sumber belajar lanjutan yang layak dikunjungi: Security Considerations di dokumentasi resmi Solidity mencakup lebih banyak kelas kerentanan (denial of service lewat gas limit, randomness yang bisa ditebak, dst.) di luar tiga yang dibahas di sini. Untuk kontrak yang menyimpan aset bernilai signifikan, materi ini adalah titik awal, bukan daftar lengkap — itulah kenapa audit independen (materi terakhir fase ini) tetap wajib sebelum deploy ke mainnet.
Latihan: tulis ulang fungsi redeemGold() di kontrak GoldToken
(Fase 2) — meski versi aslinya sudah relatif aman karena memakai _burn sebelum interaksi
eksternal apa pun — tambahkan modifier nonReentrant dari ReentrancyGuard sebagai
lapisan pertahanan kedua, lalu jelaskan dalam komentar kenapa itu tetap berguna meski urutan
Checks-Effects-Interactions-nya sudah benar.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.