Summary
The withdrawInheritedFunds
function in the InheritanceManager contract uses a push pattern for distributing funds to beneficiaries. If one beneficiary's transfer reverts (e.g., due to a malicious contract or gas limit), all subsequent transfers will fail, potentially blocking the distribution process.
Vulnerability Details
The withdrawInheritedFunds
function distributes funds to all beneficiaries in a single transaction:
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
);
}
}
}
This implementation has several issues:
It uses a push pattern where the contract sends funds to each beneficiary
If any beneficiary's transfer fails (e.g., due to a malicious contract that reverts), the entire function reverts
This could block all beneficiaries from receiving their funds
A malicious beneficiary could intentionally block the distribution
Impact
This vulnerability could lead to:
Permanent blocking of fund distribution if one beneficiary is malicious
Denial of service for legitimate beneficiaries
Funds being locked in the contract indefinitely
Requiring contract redeployment or complex workarounds
The severity is medium because while it could lead to temporary fund locking, it doesn't directly expose funds to theft and requires a malicious beneficiary.
Tools Used
Manual code review
Recommendations
Implement a pull pattern instead of a push pattern for fund distribution:
mapping(address => uint256) public beneficiaryShares;
bool public sharesCalculated;
function calculateShares(address _asset) external {
require(isInherited, "Not inherited yet");
require(!sharesCalculated, "Shares already calculated");
if (_asset == address(0)) {
uint256 ethAmount = address(this).balance / beneficiaries.length;
for (uint256 i = 0; i < beneficiaries.length; i++) {
beneficiaryShares[beneficiaries[i]] = ethAmount;
}
} else {
uint256 tokenAmount = IERC20(_asset).balanceOf(address(this)) / beneficiaries.length;
for (uint256 i = 0; i < beneficiaries.length; i++) {
beneficiaryShares[beneficiaries[i]] = tokenAmount;
}
}
sharesCalculated = true;
emit SharesCalculated(_asset);
}
function withdrawShare(address _asset) external {
require(sharesCalculated, "Shares not calculated yet");
uint256 share = beneficiaryShares[msg.sender];
require(share > 0, "No share available or already withdrawn");
beneficiaryShares[msg.sender] = 0;
if (_asset == address(0)) {
payable(msg.sender).transfer(share);
} else {
IERC20(_asset).safeTransfer(msg.sender, share);
}
emit ShareWithdrawn(msg.sender, _asset, share);
}
This pattern allows each beneficiary to withdraw their funds independently, preventing a single malicious beneficiary from blocking the entire distribution process.