Summary
The InheritanceManager contract does not prevent the same address from being added multiple times as a beneficiary. This vulnerability allows certain beneficiaries to receive a larger share of the inheritance than intended, breaking the contract's core assumption of equal fund distribution.
Vulnerability Details
The addBeneficiery function lacks a check to prevent adding duplicate beneficiary addresses:
function addBeneficiery(address _beneficiary) external onlyOwner {
beneficiaries.push(_beneficiary);
_setDeadline();
}
The withdrawInheritedFunds function distributes funds based on the length of the beneficiaries array:
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 {
}
}
When a beneficiary appears multiple times in the array, they will receive multiple shares of the inheritance, creating an unfair distribution.
Impact
This vulnerability directly impacts the fairness of inheritance distribution:
Beneficiaries added multiple times receive a proportionally larger share of the inheritance
Other legitimate beneficiaries receive less than their intended share
This could be exploited intentionally to favor certain beneficiaries
Violates the core contract functionality of equal distribution
The impact is rated as medium because:
It does not directly lead to fund loss for the contract
But it does lead to incorrect fund distribution
It breaks the core assumption of equal distribution
Tools Used
Manual code review and Foundry testing
Proof of Concept
Add the following test to InheritanceManager.t.sol
This test adds user1 twice. We calculate the proportion of funds by dividing the total funds by the number of beneficiaries (4 entries for 3 unique addresses).
We then assert that user1 gets twice the proportion while other users get one proportion each.
function test_withdrawInheritedFundsEtherDuplicateBeneficiary() public {
address user2 = makeAddr("user2");
address user3 = makeAddr("user3");
vm.startPrank(owner);
im.addBeneficiery(user1);
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();
im.withdrawInheritedFunds(address(0));
vm.stopPrank();
uint256 proportion = 9e18 / 4;
assertEq(proportion * 2, user1.balance);
assertEq(proportion, user2.balance);
assertEq(proportion, user3.balance);
}
Run the test
forge test --mt test_withdrawInheritedFundsEtherDuplicateBeneficiary
Recommendations
Implement a check to prevent duplicate beneficiary addresses:
function addBeneficiery(address _beneficiary) external onlyOwner {
for (uint256 i = 0; i < beneficiaries.length; i++) {
if (beneficiaries[i] == _beneficiary) {
revert("InheritanceManager: beneficiary already exists");
}
}
beneficiaries.push(_beneficiary);
_setDeadline();
}
Alternatively, implement a helper function for reusability:
function _beneficiariesContains(address _beneficiary) internal view returns (bool) {
for (uint256 i = 0; i < beneficiaries.length; i++) {
if (beneficiaries[i] == _beneficiary) {
return true;
}
}
return false;
}
function addBeneficiery(address _beneficiary) external onlyOwner {
require(!_beneficiariesContains(_beneficiary), "InheritanceManager: beneficiary already exists");
beneficiaries.push(_beneficiary);
_setDeadline();
}