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

Gas Denial of Service via Inefficient Beneficiary Lookup

Summary

The onlyBeneficiaryWithIsInherited modifier in the InheritanceManager contract exhibits a gas-based Denial-of-Service (DoS) vulnerability. Specifically, when a non-beneficiary attempts to call a function protected by this modifier, the loop iterates through all beneficiaries, leading to extremely high gas consumption. This can make transactions prohibitively expensive or even unexecutable under network gas limits.

Vulnerability Details

Vulnerable Code:

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

modifier onlyBeneficiaryWithIsInherited() {
uint256 i = 0;
while (i < beneficiaries.length + 1) {
if (msg.sender == beneficiaries[i] && isInherited) {
break;
}
i++;
}
_;
}

The issue arises due to the linear search through the beneficiaries array. If msg.sender is not found within the list, the loop executes fully, consuming gas proportional to the number of stored beneficiaries.

Proof of Code:

Place the following code in InheritanceManagerTest.t.sol

function testGasDoS() public {
vm.startPrank(owner);
// Adding two beneficiaries
uint256 gasStartA = gasleft();
address user2 = makeAddr("user2");
address user3 = makeAddr("user3");
im.addBeneficiery(user2);
im.addBeneficiery(user3);
uint256 gasCostA = gasStartA - gasleft();
console.log("Gas cost for adding 2 beneficiaries:", gasCostA);
// Adding 1,000 beneficiaries
uint256 gasStartB = gasleft();
for (uint256 i = 0; i < 1000; i++) {
im.addBeneficiery(address(uint160(i + 1)));
}
uint256 gasCostB = gasStartB - gasleft();
console.log("Gas cost for adding 1000 beneficiaries:", gasCostB);
vm.stopPrank();
vm.warp(1 + 90 days);
vm.startPrank(address(2));
im.inherit();
uint256 gasStartE = gasleft();
im.appointTrustee(address(10));
uint256 gasCostE = gasStartE - gasleft();
console.log("Gas cost for calling appointTrustee with beneficiary (1st call):", gasCostE);
vm.stopPrank();
vm.startPrank(address(attacker));
im.inherit();
uint256 gasStartC = gasleft();
vm.expectRevert();
im.appointTrustee(address(10));
uint256 gasCostC = gasStartC - gasleft();
console.log("Gas cost for calling appointTrustee with non-beneficiary (2nd call):", gasCostC);
vm.stopPrank();
vm.startPrank(address(1000));
im.inherit();
uint256 gasStartD = gasleft();
im.appointTrustee(address(10));
uint256 gasCostD = gasStartD - gasleft();
console.log("Gas cost for calling appointTrustee with beneficiary (3rd call):", gasCostD);
vm.stopPrank();
}

Observations:

Action Gas Used
Adding 2 beneficiaries 100,341
Adding 1,000 beneficiaries 23,714,044
Calling InheritanceManager::appointTrustee as a beneficiary (1st call) 25,585
Calling InheritanceManager::appointTrustee as a non-beneficiary (2nd call) 631,872
Calling InheritanceManager::appointTrustee as a beneficiary (3rd call) 631,427

The third call (InheritanceManager::appointTrustee by a beneficiary) is prohibitively expensive, confirming the DoS vulnerability.

Impact

  • Denial-of-Service: If the list of beneficiaries is large, transactions can become too expensive to execute.

  • Financial Loss: Users may incur high gas costs attempting transactions.

Tools Used

  • Foundry

Recommendations

  1. Optimize the Loop: Instead of iterating through all beneficiaries, use a mapping(address => bool) to track beneficiary status. Example:

    mapping(address => bool) public isBeneficiary;
  2. Restrict Array Growth: Implement a reasonable cap on the number of beneficiaries.

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.