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

Denial of Service via External Calls in a for loop resulting in funds lockup

Summary

The withdrawInheritedFunds function performs external calls (call and safeTransfer) in a for loop to send funds to each beneficiary. This can result in funds lockup or excessive gas usage if the beneficiary grows too large.

Vulnerability Details

The withdrawInheritedFunds function performs external calls (call and safeTransfer) in a for loop to send funds to each beneficiary. If any of these calls fails (If a beneficiary is a contract that contain receive or fallback function), the entire transaction reverts, preventing all other beneficiaries from receiving their funds

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++) {
address payable beneficiary = payable(beneficiaries[i]);
(bool success,) = beneficiary.call{value: amountPerBeneficiary}("");
require(success, "something went wrong");
}
} else {
uint256 assetAmountAvailable = IERC20(_asset).balanceOf(address(this));
uint256 amountPerBeneficiary = assetAmountAvailable / divisor;
for (uint256 i = 0; i < divisor; i++) {
IERC20(_asset).safeTransfer(beneficiaries[i], amountPerBeneficiary);
}
}

The following PoC add a contract that has a receive function that causes the revert to prove that the entire transaction revert, blocking payments to all other beneficiaries.

function test_DenialOfService_withdrawInheritedFunds() public {
address user2 = makeAddr("user2");
address user3 = makeAddr("user3");
vm.startPrank(owner);
MaliciousBeneficiary malicious = new MaliciousBeneficiary();
address maliciousAddr = address(malicious);
im.addBeneficiery(maliciousAddr);
im.addBeneficiery(user1);
im.addBeneficiery(user2);
im.addBeneficiery(user3);
vm.stopPrank();
vm.warp(1);
vm.deal(address(im), 9e18);
vm.warp(1 + 90 days);
vm.startPrank(user1);
im.inherit();
vm.expectRevert();
im.withdrawInheritedFunds(address(0));
vm.stopPrank();
assertEq(0, user1.balance);
assertEq(0, user2.balance);
assertEq(0, user3.balance);
assertEq(0, maliciousAddr.balance);
}
}
contract MaliciousBeneficiary {
receive() external payable {
require(false, "Reverting to cause DoS");
}
}

Impact

If a single beneficiary causes a failure, the entire withdrawal process is blocked such that no beneficiary can receive their funds.

Tools Used

Manual and confirmed by slither.

Recommendations

Use mapping to avoid using external call in for loop where each user claim their fund individually

mapping(address => uint256) public pendingWithdrawals;
function withdrawInheritedFunds(address _asset) external {
require(isInherited, "Not yet inherited");
uint256 divisor = beneficiaries.length;
if (_asset == address(0)) {
uint256 ethAmountAvailable = address(this).balance;
uint256 amountPerBeneficiary = ethAmountAvailable / divisor;
for (uint256 i = 0; i < divisor; i++) {
pendingWithdrawals[beneficiaries[i]] += amountPerBeneficiary;
}
} else {
uint256 assetAmountAvailable = IERC20(_asset).balanceOf(address(this));
uint256 amountPerBeneficiary = assetAmountAvailable / divisor;
for (uint256 i = 0; i < divisor; i++) {
pendingWithdrawals[beneficiaries[i]] += amountPerBeneficiary;
}
}
}
function claimFunds() external {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0, "Nothing to withdraw");
pendingWithdrawals[msg.sender] = 0;
(bool success,) = msg.sender.call{value: amount}("");
require(success, "Withdrawal failed");
}
Updates

Lead Judging Commences

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.