Pieces Protocol

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

Multiple ERC20 Tokens from the same address but different token IDs

Description:
The TokenDivider::divideNFT function splits NFTs into ERC20 tokens but does not account for the specific TokenID when generating the ERC20 token. Instead, it only considers the nftAddress of the ERC721 contract.

Impact:
For example: If someone mints another NFT from the same ERC721 contract where existing divided NFTs already exist, they could create a large number of ERC20 tokens (e.g., 1000 * the AMOUNT for the new NFT). This would allow them to claim all NFTs with fewer token amounts, as the generated ERC20 tokens are not uniquely associated with the specific TokenID. Consequently, the ERC20 tokens from different NFTs of the same contract are treated as interchangeable, leading to potential exploits.

Proof of Concept:

Updated ERC721Mock

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract ERC721Mock2 is ERC721 {
uint256 private _tokenIdCounter;
constructor() ERC721("MockToken", "MTK") {}
function mint(address to) public {
_safeMint(to, _tokenIdCounter);
_tokenIdCounter++;
}
}

Test Divider with 2 Users

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import {Test, console} from "forge-std/Test.sol";
import {DeployTokenDivider} from "script/DeployTokenDivider.s.sol";
import {TokenDivider} from "src/TokenDivider.sol";
import {ERC721Mock2} from "../mocks/ERC721Mock2.sol";
import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract TokenDividerTestToCompareERC20Token is Test {
DeployTokenDivider deployer;
TokenDivider tokenDivider;
ERC721Mock2 erc721Mock2;
address public USER = makeAddr("user");
address public USER2 = makeAddr("user2");
uint256 public constant STARTING_USER_BALANCE = 10e18;
uint256 public constant AMOUNT = 2e18;
uint256 public constant TOKEN_ID_1 = 0;
uint256 public constant TOKEN_ID_2 = 1;
function setUp() public {
deployer = new DeployTokenDivider();
tokenDivider = deployer.run();
erc721Mock2 = new ERC721Mock2();
erc721Mock2.mint(USER);
erc721Mock2.mint(USER2);
}
function testDivideNftProducesSameTokenAddress() public {
vm.startPrank(USER);
erc721Mock2.approve(address(tokenDivider), TOKEN_ID_1);
tokenDivider.divideNft(address(erc721Mock2), TOKEN_ID_1, AMOUNT);
vm.stopPrank();
vm.startPrank(USER2);
erc721Mock2.approve(address(tokenDivider), TOKEN_ID_2);
tokenDivider.divideNft(address(erc721Mock2), TOKEN_ID_2, AMOUNT);
vm.stopPrank();
address erc20Address1 = tokenDivider
.getErc20InfoFromNft(address(erc721Mock2))
.erc20Address;
address erc20Address2 = tokenDivider
.getErc20InfoFromNft(address(erc721Mock2))
.erc20Address;
assertEq(erc20Address1, erc20Address2);
console.log("ERC20 Address 1: ", erc20Address1);
console.log("ERC20 Address 2: ", erc20Address2);
}
}
Updates

Lead Judging Commences

fishy Lead Judge 8 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.