Tekan ESC untuk menutup

Celah Reentrancy Solidity ChatGPT: 3 Contoh Kode Berbahaya

Ngobrolin soal begadang di hackathon... eh sebentar, mana ada tidur beneran, dulu kita cuma tidur tiga jam di bawah meja server, gue kira kerentanan dasar di smart contract udah pasti beres sama static analyzer apa aja. Apa susahnya sih? Reentrancy (serangan reentrancy) itu udah klasik banget, semua orang juga udah pada bahas. Mulai dari pola Checks-Effects-Interactions, berbagai macam modifier... Eh, tau-tau beberapa bulan lalu ada anak junior masuk tim terus dengan bangganya bilang: «Gue dibikinin contract staking yang sempurna sama ChatGPT, udah dicek kok, aman banget!»

Gue hampir aja menumpahkan kopi ke keyboard mekanik kesayangan.

Jujur-jujuran aja nih: LLM zaman sekarang emang jago nulis Solidity yang estetik. Sintaksnya keren, importnya rapi, OpenZeppelin di-setup paling update. Tapi pas masuk ke logika distribusi state yang njlimet, si AI ini mulai ngasih kode dengan tingkat pede yang kelewat batas—pede banget padahal salah—sampai-sampai nggiring celah keamanan yang pas diaudit bisa ngerampok jutaan dolar. Mari kita bedah tiga kasus nggak biasa di mana ChatGPT keblinger, padahal sok-sokan bilang kalau kode kalian bakal lolos audit CertiK.

Kasus #1: Staking Asinkron dengan Reward per Blok dan Panggilan Eksternal Tersembunyi

Dengerin ya, pas kalian minta AI bikin contract buat distribusi token berdasarkan lama waktu di dalam pool, apa yang dilakuin sama modelnya? Dia bakal pasang modifier nonReentrant di mana-mana yang ada transfer dana ke user. Tapi dia lupa satu detail arsitektur yang krusial.

Coba intip cuplikan kode yang gue comot dari code review asli gara-gara ulah si anak junior tadi:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
interface IERC20Mintable {
    function mint(address to, uint256 amount) external;
}
contract SecureStakingVault {
    IERC20Mintable public rewardToken;
    mapping(address => uint256) public balances;
    mapping(address => uint256) public lastUpdateBlock;
    
    uint256 public rewardRatePerBlock = 10 * 10**18;
    bool private locked;
    // Udah diproteksi nih! Mau apa lagi, si paling paranoid?
    modifier noReentry() {
        require(!locked, "LOCKED");
        locked = true;
        _;
        locked = false;
    }
    constructor(address _token) {
        rewardToken = IERC20Mintable(_token);
    }
    function stake() external payable {
        require(msg.value > 0, "Zero stake");
        balances[msg.sender] += msg.value;
        lastUpdateBlock[msg.sender] = block.number;
    }
    // Karya agung ala ChatGPT yang keliatannya aman banget
    function harvestAndUnstake(uint256 _amount) external noReentry {
        require(balances[msg.sender] >= _amount, "Insufficient balance");
        uint256 blocksPassed = block.number - lastUpdateBlock[msg.sender];
        uint256 reward = blocksPassed * rewardRatePerBlock * _amount / balances[msg.sender];
        // Perhatian: pembaruan state terjadi SEBELUM pemanggilan eksternal mint
        balances[msg.sender] -= _amount;
        lastUpdateBlock[msg.sender] = block.number;
        // Emisi reward memicu constructor token atau hook, dan jeng jeng... kejutan!
        rewardToken.mint(msg.sender, reward);
        (bool success, ) = msg.sender.call{value: _amount}("");
        require(success, "Transfer failed");
    }
}

Di mana letak jebakannya? Si model mikir karena variabel saldo udah di-reset sebelum rewardToken.mint(), serangan mustahil terjadi. Padahal, rewardToken itu ERC-20 kustom yang contract mint-nya punya hook _beforeTokenTransfer atau callback ala ERC-777 (kalau tokennya kompleks atau pakai proxy). Pas fungsi mint dipanggil, kontrol eksekusi pindah ke kode pihak ketiga sebelum ETH benar-benar ketransfer ke user, tapi posisinya pas sesi aktif di mana state pool udah berubah sebelah dan perhitungan reward buat user lain jadi berantakan. Ini tuh jenis Reentrancy antar-contract generasi baru yang jarang dibahas di buku teks standar.

Kasus #2: Pembajakan Lintas Fungsi Lewat Oracle Likuiditas

