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

Dust Accumulation in `withdrawInheritedFunds` Function

Summary

In src/InheritanceManager.sol, the withdrawInheritedFunds function distributes the contract’s remaining assets equally among the beneficiaries. However, if the total asset amount is not perfectly divisible by the number of beneficiaries, residual dust (leftover assets) remains trapped in the contract, leading to fund lockup.

Vulnerability Details

In InheritanceManager.sol#L236, the withdrawInheritedFunds function calculates the share per beneficiary using integer division, which truncates any remainder.

Consider the following code snippet:

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);
}
}
}

When the contract holds assets that cannot be evenly divided, the leftover value (i.e., dust) remains locked because the calculation only distributes the integer portion.

PoC

The following Foundry test demonstrates the issue:

// 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 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();
}
function test_withdrawInheritedFundsEtherDust() public {
address user2 = makeAddr("user2");
address user3 = makeAddr("user3");
vm.startPrank(owner);
im.addBeneficiary(user1);
im.addBeneficiary(user2);
im.addBeneficiary(user3);
vm.stopPrank();
vm.warp(1);
vm.deal(address(im), 1e18); // 1 Ether (not a multiple of 3)
vm.warp(1 + 90 days);
vm.startPrank(user1);
im.inherit();
im.withdrawInheritedFunds(address(0));
vm.stopPrank();
// Check if dust remains in the contract
assertEq(1, address(im).balance); // 1 Wei remains trapped
}
}

Impact

  • Residual Dust: Small fractions of ETH or ERC20 tokens may remain stuck in the contract.

  • Fund Inefficiency: This leads to imperfect fund distribution, causing permanent lockup of unclaimed funds.

  • Gas Wastage: Beneficiaries may attempt repeated withdrawals to retrieve the dust unsuccessfully.

Tools Used

  • Manual code review

  • Foundry for Solidity testing

Recommendations

To prevent dust accumulation, track the remaining balance and assign all leftover assets to the last beneficiary:

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++) {
if (i == divisor - 1) {
// Give the last beneficiary the remaining dust
amountPerBeneficiary = ethAmountAvailable;
} else {
ethAmountAvailable -= amountPerBeneficiary;
}
address payable beneficiary = payable(beneficiaries[i]);
(bool success, ) = beneficiary.call{value: amountPerBeneficiary}("");
require(success, "Transfer failed");
}
} else {
uint256 assetAmountAvailable = IERC20(_asset).balanceOf(address(this));
uint256 amountPerBeneficiary = assetAmountAvailable / divisor;
for (uint256 i = 0; i < divisor; i++) {
if (i == divisor - 1) {
// Handle dust for ERC20 tokens
amountPerBeneficiary = assetAmountAvailable;
} else {
assetAmountAvailable -= amountPerBeneficiary;
}
IERC20(_asset).safeTransfer(beneficiaries[i], amountPerBeneficiary);
}
}
}

Although it is unfair to give the dust to a single beneficiary, this ensures that no assets remain trapped. This implementation guarantees that the contract fully distributes all available funds.

Updates

Lead Judging Commences

0xtimefliez Lead Judge 6 months ago
Submission Judgement Published
Validated
Assigned finding tags:

truncation of integers

Support

FAQs

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