Pieces Protocol

First Flight #32
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: medium
Valid

Unauthorized NFT Locking Through Direct Transfers

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:

// In TokenDivider.sol
function onERC721Received(
address, /* operator */
address, /* from */
uint256, /* tokenId */
bytes calldata /* data */
) 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

// test/unit/TokenDividerReceiveTest.t.sol
// SPDX-License-Identifier: MIT
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));
// Direct transfer bypassing divideNft
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();
}
}
  • Run forge test --mc TokenDividerReceiveTest -vvv

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

  • Foundry

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;
// ... existing code ...
_isProcessingDivide = false;
}
Updates

Lead Judging Commences

fishy Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Direct NFT transfer

Support

FAQs

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