The removeBeneficiary function in InheritanceManager.sol has a flaw that causes funds to be lost. When a beneficiary is removed, their slot in the beneficiaries array is set to address(0) without shortening the array. Later, when withdrawInheritedFunds distributes funds, it sends a portion to address(0), effectively burning those assets.
How the Exploit Works
When a beneficiary is removed, the array retains its length, and the removed slot becomes address(0). The withdrawInheritedFunds function then splits the contract’s balance among all array entries, including address(0), causing a portion of the funds to be unrecoverable.
Step-by-Step Exploit Execution
Setup:
The contract is deployed with two beneficiaries: beneficiary1 and beneficiary2.
The owner calls removeBeneficiary(beneficiary1), setting its slot to address(0).
Inheritance:
After 90 days of inactivity, beneficiary2 calls inherit() to take ownership.
Fund Withdrawal:
The contract receives 1 ETH (e.g., via vm.deal).
beneficiary2 calls withdrawInheritedFunds(address(0)) to withdraw ETH.
The function splits 1 ETH between two “beneficiaries” (including address(0)):
address(0) gets 0.5 ETH (lost).
beneficiary2 gets 0.5 ETH.
Result:
Half the funds (0.5 ETH) are burned, and beneficiary2 receives only half of what they should.
Funds (e.g., ETH) are sent to address(0) and lost forever, reducing the amount beneficiaries receive.
Root Cause: The removeBeneficiary function doesn’t properly manage the array, leaving address(0) as a “valid” beneficiary that receives funds.
slither, my own scanner, AI and foundry
Proof of Concept
This bug results in permanent loss of assets, undermining the contract’s purpose of fairly distributing funds to beneficiaries. The more beneficiaries removed, the worse the loss becomes.
Issue
The removeBeneficiary function sets a beneficiary’s slot to address(0) without reducing the array length. This causes withdrawInheritedFunds to mistakenly send funds to address(0), resulting in lost assets.
Fix
Update removeBeneficiary to properly remove entries by swapping the target with the last element and reducing the array size:
Explanation
Change: Replaced setting the slot to address(0) with a swap-and-pop approach.
Why It Works: Swapping the target beneficiary with the last element and calling pop() removes the entry cleanly, keeping the array free of invalid addresses.
Best Practice: When removing items from dynamic arrays, adjust the length to maintain data integrity and prevent unintended behavior.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.