Liciknya eksploit modern tuh karena reentrancy sering kali bukan nongol di dalam satu fungsi, tapi nyangkut di antara dua entry point berbeda yangsharing mapping yang sama. AI paling demen mecah logika jadi modul-modul "bersih" tanpa peduli sama urutan perubahan global invariant.

  • Parameter Kerentanan: Penilaian static analyzer
  • Kondisi Nyata: Modifier
  • Hadir (nonReentrant): Nggak mempan buat cegah panggilan lintas fungsi
  • Urutan Operasi: Checks-Effects terpenuhi secara lokal
  • Saldo Global Protokol Rusak: Reaksi ChatGPT
  • «Kode aman total dari reentrancy»: Kecolongan manipulasi state silang

Kasus #3: Kontrak Durasi Dinamis dan Vault ERC-4626

ChatGPT demen banget sama template standar OpenZeppelin buat vault yield (ERC-4626). Dia comot contoh jadinya, ditambahin rumus matematika kustom buat ngitung share, terus ngehasilin kode yang langsung compilled di percobaan pertama. Tapi masalahnya, fungsi konversi share ke aset (convertToAssets) dalam kondisi tertentu pas dipanggil lewat kontrak perantara bisa memicu return kontrol multi-level waktu ngitung likuiditas virtual.

Gue sendiri sempet begadang sampai subuh tiga minggu lalu buat debug bug di testnet pas bot simulator nguras semua likuiditas dari pool persis pakai pola begini. Si AI malah nyaranin: «Gunakan scaling multiplier untuk akurasi». Eh, hasilnya malah jebol kena read-only reentrancy klasik, di mana sistem eksternal ngebaca state vault sementara yang belum di-commit pas proses deposit lagi jalan.

Mari kita lanjutkan penyelaman ke dalam teater absurditas yang luar biasa ini, yang dihasilkan oleh model bahasa ketika kita meminta mereka untuk menulis "smart contract yang sederhana dan andal."

Tahukah kalian apa hal paling lucu dari pekerjaan seorang mantan auditor keamanan? Melihat para developer mentah-mentah mempercayai komentar buatan ChatGPT sendiri. AI menulis baris di bagian atas seperti // Safe against reentrancy attacks thanks to Checks-Effects-Interactions — dan selesailah sudah, pemikiran kritis langsung mati. Otak jadi autopilot, mata berkunang-kunang, dan tiba-tiba mainnet langsung meledak jadi kembang api.

Mari kita bedah dua contoh sadis lainnya yang dengan keras kepala dianggap oleh AI sebagai standar emas keamanan, padahal dalam praktiknya bisa jebol dengan sangat mudah.

Kasus #4: Arbitrase Multi-Langkah dengan Pengembalian via Flash Loan

Ini adalah tugas klasik yang sering diberikan di hackathon atau dilempar ke junior dev: menulis kontrak yang mengambil dana flash loan, menggelontorkannya lewat DEX, meraup profit, mengembalikan pokok pinjaman, dan mencatat biayanya ke dalam liquidity pool. Apa yang dilakukan ChatGPT? Dia dengan rapi memasang validasi require untuk sisa saldo di awal dan akhir transaksi, dengan polosnya berasumsi bahwa jika deltanya klop, sistem berarti kebal.

Tapi setan, seperti biasa, bersembunyi di detail pemanggilan fungsi callback uniswapV2Call (atau interface serupa pada protokol lain).

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
interface IUniswapV2Pair {
    function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
}
contract AIArbitrageBotHolder {
    address private immutable owner;
    bool private unlocked = true;
    // AI menganggap penghalang ini sudah cukup untuk mengamankan seluruh logika
    modifier lock() {
        require(unlocked, "LOCKED");
        unlocked = false;
        _;
        unlocked = true;
    }
    constructor() {
        owner = msg.sender;
    }
    // Metode "aman" versi AI
    function executeFlashLoan(address _pair, uint256 _amountA, bytes calldata _data) external lock {
        // Meminta likuiditas dari pool
        IUniswapV2Pair(_pair).swap(_amountA, 0, address(this), _data);
    }
    // Callback tempat pool mengembalikan kontrol eksekusi
    function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external {
        // Kesalahan fatal: validasi msg.sender sering dilewatkan AI demi "estetika" kode
        // Voila, kontrak sembarangan mana pun bisa masuk ke sini dan memicu loop reentrancy
        
        uint256 fee = amount0 * 3 / 997 + 1;
        uint256 repayment = amount0 + fee;
        // Logika arbitrase kustom buatan ChatGPT
        _executeInternalArbitrage(amount0);
        // Mengembalikan pinjaman ke pool
        // (Lupa bahwa state pool eksternal belum sepenuhnya difinalisasi)
        bool success = IERC20(msg.sender).transfer(msg.sender, repayment);
        require(success, "Repayment failed");
    }
    function _internalArbitrage(uint256 amount) internal {
        // Stub untuk logika swap yang rumit
    }
}

