Summary
The TokenDivider contract's onERC721Received implementation allows direct NFT transfers without proper initialization, leading to permanent NFT locking.
Vulnerability Details
The contract accepts any NFT transfer through onERC721Received without validation:
function onERC721Received(
address,
address,
uint256,
bytes calldata
) external pure override returns (bytes4) {
return this.onERC721Received.selector;
}
Impact
Critical. This vulnerability allows:
NFTs to be transferred directly to the contract
No corresponding ERC20 tokens are minted
NFTs become permanently locked
Users can lose valuable NFTs through accidental transfers
POC
pragma solidity ^0.8.18;
import {Test, console} from "forge-std/Test.sol";
import {TokenDivider} from "../../src/TokenDivider.sol";
import {ERC721Mock} from "../mocks/ERC721Mock.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract TokenDividerReceiveTest is Test {
TokenDivider public divider;
ERC721Mock public legitimateNft;
address public constant ATTACKER = address(0x2);
uint256 public constant TOKEN_ID = 0;
function setUp() public {
divider = new TokenDivider();
legitimateNft = new ERC721Mock();
legitimateNft.mint(ATTACKER);
}
function test_UnauthorizedNFTTransfer() public {
console.log("\n=== Testing Unauthorized NFT Transfer Vulnerability ===");
console.log("Contract's onERC721Received function accepts any NFT without validation");
vm.startPrank(ATTACKER);
console.log("\nAttempting to transfer NFT without using divideNft function...");
console.log("Initial NFT owner:", legitimateNft.ownerOf(TOKEN_ID));
console.log("Target contract:", address(divider));
legitimateNft.safeTransferFrom(ATTACKER, address(divider), TOKEN_ID, "");
console.log("\nVULNERABILITY: NFT transfer succeeded without proper initialization!");
console.log("New NFT owner:", legitimateNft.ownerOf(TOKEN_ID));
console.log("No ERC20 tokens were minted");
console.log("NFT is now locked in contract without corresponding ERC20 tokens");
vm.stopPrank();
}
}
Output:
=== Testing Unauthorized NFT Transfer Vulnerability ===
Contract's onERC721Received function accepts any NFT without validation
Attempting to transfer NFT without using divideNft function...
Initial NFT owner: 0x0000000000000000000000000000000000000002
Target contract: 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
VULNERABILITY: NFT transfer succeeded without proper initialization!
New NFT owner: 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
No ERC20 tokens were minted
NFT is now locked in contract without corresponding ERC20 tokens
Tools Used
Recommendations
1. Add transfer validation in onERC721Received
bool private _isProcessingDivide;
modifier onlyDuringDivide() {
require(_isProcessingDivide, "Only accept transfers through divideNft");
_;
}
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external override onlyDuringDivide returns (bytes4) {
return this.onERC721Received.selector;
}
function divideNft(address nftAddress, uint256 tokenId, uint256 amount) external {
_isProcessingDivide = true;
_isProcessingDivide = false;
}