Mengenali dan menangkis kerentanan klasik kontrak pintar: reentrancy dengan pola checks-effects-interactions dan nonReentrant, overflow bawaan 0.8.x, serangan flash loans, manipulasi oracle, front-running, serta pitfall tx.origin dan selfdestruct

Setelah di episode 14 kalian menguasai proxy dan governance, pada episode ini kita memasuki topik yang membedakan developer Solidity biasa dari yang profesional: keamanan.
Mengapa episode ini penting? Karena sejarah menunjukkan bahwa bug keamanan kecil bisa berarti kerugian miliaran dolar. Serangan reentrancy pada 2016 memicu hard fork Ethereum (The DAO). Puluhan protokol sejak itu jatuh karena pola yang sama atau variasinya. Di dunia kontrak pintar, keamanan bukan pelengkap — ia adalah syarat untuk beroperasi.
Reentrancy terjadi saat kontrak mengirim ETH/token keluar sebelum memperbarui state-nya, sehingga pemanggil bisa masuk lagi ke fungsi yang sama sebelum panggilan pertama selesai.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.36;
contract VulnerableVault {
mapping(address => uint256) public balances;
function withdraw() external {
uint256 amount = balances[msg.sender];
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "gagal kirim");
balances[msg.sender] = 0; // BUG: state diubah SETELAH kirim dana
}
}Kontrak penyerang dengan receive() kosong (fallback) memanggil kembali withdraw() — dan karena balances belum di-set 0, saldo masih penuh. Ini bisa diulang sampai vault terkuras.
Aturan emas: ubah state dulu, baru berinteraksi dengan kontrak lain.
function withdraw() external {
// Checks
uint256 amount = balances[msg.sender];
require(amount > 0, "nol");
// Effects: update state SEBELUM interaksi
balances[msg.sender] = 0;
// Interactions
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "gagal kirim");
}Sekarang panggilan ulang masuk ke withdraw() yang melihat saldo sudah 0 — serangan gagal. CEI adalah pertahanan pertama dan paling penting untuk fungsi yang mentransfer aset.
Untuk jaring pengaman ekstra, gunakan reentrancy guard — lock sederhana yang menolak panggilan masuk saat fungsi sedang berjalan:
bool private _locked;
modifier nonReentrant() {
require(!_locked, "reentrancy terdeteksi");
_locked = true;
_;
_locked = false;
}Versi production tersedia di OpenZeppelin (ReentrancyGuard). Kebiasaan baik: pasang nonReentrant di semua fungsi yang mentransfer aset, bahkan jika sudah menerapkan CEI — pertahanan berlapis.
Warning
Ingat pelajaran besar dari The DAO: reentrancy tidak hanya lewat call — bisa lewat token yang memiliki hook (ERC-777) atau transferFrom yang memanggil kontrak lain. Selalu kombinasikan CEI + nonReentrant, dan audit semua jalur interaksi eksternal.
Sejak Solidity 0.8.0, overflow/underflow otomatis revert:
uint8 x = 255;
x + 1; // revert: overflowUntuk kasus tertentu yang membutuhkan wrap-around (misal hash computation), gunakan unchecked — tapi sadarilah kalian menonaktifkan proteksi:
unchecked {
counter += 1; // dibungkus, tanpa cek
}Pada kontrak lama (pra-0.8), overflow adalah kerentanan besar — inilah mengapa upgrade ke 0.8.x sangat disarankan (kita bahas versi di episode 16).
Flash loan meminjamkan dana tanpa kolateral dengan syarat dikembalikan dalam satu transaksi. Ini alat sah, tetapi juga bahan serangan: attacker meminjam miliaran untuk memanipulasi harga sementara, lalu memanfaatkan protokol yang bergantung pada harga itu. Mitigasi terkuat adalah harga yang tidak mudah dimanipulasi (oracle terdesentralisasi dari episode 13).
Jika protokol memakai harga dari pool yang dangkal (misal cadangan kecil di satu AMM), attacker bisa memompa harga, menabrak protokol, lalu menjual kembali. Solusi: pakai Chainlink Price Feeds yang mengagregasi banyak sumber dan tahan manipulasi.
Validator/mempool memungkinkan orang lain melihat transaksi sebelum masuk block. Attacker bisa mendahului transaksi swap yang besar (sandwich attack) atau memburu transaksi yang menguntungkan. Mitigasi:
amountOutMin — batasi slippage (kita buat di episode 12).require(tx.origin == owner); // RENTAN: phishingtx.origin adalah alamat yang memulai rantai transaksi, bukan pemanggil langsung. Sebuah kontrak phising bisa memanggil kontrak kalian, dan tx.origin tetap mengembalikan wallet pengguna — melewati cek! Selalu gunakan msg.sender:
require(msg.sender == owner); // amanselfdestruct menghapus kontrak dan mengirim ETH-nya — tetapi fungsi yang memakai selfdestruct untuk transfer tidak bisa dipercaya sebagai "guaranteed payment": penyerang bisa memanggil selfdestruct terhadap alamat lain dan memaksakan ETH masuk tanpa melewati receive() kontrak tujuan. Jangan pernah menganggap saldo ETH sebagai "aman" tanpa logika.
Keamanan bukan sekadar daftar teknik — ini pola pikir:
Important
Aturan praktis yang menghemat segalanya: CEI untuk semua transfer aset, msg.sender bukan tx.origin, oracle yang tidak termanipulasi, dan test negatif untuk setiap jalur revert. Terapkan empat ini, dan kalian sudah menghindari mayoritas kerentanan klasik.
Inti yang harus dibawa pulang:
nonReentrant.unchecked menonaktifkannya dengan sadar.amountOutMin.tx.origin tidak pernah dipakai untuk authorization; selfdestruct tidak bisa menjamin transfer.Di episode 16 selanjutnya kita akan membahas Solidity 0.8.36 & fitur terbaru — rilis stabil terbaru, fitur SSA stack-to-memory spilling, penghapusan backend EOF eksperimental, builtin erc7201, support Fusaka dan default EVM osaka, serta persiapan menuju 0.9.0 yang breaking. Sampai jumpa di episode 16!