sherlock-audit/2023-04-footium-judging

juancito - Increasing `_maxGenerationId` allows extra minting of academy players on previous seasons

sherlock-admin opened this issue · 5 comments

juancito

medium

Increasing _maxGenerationId allows extra minting of academy players on previous seasons

Summary

_maxGenerationId is defined in the Footium Academy docs as:

maxGenerationId: The maximum integer generationId that can be used to mint players. This is the number of players that can be minted per cohort.

Due to a lack of checks, if maxGenerationId is increased, it is possible to mint extra players.

Vulnerability Detail

_validateMintingParams allows minting on previous seasons:

    if (SeasonID.unwrap(seasonId) <= 0) {
        revert InvalidSeasonId(seasonId);
    }

    if (SeasonID.unwrap(seasonId) > currentSeasonId) {
        revert PlayerTooYoung(seasonId);
    }

mintPlayers doesn't enforce a limit of minted players per season either:

    for (uint256 i = 0; i < generationIds.length; ) {
        generationId = generationIds[i];

        if (generationId > _maxGenerationId) {
            revert GenerationIDTooHigh(generationId, _maxGenerationId);
        }

        if (redeemed[seasonId][clubId][generationId]) {
            revert PlayerAlreadyRedeemed(generationId);
        }

        redeemed[seasonId][clubId][generationId] = true;

        uint256 playerId = _footiumPlayer.safeMint(
            _footiumClub.clubToEscrow(clubId)
        );

        emit AcademyPlayerMinted(seasonId, clubId, generationId, playerId);

        unchecked {
            i++;
        }
    }

Those two factors combined are the reason for the bug.

