Berpikir seperti penyerang: reentrancy, integer overflow, front-running, dan pola Checks-Effects-Interactions (CEI), plus praktik melakukan security review pada kontrak

Setelah di episode 17 kalian mengoptimasi gas, pada episode ini kita masuk ke ranah yang menentukan nasib proyek: keamanan smart contract. Di Web2, bug berarti data bocor; di Web3, bug berarti dana hilang — dan tidak ada "rollback" atau "hotfix" setelah aset berpindah tangan.
Episode ini mengajarkan kalian berpikir seperti penyerang: memahami kerentanan paling berbahaya, pola yang mencegahnya, dan melakukan security review dengan benar.
Reentrancy terjadi ketika kontrak memanggil kontrak lain (external call) sebelum state-nya diperbarui. Penyerang memanfaatkan celah ini untuk memanggil ulang fungsi sebelum panggilan pertama selesai.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Vulnerable {
mapping(address => uint256) public balances;
// VULNERABLE: update state SETELAH external call
function withdraw(uint256 _amount) external {
require(balances[msg.sender] >= _amount, "insufficient");
(bool ok, ) = msg.sender.call{ value: _amount }("");
require(ok, "transfer failed");
balances[msg.sender] -= _amount; // terlambat!
}
}Alur serangan:
1. Attacker memanggil withdraw(100)
2. Kontrak mengirim 100 ETH ke attacker
3. Attacker (kontrak) memanggil withdraw(100) LAGI
4. balances masih belum dikurangi → cek require lolos
5. ETH dikirim lagi... berulang sampai gas habis / saldo habisSerangan ini membobol DAO pada 2016 (kerugian ±$60 juta) dan terus muncul dalam berbagai varian. Perlindungannya sudah kita singgung di episode 5: update state sebelum external call, atau pakai ReentrancyGuard.
CEI (Checks-Effects-Interactions) adalah aturan emas penulisan fungsi yang aman:
1. Checks → validasi semua kondisi (require)
2. Effects → perbarui semua state
3. Interactions → baru lakukan external callTerapkan pada fungsi withdraw:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Safe {
mapping(address => uint256) public balances;
// AMAN: Effects sebelum Interactions
function withdraw(uint256 _amount) external {
// 1. Checks
require(balances[msg.sender] >= _amount, "insufficient");
// 2. Effects — state di-update DULU
balances[msg.sender] -= _amount;
// 3. Interactions — baru external call
(bool ok, ) = msg.sender.call{ value: _amount }("");
require(ok, "transfer failed");
}
}Dengan state sudah dikurangi, panggilan ulang akan ditolak oleh require pada langkah Checks. Inilah satu-satunya perlindungan yang mengatasi akar masalah, bukan gejalanya.
EVM bekerja dengan integer 256-bit. Pada Solidity di bawah 0.8, operasi yang melampaui batas akan membungkus (wrap around): type(uint8).max + 1 menjadi 0. Sejak Solidity 0.8, overflow otomatis melempar error — tapi masih ada dua jebakan:
unchecked blocks — kalian menonaktifkan cek, dan salah batas = bug.uint256 ke uint128 dipotong diam-diam.// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Overflow {
uint256 public counter;
function incrementUnchecked() external {
// BISA overflow! hanya aman jika dipastikan tidak melampaui batas
unchecked {
counter += 1;
}
}
function badCast(uint256 _value) external pure returns (uint128) {
// TRUNCATION: 2^128+1 → 1, data hilang diam-diam
return uint128(_value);
}
}Aturan: unchecked hanya di blok yang bisa dibuktikan aman secara matematis; konversi tipe harus di-validasi (require(_value <= type(uint128).max)).
Karena semua transaksi terlihat publik di mempool sebelum masuk block, penyerang bisa melihat transaksi menguntungkan (misal pending swap) dan mengirim transaksi mereka dengan gas lebih tinggi agar masuk block lebih dulu. Ini disebut front-running.
Kasus umum: pengguna membeli token via AMM; penyerang melihat harga akan naik, membeli duluan, lalu menjual setelah harga pengguna masuk — untung tanpa risiko.
Mitigasi yang dipakai di production:
| Pendekatan | Cara kerja |
|---|---|
| Commit-reveal | Pengguna kirim hash komitmen dulu, buka nilai belakangan |
| Batch/private mempool | Transaksi tidak terlihat publik (Flashbots, MEV protection) |
| Batas slippage | Harga maksimal yang mau diterima (parameter di swap) |
| Oracle price check | Validasi harga eksekusi terhadap harga pasar |
Yang wajib diingat: slippage protection bukan fitur bonus — ia perlindungan minimal terhadap front-running dan sandwich attacks. Jangan pernah mengirim swap tanpa batas slippage.
Selain tiga besar di atas, berikut yang wajib masuk radar kalian:
onlyOwner — siapa pun bisa memanggil.address(0) mengunci aset.receive().Warning
Keamanan bukan checklist satu kali — ia adalah proses. Setiap fungsi baru adalah permukaan serangan baru. Biasakan menulis test negatif untuk setiap fungsi (episode 7) dan meninjau ulang logika setelah setiap perubahan. Audit eksternal (episode 19) melengkapi, bukan menggantikan, disiplin ini.
Sekarang praktik. Baca kontrak berikut dan temukan semua masalahnya:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Review {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
// TEMUKAN masalah di fungsi ini
function withdrawAll() external {
uint256 amount = balances[msg.sender];
(bool ok, ) = msg.sender.call{ value: amount }("");
require(ok, "failed");
balances[msg.sender] = 0;
}
}Langkah review yang benar:
balances di-set 0 setelah call → penyerang bisa withdraw berulang.balances[msg.sender] = 0 ke sebelum external call.Ini persis pola yang kita perbaiki dengan CEI. Latih metode ini berulang: setiap kontrak yang kalian baca, tanyakan "bagaimana aku mengeksploitasi ini?"
Inti yang harus dibawa pulang:
unchecked dan type conversion adalah jebakan.Di episode 19 selanjutnya kita membawa keamanan ke level profesional — Auditing & Certifications: proses audit, tools Slither/Mythril, dan mini-audit mandiri. Mari lanjut ke episode 19!