Kelihatan kan celahnya? AI memasang modifier lock pada entry point eksternal executeFlashLoan, tetapi benar-benar lupa bahwa fungsi callback uniswapV2Call harus diisolasi secara terpisah atau memvalidasi alamat pemanggil secara ketat (msg.sender). Penyerang dapat memanggil callback ini secara langsung dari kontrak mereka sendiri di tengah-tengah perhitungan sementara, ketika saldo kontrak pembungkus (wrapper) belum konsisten, lalu menyedot habis seluruh jaminannya. ChatGPT tidak akan pernah memperingatkanmu soal ini; dia cuma akan nyengir dengan emoji chat-nya dan bilang: "Kode sudah dioptimalkan untuk standar ERC."

Kasus #5: Kontrak Minting Batch ERC-721 yang Dioptimalkan dengan Caching

Pasar NFT modern menuntut gas yang murah. AI sangat memahami hal ini dan mulai "mengoptimalkan" kode dengan menerapkan struktur pemetaan pemilik kustom serta penghitung cache langsung di memori kontrak.

  • Metrik Audit: Vonis ChatGPT — Status Kerentanan Aktual
  • Konsumsi Gas: Sangat rendah (Gas Optimized) — Dicapai dengan mengorbankan isolasi state
  • Pola Penyimpanan: Menggunakan struktur lokal di memori — Rentan terhadap manipulasi via hook onERC721Received
  • Stabilitas: Lolos tes Hardhat standar — Crash saat menghadapi stack panggilan reentrancy yang dalam

Ketika kontrak mengirimkan NFT melalui safeMint, standar mewajibkan pemanggilan fungsi konfirmasi pada smart contract penerima. Jika kamu melakukan batch mint (misalnya 10 token sekaligus) dan memperbarui penghitung total saldo setelah pengiriman setiap token (atau menggunakan loop di dalam panggilan eksternal), penyerang akan membajak kontrol pada iterasi ketiga—ketika array token internal baru terisi separuh, sementara penghitung transaksi masih mengira proses baru saja dimulai. Hasil akhirnya? Duplikasi indeks dan pencetakan aset tak terbatas dari udara kosong.

Lalu apa yang harus kita lakukan, aku, kamu, dan seluruh industri ini?

Dengar, aku sendiri menulis kode setiap hari, tapi mempercayai model generatif secara buta di dunia crypto itu sama saja seperti main orak-arik nyawa dengan peluru aktif di semua silinder kecuali satu. Jika kamu menggunakan AI untuk menelurkan protokol, ingat tiga aturan mati ini:

  • Jangan pernah percaya modifier dari template. Kalau AI menempelkan tag nonReentrant, itu cuma berarti dia tahu kosakatanya, tapi tidak paham konteks asinkron dari blockchain spesifikmu.
  • Selalu validasi msg.sender di dalam fungsi callback. Interface apa pun seperti uniswapV2Call, onERC721Received, atau tokensReceived harus dilindungi lebih ketat daripada gerbang bunker militer.
  • Tulis fuzz test buatanmu sendiri pakai Foundry. Tidak ada unit test statis yang bisa mendeteksi reentrancy antar-fungsi sebaik jutaan eksekusi invarian acak.

Selagi saya ngoceh soal celah keamanan ini, satu pikiran mengganggu terus muncul di kepala saya: bukannya kita sendiri yang bikin pasar jadi terlalu santai? Kita udah terbiasa mendelegasikan tugas-tugas rutin ke AI code generation, lupa kalau blockchain itu sama sekali nggak mentoleransi error kompilasi logika. Di web dev tradisional, lu bisa aja rollback commit, deploy patch, terus minta maaf ke user gara-gara downtime lima menit. Di ekosistem EVM, bug lu bakal abadi nangkring di ledger yang immutable sebagai monumen kebodohan manusia dan kejenuhan buta algoritmik.

Yuk, kita tuntasin sisa pembahasannya dan bedah satu lagi vektor tersembunyi yang konsisten banget di-generate sama ChatGPT.

Kasus #6: Liquidity Aggregators dan Virtual Shares di Stablecoin Pools

Ini nih tren paling hits di kalangan developer DeFi—bikin custom swap pools dengan kalkulasi bobot token dinamis berbasis kurva constant-product. Neural network demen banget sama matematika kayak gini karena internet penuh sama terabyte open-source code dari Curve dan Uniswap v2. Modelnya tinggal comot rumus, sesuaikan sama kebutuhan lu, tambahin fungsi addLiquidity, terus dengan bangganya ngelaporin kalau produk siap dideploy.