Add this test to FootiumAcademy.test.ts to prove how extra players can be minted on previous seasons if _maxGenerationId is increased (it doesn't only affect the current season).

    it.only("mints extra players for past seasons when maxGenerationId is increased", async () => {
        // Set season 1
        await expect(academy.changeCurrentSeasonId(1))
            .to.emit(academy, "ChangedCurrentSeasonId")
            .withArgs(1);

        const maxGenerationId = 3; // @audit max 3 players
        await expect(academy.changeMaxGenerationId(maxGenerationId))
            .to.emit(academy, "ChangedMaxGenerationId")
            .withArgs(maxGenerationId);

        //const token = [clubId = 7, division = 4]
        const clubDivProof = clubDivsTree.getHexProof(
            hashClubDivisionInputs([7, 4])
        );

        const divisionTier = 4;
        const generationIds = [0, 1, 2]; // @audit mint 3 players
        const ethAmount = ethers.BigNumber.from(divisionFees[0]).mul(
            generationIds.length
        );

        await expect(
            academy
                .connect(addr1)
                .mintPlayers(
                    1,
                    7,
                    divisionTier,
                    generationIds,
                    clubDivProof,
                    {
                        value: ethAmount,
                    }
                )
        ).to.not.reverted;

        // Advance to season 2
        await expect(academy.changeCurrentSeasonId(2))
            .to.emit(academy, "ChangedCurrentSeasonId")
            .withArgs(2);

        // Increase the generation length to 6
        // It should only affect the current generation, but it affect the past ones in fact as well
        const newMaxGenerationId = 6; // @audit max 6 players
        await expect(academy.changeMaxGenerationId(newMaxGenerationId))
            .to.emit(academy, "ChangedMaxGenerationId")
            .withArgs(newMaxGenerationId);

        const extraGenerationIds = [3, 4, 5]; // @audit mint 3 more players

        await expect(
            academy
                .connect(addr1)
                .mintPlayers(
                    1, // @audit mint for the previous season
                    7,
                    divisionTier,
                    extraGenerationIds, // @audit extra players
                    clubDivProof,
                    {
                        value: ethAmount,
                    }
                )
        ).to.not.reverted;

        // It was possible to mint three extra players on the previous season
    });

Impact

Extra academy players can be minted bypassing the expected limitations.

Code Snippet

https://github.com/sherlock-audit/2023-04-footium/blob/main/footium-eth-shareable/contracts/FootiumAcademy.sol#L249-L255

Tool used

Manual Review

Recommendation

Define a _maxGenerationId limit per season and check it accordingly.

Duplicate of #319

Escalate for 10 USDC

This issue is a duplicate of #319, not of #152.

I'd also like to request to consider this report as the main one as it provides a coded POC demonstrating its validity, as well as addressing the impact and validity correctly as the former primary issue.

Duplicate of #319, not #152

#319 issue: changeMaxGenerationId allows to mint tokens from older generations retroactively
This issue: Increasing _maxGenerationId allows extra minting of academy players on previous seasons

On the other hand #152: Clubs can mint +1 players more than maxGenerationId
I have already created an issue for that one on #273: One extra academy player can be minted per season due to mischeck in mintPlayers

Same impact

#319 issue: Minting of X tokens where X is the number of old seasons
This issue title: ...allows extra minting of academy players on previous seasons
This issue impact: Extra academy players can be minted bypassing the expected limitations.

Same root cause

#319 issue:

changeMaxGenerationId is meant to allow more minting for the current generation, however, because of the fact that merkleProofs do not validate for the current season, whenever the limit will be raised, the limit will allow to mint players from older seasons

This issue shows it with the coded POC:

        // Increase the generation length to 6
        // It should only affect the current generation, but it affect the past ones in fact as well
        const newMaxGenerationId = 6; // @audit max 6 players
        await expect(academy.changeMaxGenerationId(newMaxGenerationId))
            .to.emit(academy, "ChangedMaxGenerationId")
            .withArgs(newMaxGenerationId);

And also provides further explanation of the root cause of this with:

_validateMintingParams allows minting on previous seasons
mintPlayers doesn't enforce a limit of minted players per season either:

Same mitigation

#319 issue:

Enforce minting exclusively for the current season or change validation to validate the generations and seasons being minted

This issue:

Define a _maxGenerationId limit per season and check it accordingly.

Escalate for 10 USDC

This issue is a duplicate of #319, not of #152.

I'd also like to request to consider this report as the main one as it provides a coded POC demonstrating its validity, as well as addressing the impact and validity correctly as the former primary issue.

Duplicate of #319, not #152

#319 issue: changeMaxGenerationId allows to mint tokens from older generations retroactively
This issue: Increasing _maxGenerationId allows extra minting of academy players on previous seasons

On the other hand #152: Clubs can mint +1 players more than maxGenerationId
I have already created an issue for that one on #273: One extra academy player can be minted per season due to mischeck in mintPlayers

Same impact

#319 issue: Minting of X tokens where X is the number of old seasons
This issue title: ...allows extra minting of academy players on previous seasons
This issue impact: Extra academy players can be minted bypassing the expected limitations.

Same root cause

#319 issue:

changeMaxGenerationId is meant to allow more minting for the current generation, however, because of the fact that merkleProofs do not validate for the current season, whenever the limit will be raised, the limit will allow to mint players from older seasons

This issue shows it with the coded POC:

        // Increase the generation length to 6
        // It should only affect the current generation, but it affect the past ones in fact as well
        const newMaxGenerationId = 6; // @audit max 6 players
        await expect(academy.changeMaxGenerationId(newMaxGenerationId))
            .to.emit(academy, "ChangedMaxGenerationId")
            .withArgs(newMaxGenerationId);

And also provides further explanation of the root cause of this with:

_validateMintingParams allows minting on previous seasons
mintPlayers doesn't enforce a limit of minted players per season either:

Same mitigation

#319 issue:

Enforce minting exclusively for the current season or change validation to validate the generations and seasons being minted

This issue:

Define a _maxGenerationId limit per season and check it accordingly.

You've created a valid escalation for 10 USDC!

To remove the escalation from consideration: Delete your comment.

You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final.

Valid duplicate of #319

Escalation accepted

Valid duplicate of #319

Escalation accepted

Valid duplicate #319

This issue's escalations have been accepted!

Contestants' payouts and scores will be updated according to the changes made on this issue.