Belajar Blockchain Developer - Smart Contract Security
Episode 18 of 28

Belajar Blockchain Developer - Smart Contract Security

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

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

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: Serangan Klasik

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.

contracts/Vulnerable.sol
// 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:

Alur serangan reentrancy
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 habis

Serangan 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 Pattern

CEI (Checks-Effects-Interactions) adalah aturan emas penulisan fungsi yang aman:

Urutan CEI
1. Checks    → validasi semua kondisi (require)
2. Effects   → perbarui semua state
3. Interactions → baru lakukan external call

Terapkan pada fungsi withdraw:

contracts/Safe.sol
// 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.

Integer Overflow dan Underflow

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:

  1. unchecked blocks — kalian menonaktifkan cek, dan salah batas = bug.
  2. Conversion antar tipe — misal uint256 ke uint128 dipotong diam-diam.
contracts/Overflow.sol
// 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)).

Front-Running

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:

PendekatanCara kerja
Commit-revealPengguna kirim hash komitmen dulu, buka nilai belakangan
Batch/private mempoolTransaksi tidak terlihat publik (Flashbots, MEV protection)
Batas slippageHarga maksimal yang mau diterima (parameter di swap)
Oracle price checkValidasi 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.

Kerentanan Lain yang Sering Muncul

Selain tiga besar di atas, berikut yang wajib masuk radar kalian:

  • Access control: fungsi admin tanpa onlyOwner — siapa pun bisa memanggil.
  • Missing zero-address check: transfer ke address(0) mengunci aset.
  • Stale oracle: harga basi dipakai untuk eksekusi (episode 11).
  • Denial of Service: external call yang selalu gagal mengunci kontrak (kenapa pull-over-push penting, episode 5).
  • Selfdestruct/selbstransfer: kontrak yang memaksa transfer ETH tanpa 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.

Praktik: Security Review

Sekarang praktik. Baca kontrak berikut dan temukan semua masalahnya:

contracts/Review.sol
// 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:

  1. Baca alur fungsi — external call terjadi sebelum state update → reentrancy.
  2. Cek orderbalances di-set 0 setelah call → penyerang bisa withdraw berulang.
  3. Cek edge case — saldo 0 tetap menjalankan call dengan value 0 (boros, dan bisa gagal pada kontrak yang menolak).
  4. Cek fix — pindahkan 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?"

Penutup

Inti yang harus dibawa pulang:

  • Reentrancy = external call sebelum update state; solusi: CEI + ReentrancyGuard.
  • CEI pattern: Checks → Effects → Interactions.
  • Overflow aman sejak 0.8, tapi unchecked dan type conversion adalah jebakan.
  • Front-running butuh slippage protection dan private mempool.
  • Security review = berpikir seperti penyerang di setiap fungsi.

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!

Belajar Blockchain Developer - Smart Contract Security | Belajar Blockchain Developer