Summary
The buyOutEstateNFT function lacks a check to verify if the specified _nftID corresponds to an existing NFT. This allows the function to process payments for non-existent NFTs, potentially affecting operations if the NFT is minted later, as the nftValue mapping might be overwritten or misinterpreted without proper validation.
Vulnerability Details
The vulnerable code is in the buyOutEstateNFT function:
solidity
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);
}
POC
pragma solidity 0.8.26;
import {Test, console} from "forge-std/Test.sol";
import {InheritanceManager} from "../src/InheritanceManager.sol";
import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol";
contract InheritanceManagerTest is Test {
InheritanceManager im;
ERC20Mock usdc;
ERC20Mock weth;
address owner = makeAddr("owner");
address user1 = makeAddr("user1");
function setUp() public {
vm.prank(owner);
im = new InheritanceManager();
usdc = new ERC20Mock();
weth = new ERC20Mock();
}
function test_buyOutEstateNFTForNonExistentNFT() public {
address user2 = makeAddr("user2");
address user3 = makeAddr("user3");
vm.warp(1);
vm.startPrank(owner);
im.addBeneficiery(user1);
im.addBeneficiery(user2);
im.addBeneficiery(user3);
im.createEstateNFT("our beach-house", 3e6, address(usdc));
vm.stopPrank();
usdc.mint(user3, 4e6);
vm.warp(1 + 90 days);
vm.startPrank(user3);
usdc.approve(address(im), 4e6);
im.inherit();
im.buyOutEstateNFT(10);
vm.stopPrank();
}
}
Issues:
-
No Existence Validation:
The function uses nftValue[_nftID] without checking if the NFT exists. If _nftID hasn’t been minted, nftValue[_nftID] defaults to 0, yet the function proceeds (though payments may be 0).
nft.burnEstate(_nftID) will revert if the NFT doesn’t exist (due to ERC721._burn), but funds may already be transferred.
-
Future Impact:
If a non-existent _nftID is "bought out" (with value = 0), a later minting of that ID could confuse the state, as nftValue[_nftID] might be set afterward, but the buyout already occurred.
-
Early Fund Transfer:
Impact
Funds at Risk: Payments for non-existent NFTs could occur if nftValue[_nftID] is non-zero (e.g., from a previous bug), with the burn reverting, leaving funds stuck.
State Confusion: Buying out a non-existent NFT could mislead users, and later minting the same ID might create ownership disputes or duplicate payments.
Low Severity: Currently mitigated by burnEstate reverting, but still a logic flaw.
Tools Used
Manual Review and foundry
Recommendations
Add an existence check before processing:
solidity
function buyOutEstateNFT(uint256 _nftID) external onlyBeneficiaryWithIsInherited {
require(nft.ownerOf(_nftID) != address(0), "NFT does not exist");
uint256 value = nftValue[_nftID];
require(value > 0, "NFT has no value");
uint256 divisor = beneficiaries.length - 1;
uint256 totalAmount = value;
IERC20(assetToPay).safeTransferFrom(msg.sender, address(this), totalAmount);
for (uint256 i = 0; i < beneficiaries.length; i++) {
if (msg.sender != beneficiaries[i]) {
IERC20(assetToPay).safeTransfer(beneficiaries[i], value / divisor);
}
}
nft.burnEstate(_nftID);
}