Summary
Flawed Loop Logic in buyOutEstateNFT Skips Payments to Most Beneficiaries, Enabling Caller to Retain Funds
Vulnerability Details
The buyOutEstateNFT function contains an early return when encountering the caller’s address in the beneficiaries array. This skips payments to all beneficiaries listed after the caller that acquiring the NFT.
Impact
High Severity
Theft of Beneficiary Funds: Only beneficiaries positioned before the caller in the array receive payments; others are ignored.
Tools Used
Manual code review
Foundry test case (provided)
PoC
function test_OnlyUserinFrontIsPayed() public {
address owner = makeAddr("owner");
address user1 = makeAddr("user1");
address user2 = makeAddr("user2");
address user3 = makeAddr("user3");
address user3 = makeAddr("user4");
vm.startPrank(owner);
im.addBeneficiery(user1);
im.addBeneficiery(user2);
im.addBeneficiery(user3);
im.addBeneficiery(user4);
im.createEstateNFT("our beach-house", 23, address(usdc));
vm.stopPrank();
usdc.mint(user2, 15);
vm.warp(1 + 90 days);
vm.startPrank(user2);
usdc.approve(address(im), 20);
im.inherit();
im.buyOutEstateNFT(1);
vm.stopPrank();
console.log("User1 will have:", usdc.balanceOf(user1));
console.log("User2 will have:", usdc.balanceOf(user2));
console.log("User3 will have:", usdc.balanceOf(user3));
console.log("User4 will have:", usdc.balanceOf(user4));
}
Log
Ran 1 test for test/Mytest.t.sol:Testcontract
[PASS] test_OnlyUserinFrontIsPayed() (gas: 438575)
Logs:
User1 will have: 3
User2 will have: 0
User3 will have: 0
User4 will have: 0
Recommendations
Remove Early Return and Track Caller
function buyOutEstateNFT(uint256 _nftID) external onlyBeneficiaryWithIsInherited {
uint256 value = nftValue[_nftID]
uint256 divisor = beneficiaries.length;
uint256 multiplier = beneficiaries.length - 1;
uint256 finalAmount = (value / divisor) * multiplier;
IERC20(assetToPay).safeTransferFrom(msg.sender, address(this), finalAmount);
for (uint256 i = 0; i < beneficiaries.length; i++) {
- if (msg.sender == beneficiaries[i]) {
- return;
- } else {
+ if (beneficiaries[i] != msg.sender) {
IERC20(assetToPay).safeTransfer(beneficiaries[i], finalAmount / divisor);
}
}
nft.burnEstate(_nftID);
}