Description: In the InheritanceManager::removeBeneficiary
function, a beneficiary is intended to be removed from the list, but the removal is not performed correctly. Using delete beneficiaries[indexToRemove]
only clears the value at that position but does not shift or remove the index itself. Since beneficiaries
is an array
of addresses, this results in an address(0)
entry at that position, keeping the array size unchanged but introducing inconsistencies in the list of valid beneficiaries.
function removeBeneficiary(address _beneficiary) external onlyOwner {
uint256 indexToRemove = _getBeneficiaryIndex(_beneficiary);
@> delete beneficiaries[indexToRemove];
}
Impact: In the InheritanceManager::withdrawInheritedFunds
function, this issue affects the contract in two different ways, potentially causing a significant loss of funds if large amounts are managed and a denial of service (DoS)
that, while not resulting in fund loss, partially blocks the function’s execution.
Since the beneficiary is not correctly removed, the total number of beneficiaries does not decrease, causing the funds to be divided into more parts than they should be. As a result, each beneficiary receives less than their actual entitlement.
In the ETH transfer section, when a beneficiary is removed, their position in the array is replaced with address(0). The call{value:} function does not revert when sending funds to 0x0, meaning the funds will be transferred and permanently lost with no possibility of recovery.
For ERC-20 tokens, most token contracts do not allow transfers to address(0), causing the transaction to revert. While this prevents fund loss, it triggers a DoS in this part of the function, blocking the asset distribution until the issue is manually resolved.
function test_removeBeneficiariesETH() public {
address emergencyWallet = makeAddr("emergencyWallet");
address alice = makeAddr("alice");
address bob = makeAddr("bob");
address charlie = makeAddr("charlie");
uint256 ethAmount = 1_000_000e18;
vm.deal(address(im), ethAmount);
uint256 ethBalanceImBefore = address(im).balance;
assertEq(ethBalanceImBefore, ethAmount, "Incorrect initial ETH balance");
vm.startPrank(owner);
im.addBeneficiery(emergencyWallet);
im.addBeneficiery(alice);
im.addBeneficiery(bob);
im.addBeneficiery(charlie);
im.removeBeneficiary(bob);
vm.stopPrank();
vm.warp(block.timestamp + 90 days);
vm.startPrank(charlie);
im.inherit();
im.withdrawInheritedFunds(address(0));
uint256 emergencyWalletBal = emergencyWallet.balance;
uint256 aliceBal = alice.balance;
uint256 bobBal = bob.balance;
uint256 charlieBal = charlie.balance;
console.log("EmergencyWallet ETH Bal...", emergencyWalletBal);
console.log("Alice ETH Bal.............", aliceBal);
console.log("Bob ETH Bal...............", bobBal);
console.log("Charlie ETH Bal...........", charlieBal);
uint256 amountXBeneficiary = ethAmount / 3;
console.log("---------------------------");
console.log("Correct Amount............", amountXBeneficiary);
vm.stopPrank();
}
Logs:
EmergencyWallet ETH Bal... 250000000000000000000000
Alice ETH Bal............. 250000000000000000000000
Bob ETH Bal............... 0
Charlie ETH Bal........... 250000000000000000000000
---------------------------
Correct Amount............ 333333333333333333333333
Proof of Concept 2: In this second test, we demonstrate how the ERC-20 token
transfer section reverts. Since the transfers are executed within a for
loop, if one of the transactions fails, none of the beneficiaries will receive their share of the funds.
im.addBeneficiery(emergencyWallet);
im.addBeneficiery(alice);
im.addBeneficiery(bob);
im.addBeneficiery(charlie);
im.removeBeneficiary(bob);
vm.stopPrank();
vm.warp(block.timestamp + 90 days);
vm.startPrank(charlie);
im.inherit();
im.withdrawInheritedFunds(address(usdc));
vm.stopPrank();
Log:
│ ├─ [1076] ERC20Mock::transfer(0x0000000000000000000000000000000000000000, 250000000000 [2.5e11])
│ │ └─ ← [Revert] ERC20InvalidReceiver(0x0000000000000000000000000000000000000000)
Tools Used
Manual review
Foundry for Testing
Recommended Mitigation: We recommend making the following changes to the InheritanceManager::removeBeneficiary
function.
function removeBeneficiary(address _beneficiary) external onlyOwner {
uint256 indexToRemove = _getBeneficiaryIndex(_beneficiary);
+ uint256 lastIdx = beneficiaries.length - 1;
+ if (indexToRemove < lastIdx) {
+ beneficiaries[indexToRemove] = beneficiaries[lastIdx];
+ }
+ beneficiaries.pop();
- delete beneficiaries[indexToRemove];
}