Summary
Early Return in InheritanceManager::buyOutEstateNFT Prevents NFT Burning, Enabling Infinite Buy-Outs
Vulnerability Details
The InheritanceManager::buyOutEstateNFT function contains an early return statement when the caller is identified as a beneficiary. This skips the critical nft.burnEstate(_nftID) call, leaving the NFT unburned and allowing repeated buy-outs. Additionally, the payment distribution logic is flawed, underpaying beneficiaries.
Impact
High Severity
The NFT’s intended "burn-after-purchase" mechanism is broken, rendering the system’s economic model nonfunctional user can repeatedly call InheritanceManager::buyOutEstateNFT on the same NFT.
Tools Used
Manual code review
Foundry test case (provided)
PoC
1 - The test shows user1, user2, and user3 each call InheritanceManager::buyOutEstateNFT(1), spending 20 USDC each.
2 - The NFT (ID 1) is never burned, as confirmed by the absence of NFTFactory::burnEstate calls in the InheritanceManager logic.
function test_buyOutEstateNFTMultiple() public {
address owner = makeAddr("owner");
address user1 = makeAddr("user1");
address user2 = makeAddr("user2");
address user3 = makeAddr("user3");
vm.startPrank(owner);
im.addBeneficiery(user1);
im.addBeneficiery(user2);
im.addBeneficiery(user3);
im.createEstateNFT("our beach-house", 20, address(usdc));
vm.stopPrank();
usdc.mint(user1, 20);
usdc.mint(user2, 20);
usdc.mint(user3, 20);
vm.warp(1 + 90 days);
vm.startPrank(user1);
usdc.approve(address(im), 20);
im.inherit();
im.buyOutEstateNFT(1);
vm.stopPrank();
vm.startPrank(user2);
usdc.approve(address(im), 20);
im.inherit();
im.buyOutEstateNFT(1);
vm.stopPrank();
vm.startPrank(user3);
usdc.approve(address(im), 20);
im.inherit();
im.buyOutEstateNFT(1);
vm.stopPrank();
}
Recommendations
Ensure the NFT is burned after processing payments:
Ensure the NFT is burned after processing payments:
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 {
IERC20(assetToPay).safeTransfer(beneficiaries[i], finalAmount / divisor);
}
}
nft.burnEstate(_nftID);
}