Masalahnya, si model sama sekali nggak paham gimana cara kerja kalkulasi asinkron untuk virtual balance pas manggil token eksternal dengan fee mengambang (kayak USDT atau token deflasi custom yang pakai pajak transfer).

Coba deh perhatiin pola di bawah ini yang dianggap paling aman sentosa sama AI:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
interface ITokenWithFee {
    function transferFrom(address sender, address recipient, amount) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
}
contract AIWarpPool {
    mapping(address => uint256) public userShares;
    uint256 public totalShares;
    
    // AI mikir kalau kita cek balance dulu terus nulis ke mapping itu udah aman
    function deposit(address token, uint256 amount) external {
        uint256 balanceBefore = ITokenWithFee(token).balanceOf(address(this));
        
        // Pemanggilan transfer token eksternal
        bool success = ITokenWithFee(token).transferFrom(msg.sender, address(this), amount);
        require(success, "Transfer failed");
        
        uint256 balanceAfter = ITokenWithFee(token).balanceOf(address(this));
        uint256 realReceived = balanceAfter - balanceBefore;
        // Minting share berdasarkan dana yang beneran masuk
        uint256 shares = totalShares == 0 ? realReceived : (realReceived * totalShares) / balanceBefore;
        
        userShares[msg.sender] += shares;
        totalShares += shares;
    }
    function withdraw(uint256 shares) external {
        require(userShares[msg.sender] >= shares, "Not enough shares");
        
        uint256 amountToReturn = (shares * ITokenWithFee(msg.sender).balanceOf(address(this))) / totalShares;
        
        // Update state-nya dulu...
        userShares[msg.sender] -= shares;
        totalShares -= shares;
        // ...baru kirim dananya. Klasik banget, Checks-Effects-Interactions!
        // Tapi tunggu sebentar...
        (bool success, ) = msg.sender.call{value: 0}(""); // Pemanggilan transfer token eksternal bersyarat
        require(success, "Withdraw failed");
    }
}

Keliatan kan celahnya? AI emang patuh ngikutin urutan Checks-Effects-Interactions di dalam fungsi withdraw. Tapi dia benar-benar kelewatan fakta kalau kalkulasi amountToReturn bergantung banget ke balanceOf(address(this)) yang lagi aktif pas fungsi dipanggil. Kalau attacker bikin contract wrapper yang pas nerima kontrol (atau lewat callback token) malah manggil ulang withdraw sebelum totalShares global berubah di pool lain yang terhubung atau lewat cross-contract oracle call, dia bisa ngitung proporsi pakai nilai likuiditas yang udah basi.

Checklist Terakhir Buat Ngecek Kode Buatan AI

Berhenti deh ngulangin kesalahan yang sama sambil berharap AI lebih jago soal arsitektur sistem terdistribusi daripada lu. Sebelum nge-deploy kode hasil generate ke production atau bahkan testnet yang pakai duit beneran, pastikan lu udah ngecek poin-poin ini:

  • Isolasi semua fungsi callback seolah-olah di dalamnya ada hacker yang siap nembakin exploit.
  • Periksa setiap external call buat mastiin apakah fungsi itu bisa balikin kontrol ke contract pas invariant global belum sinkron.
  • Jangan pernah percaya buta sama modifier — modifier cuma ngamanin dari reentrancy langsung ke fungsi yang sama, tapi nggak berkutik ngadepin rantai cross-contract yang ribet.

FAQ

Model bahasa besar hanya mencocokkan pola statis dan gagal melacak perubahan status lintas kontrak yang asinkron, di mana invarian global belum tersinkronisasi sebelum eksekusi panggilan eksternal.

Pengembang wajib menerapkan pola checks-effects-interactions secara ketat di seluruh modul, mengisolasi fungsi callback internal, serta menjalankan fuzzing invarian otomatis menggunakan Foundry.

Model AI mengabaikan perhitungan saldo virtual dan pembaruan status prematur, sehingga kontrak berbahaya dapat masuk kembali ke dalam logika inti selama callback token perantara.
Oleg Filatov

As the Chief Technology Officer at EXMON Exchange, I focus on building secure, scalable crypto infrastructure and developing systems that protect user assets and privacy.

With over 15 years in cybersecurity, blockchain, and DevOps, I specialize in smart contract analysis, threat modeling, and secure system architecture.

At EXMON Academy, I share practical insights from real-world...

...

Sampaikan pemikiran Anda

Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib ditandai *