code-423n4 / 2024-02-ai-arena-findings

4 stars 3 forks source link

Players can unfairly mint rare fighters #2033

Closed c4-bot-10 closed 8 months ago

c4-bot-10 commented 9 months ago

Lines of code

https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L212-L220 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L252-L260 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L322-L330

Vulnerability details

Description

In Ai Arena, players can create new fighters through redeemMintPass, claimFighters, and mintFromMergingPool functions, each of which requires providing a dna to _createNewFighter. However, the calculation of dna in these functions is not entirely random and can lead to unfair outcomes.

In both the mintFromMergingPool and claimFighters functions, dna is calculated based on msg.sender and the length of the fighters array. While msg.sender is predictable because it reflects the address of the entity that initiates the transaction also fighters length is predictable. Thus, attackers can potentially manipulate their msg.sender to influence the resulting dna.

function claimFighters(
    uint8[2] calldata numToMint,
    bytes calldata signature,
    string[] calldata modelHashes,
    string[] calldata modelTypes
) 
    external 
{
    // ...
    for (uint16 i = 0; i < totalToMint; i++) {
        _createNewFighter(
            msg.sender, 
            uint256(keccak256(abi.encode(msg.sender, fighters.length))),
            modelHashes[i], 
            modelTypes[i],
            i < numToMint[0] ? 0 : 1,
            0,
            [uint256(100), uint256(100)]
        );
    }
}
function mintFromMergingPool(
    address to, 
    string calldata modelHash, 
    string calldata modelType, 
    uint256[2] calldata customAttributes
) 
    public 
{
    require(msg.sender == _mergingPoolAddress);
    _createNewFighter(
        to, 
        uint256(keccak256(abi.encode(msg.sender, fighters.length))), 
        modelHash, 
        modelType,
        0,
        0,
        customAttributes
    );
}

Moreover, in the redeemMintPass function, dna calculation lacks randomness as the player provides a string to calculate dna, making the result entirely not random.

function redeemMintPass(
        uint256[] calldata mintpassIdsToBurn,
        uint8[] calldata fighterTypes,
        uint8[] calldata iconsTypes,
        string[] calldata mintPassDnas,
        string[] calldata modelHashes,
        string[] calldata modelTypes
    ) 
        external 
    {
    //Some code...
    _createNewFighter(
        msg.sender, 
        uint256(keccak256(abi.encode(mintPassDnas[i]))), 
        modelHashes[i], 
        modelTypes[i],
        fighterTypes[i],
        iconsTypes[i],
        [uint256(100), uint256(100)]
    );
    // Some code...
}

The restricted randomness stemming from the use of the modulo operation on dna for attributes such as weight, element, and physical attributes also contributes to predictability. This makes it simpler for players to exploit these predictable patterns when minting rare attributes.

function _createFighterBase(
    uint256 dna, 
    uint8 fighterType
) 
    private 
    view 
    returns (uint256, uint256, uint256) 
{
    uint256 element = dna % numElements[generation[fighterType]];
    uint256 weight = dna % 31 + 65;
    uint256 newDna = fighterType == 0 ? dna : uint256(fighterType);
    return (element, weight, newDna);
}
function createPhysicalAttributes(
    uint256 dna, 
    uint8 generation, 
    uint8 iconsType, 
    bool dendroidBool
) 
    external 
    view 
    returns (FighterOps.FighterPhysicalAttributes memory) 
{
    // Some code...
    uint256 rarityRank = (dna / attributeToDnaDivisor[attributes[i]]) % 100;
    uint256 attributeIndex = dnaToIndex(generation, rarityRank, attributes[i]);
    finalAttributeProbabilityIndexes[i] = attributeIndex;
    // Some code...
}

Players can mint a rare fighter each time they use redeemMintPass. However, for mintFromMergingPool and claimFighters, they need to wait until the length of the fighters array matches their prediction before minting a token.

Impact

Proof of Concept

In this POC, I focused on demonstrating the exploitation of the redeemMintPass function for simplicity and only exploit weight and element for simplicity (same for physical attributes) I assume rare attribute are this:

To attain these values, a player needs to pass a string that meets the following conditions:

  1. dna % numElemnts == 2
  2. dna % 31 == 5

To obtain this specific string, we can utilize foundry fuzzing.

function test_findString(string calldata str) public {
    uint256 num = uint256(keccak256(abi.encode(str)));
    // numElemnts -> 3
    if(num % 3 == 2 && num % 31 == 5){
        console.log(str);
        assert(false);
    }else{
        assert(true);
    }
}

By utilizing the output string, player can mint fighters with rare attributes.

Tools Used

Manual Review Chisel Foundry

Recommended Mitigation Steps

Get random numbers using Chainlink's VRF.

Assessed type

Other

c4-pre-sort commented 8 months ago

raymondfam marked the issue as sufficient quality report

c4-pre-sort commented 8 months ago

raymondfam marked the issue as duplicate of #53

c4-judge commented 8 months ago

HickupHH3 changed the severity to 3 (High Risk)

c4-judge commented 8 months ago

HickupHH3 marked the issue as satisfactory

c4-judge commented 8 months ago

HickupHH3 changed the severity to 2 (Med Risk)

c4-judge commented 8 months ago

HickupHH3 marked the issue as duplicate of #376