Skip to content
This repository has been archived by the owner on Jun 2, 2024. It is now read-only.

Falconhoof - First founder receives less token allocation than expected #277

Closed
sherlock-admin2 opened this issue Dec 1, 2023 · 0 comments
Labels
Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label High A valid High severity issue Reward A payout will be made for this issue

Comments

@sherlock-admin2
Copy link
Contributor

sherlock-admin2 commented Dec 1, 2023

Falconhoof

high

First founder receives less token allocation than expected

See Token.sol here
See Token.sol::_getNextTokenId() here
See Token.sol::_isForFounder() here

Summary

If reservedUntilTokenId is set to any three digit number (e.g. 100); the first founder in the founders array loses 1% of token supply during their vesting period.

Vulnerability Detail

Token.sol::_addFounders() takes in two parameters; _foundersand reservedUntilTokenId. The function loops through each founder in the _founders array assigning tokens based on percentages due.

For each founder the starting point for assigning their tokens is reservedUntilTokenId:

        uint256 baseTokenId = reservedUntilTokenId;

For all except the first token checked in _getNextTokenId, see here, the input token ID is modulated.
This means that _getNextTokenId returns an unmodulated token ID from the tokenRecipient for all except the very first token checked; unmodulated meaning a value between0 - 99.

This returned token is then assigned to that user in the tokenRecipient mapping in _addFounders

         tokenRecipient[baseTokenId] = newFounder;

During the minting phase; assignments in the tokenRecipient mapping are checked to determine, via `Token.sol::_isForFounder()', whether a token can be used for an auction or should be minted to a founder; see here.

The problem with allocating the first token to the initial founder arises because the check in _getNextTokenId does not include modulation. Consequently, this leads to the function returning false for the condition:

         while (tokenRecipient[_tokenId].wallet != address(0)) {

This allows any provided value to be directly returned and stored in tokenRecipient. This behavior does not present a problem for token IDs that are single or double-digit numbers. H
owever, if this value has three digits i.e. reservedUntilTokenId = 100; it will be stored unchanged in the tokenRecipient mapping.

Impact

The first founder in the _founders array loses a significant amount of voting power and tokens. It will also throw the voting dynamics of the DAO into disarray.

Using as an example reservedUntilTokenId = 100 and the first founder allocation percentage of 1 ; the first founder should be entitled to 1% of all tokens minted from 100 onwards within their vesting period, e.g. Token IDs 100, 200, 300, 400, etc. However they would receive nothing.

Code Snippet

Proof of Concept

Add the following function to NounsBuilderTest.sol:

    function deployWithCustomFoundersAndReserveToken(
        address[] memory _wallets,
        uint256[] memory _percents,
        uint256[] memory _vestExpirys,
        uint256 _reservedUntilTokenId
    ) internal virtual {
        setFounderParams(_wallets, _percents, _vestExpirys);

        setMockTokenParamsWithReserve(_reservedUntilTokenId);

        setMockAuctionParams();

        setMockGovParams();

        deploy(foundersArr, tokenParams, auctionParams, govParams);

        setMockMetadata();
    }

Add the following test to Token.t.sol and run:

    function test_firstFounderAllocationLoss() public {

        uint256[] memory percentages = new uint256[](1);
        address[] memory founders = new address[](percentages.length);
        uint256[] memory vesting = new uint256[](percentages.length);

        for (uint256 i = 0; i < percentages.length; i++) {

            // Set founders[0] percentage to 1%
            percentages[i] = 1;
            founders[i] = founder;
            vesting[i] = 4 weeks;
        }

        uint256 reservedUntilTokenId = 100;

        vm.prank(founder);
        deployWithCustomFoundersAndReserveToken(founders, percentages, vesting, reservedUntilTokenId);

        // unpause mints first token for auction
        // it should be 101 as 100 should be minted for founder
        vm.prank(founder);
        auction.unpause();

        // assert first auction token is 100
        (uint256 tokenId, , , , , ) = auction.auction();
        assertEq(tokenId, 100);

        // Loop 500 times; we expect founder to get 1% i.e. 5 tokens
        for (uint256 i = 0; i < 500; i++) {
            vm.prank(address(auction));
            token.mint();
        }

        // Assert founder receives no tokens out of first 500 mints
        uint256 founderBalance = token.balanceOf(founder);
        assertEq(founderBalance, 0);
    }

Tool used

Foundry
Manual Review

Recommendation

In _getNextTokenId, modulate _tokenId before entering the while loop:

           function _getNextTokenId(uint256 _tokenId) internal view returns (uint256) {
                 unchecked {
+                      _tokenId = _tokenId % 100; 
                       while (tokenRecipient[_tokenId].wallet != address(0)) {
-                            _tokenId = (++_tokenId) % 100;
+                            _tokenId = ++_tokenId;
                        }
                        return _tokenId;
                 }
           }

Duplicate of #42

@github-actions github-actions bot closed this as completed Dec 6, 2023
@github-actions github-actions bot added High A valid High severity issue Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label labels Dec 6, 2023
@sherlock-admin sherlock-admin changed the title Nutty Rouge Toad - First founder receives less token allocation than expected Falconhoof - First founder receives less token allocation than expected Dec 13, 2023
@sherlock-admin sherlock-admin added the Reward A payout will be made for this issue label Dec 13, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label High A valid High severity issue Reward A payout will be made for this issue
Projects
None yet
Development

No branches or pull requests

2 participants