xanoutas/solidity-security-findings huggingface dataset:
`claimFighters()` is susceptible to signature malleability issue
Audit: 2024-02-ai-arena-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L206
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/Verification.sol#L21-L40


# Vulnerability details

## Description
The issue lies in the `require()` call made in the in-scope `FighterFarm.sol` to `Verification.sol` out-of-scope. But the logic error retains in the in-scope function.

The elliptic curve (SECP256k1) used in **ECDSA**, as with all elliptic curves, is symmetrical about the x-axis. Therefore, for every (r,s,v), there exists another coordinate that returns the same valid result. This means that two signatures exist for one signer at any one point on the curve, allowing attackers to compute the second, valid signature without requiring the signer’s private key.
Since the curve is reflected over the x-axis, there are two valid public keys for each value of r.

## Impact
An attacker can exploit signature malleability by providing an equivalent signature that still verifies successfully. Specifically, if (r, s) is a valid signature for a message, then (r, -s) (where -s is the additive inverse of s modulo the order of the curve) is also a valid signature for the same message.

## Proof of Concept
```javascript
assembly {
            /*
            First 32 bytes stores the length of the signature

            add(sig, 32) = pointer of sig + 32
            effectively, skips first 32 bytes of signature

            mload(p) loads next 32 bytes starting at the memory address p into memory
            */

            // first 32 bytes, after the length prefix
            r := mload(add(signature, 32))
            // second 32 bytes
            s := mload(add(signature, 64))
            // final byte (first byte of the next 32 bytes)
            v := byte(0, mload(add(signature, 96)))
            //no checks for s value in lowerhalf order
            //v value to be 27
            
        }
        bytes memory prefix = "\x19Ethereum Signed Message:\n32";
        bytes32 prefixedHash = keccak256(abi.encodePacked(prefix, msgHash));
        return ecrecover(prefixedHash, v, r, s) == signer;
}
```

## Tools Used
Manual Review

## Recommended Mitigation Steps
When implementing ecrecover directly, the value of s should be restricted to only allow the lower levels.

This issue usually arises when ecrecover is used directly; therefore, OpenZeppelin’s ECDSA library (version 4.7.3 or greater) should be used.

The [Cyfrin's article](https://www.cyfrin.io/blog/replay-attack-in-ethereum)  can be of great use to mitigate this issue.


## Assessed type

Other
Lock Persistence Issue for Fighters Losing All Stake in a Round
Audit: 2024-02-ai-arena-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L270-L290


# Vulnerability details

## Impact
Players face an unintended gameplay interruption as their fighter is kept locked when all their stake goes at risk, with the only resolution being a counterintuitive zero-value unstake action that sidelines them for the remainder of the round.

## Proof of Concept
1. **Initial Gameplay Scenario**:
   - Players engage their fighters in battles, staking NRN tokens The game's mechanics include the risk of losing staked amounts based on battle performances.

2. **Complete Loss of Stake**:
   - During a particularly challenging round, a player’s fighter can lose all its staked NRN tokens, which leaves their fighter locked.

3. **Discovery of Lock Mechanism**:
   - The player realizes that the fighter remains locked even after the battle's conclusion, with no automatic mechanism for unlocking at the round's end or the start of a new round. This lock prevents any form transfers on the fighter.

4. **Forced Unstaking for Unlocking**:
   - To unlock the fighter, the player must figure out to perform an unstake operation with a value of 0 NRN. This action, while effectively unlocking the fighter, paradoxically disqualifies them from staking again in the current round due to game rules prohibiting staking after unstaking.

5. **Involuntary Downtime**:
   - The consequence is a forced waiting period until the next round begins. This downtime is not by player choice but rather an enforced penalty due to the locking mechanism's unintended persistence, significantly detracting from the game's engagement and continuity.

6. **Strategic Implications and Frustration**:
   - This situation places players at a strategic disadvantage, unable to leverage their assets effectively and missing out on potential recoveries or strategic plays in the ongoing round. The frustration from this experience could impact player retention and the overall perception of the game's fairness.

### Coded Poc
**Output**
```python
Ran 1 test for test/RankedBattle.t - Copy .sol:RankedBattleTest
[PASS] testFighterRemainsUnlockedAfterLosingAllStake() (gas: 1739133)
Logs:
  Fighter lost all his stake: 0
  Fighter staked amount: 0, fighter locked status: true

Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.05ms

Ran 1 test suite in 3.05ms: 1 tests passed, 0 failed, 0 skipped (1 total tests)
```
**Code**
```javascript
function testFighterRemainsUnlockedAfterLosingAllStake() public {
    address player = vm.addr(3);
    _mintFromMergingPool(player);
    uint8 tokenId = 0;
    _fundUserWith4kNeuronByTreasury(player);

    // We set bpsLostPerLoss to 50% so that we can easily simulate a player that lost all his stake.
    _rankedBattleContract.setBpsLostPerLoss(5000);

    // Stake a dust amount.
    vm.prank(player);
    _rankedBattleContract.stakeNRN(4_000 * 10**8, tokenId);
    assertEq(_rankedBattleContra
Players can unfairly mint rare fighters
Audit: 2024-02-ai-arena-findings
Severity: MEDIUM

# 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.

``` solidity
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)]
        );
    }
}
```
``` solidity
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.
``` solidity
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 cont
Fighter rare attributes can be deterministically rerolled
Audit: 2024-02-ai-arena-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol?plain=1#L379


# Vulnerability details

## Impact
Users can arbitrarily control the rarityRank value, a value designed to determine what attribute a fighter is to retrieve, including the fighter receives a rare attribute. Since the user can control the rarityRank via dna, a user can call FighterFarm.reRoll() to re-roll their physical attributes to receive rare attributes. 


## Proof of Concept
To begin, DNA from re-rolls are calculated via the fighter owner's address. A fighter owner can transfer the fighter to any arbitrary address they control. Below we can see how the msg.sender acts as an input to generate a dna:

```solidity
uint256 dna = uint256(keccak256(abi.encode(msg.sender, tokenId, numRerolls[tokenId])));
```

Next, we'll consider that in order to determine the rarity of attributes, AI Arena utilizes the following formula in the AiArenaHelper contract:

```solidity
function createPhysicalAttributes(
    ...
    // AUDIT: the rarityRank is calculated based on the modulus of dna. Since dna is a user-controlled value, the hacker also controls the value of the rarityRank. Note that rarityRank is a poor name here as it should be randomNumber.
    uint256 rarityRank = (dna / attributeToDnaDivisor[attributes[i]]) % 100;
    uint256 attributeIndex = dnaToIndex(generation, rarityRank, attributes[i]);
    ...
}

function dnaToIndex(uint256 generation, uint256 rarityRank, string memory attribute) 
    public 
    view 
    returns (uint256 attributeProbabilityIndex) 
{
    uint8[] memory attrProbabilities = getAttributeProbabilities(generation, attribute);
    
    uint256 cumProb = 0;
    uint256 attrProbabilitiesLength = attrProbabilities.length;
    for (uint8 i = 0; i < attrProbabilitiesLength; i++) {
        cumProb += attrProbabilities[I];
        // The rarityRank is used to generate attribute probabilities. By controlling the rarityRank, the fighter owner can also control the value of attributeProbabilityIndex
        if (cumProb >= rarityRank) {
            attributeProbabilityIndex = i + 1;
            break;
        }
    }
    return attributeProbabilityIndex;
}
```

As you can see above, the dna's modulus is used to calculate what's called the rarityRank, a pseudorandom number. Although as mentioned previously, it is not a pseudo random number as the user can control the dna value via the fighter owner address. 

In anticipation of a re-roll, the fighter owner can generate a series of random addresses via Vanity [ETH](https://vanity-eth.tk) and determine off-chain which address will give them the best dna to receive rare attributes. 

This can result in the following scenario:

1. Owner receives fighter.
2. Owner offchain generates an address that gives them a dna that best gives them the best attribute probability indexes. 
3. Owner transfers fighter to generated address.
4. Owner sends NRN to the generated address.
5. Owner from the gener
Players can claim items beyond their daily allowance in `GameItems` contract
Audit: 2024-02-ai-arena-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/GameItems.sol#L147-L176
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/GameItems.sol#L291-L303


# Vulnerability details

## Description
In the `GameItems` contract, players have the ability to mint game items using the `mint` function, with the constraint that they can only mint up to their daily allowance amount.

``` solidity
function mint(uint256 tokenId, uint256 quantity) external {
    // Some code ...
    if (dailyAllowanceReplenishTime[msg.sender][tokenId] <= block.timestamp) {
        _replenishDailyAllowance(tokenId);
    }
    allowanceRemaining[msg.sender][tokenId] -= quantity;
    if (allGameItemAttributes[tokenId].finiteSupply) {
        allGameItemAttributes[tokenId].itemsRemaining -= quantity;
    }
    _mint(msg.sender, tokenId, quantity, bytes("random"));
    emit BoughtItem(msg.sender, tokenId, quantity);
}
```
But players can bypass the daily allowance limit by minting tokens from another account and transferring them to their own account, enabling them to claim items beyond their daily allowance.

## Impact
* Players can bypass daily allowance restrictions.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Check daily allowance in transfer functions.
``` solidity
function safeTransferFrom(
    address from, 
    address to, 
    uint256 tokenId,
    uint256 amount,
    bytes memory data
) 
    public 
    override(ERC1155)
{
    require(allGameItemAttributes[tokenId].transferable);
    require(getAllowanceRemaining(to, tokenId) > 0);
    super.safeTransferFrom(from, to, tokenId, amount, data);
}
```


## Assessed type

Other
Fighter rarity can be bruteforced to better results than other players
Audit: 2024-02-ai-arena-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/AiArenaHelper.sol#L107


# Vulnerability details

## Impact
The function `AiArenaHelper::createPhysicalAttributes` is responsible for calculating the rarity of the fighter's physical attributes. 

[AiArenaHelper.sol#L83-L121](https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/AiArenaHelper.sol#L83-L121)
```solidity
    function createPhysicalAttributes(
        uint256 dna, 
        uint8 generation, 
        uint8 iconsType, 
        bool dendroidBool
    ) 
        external 
        view 
        returns (FighterOps.FighterPhysicalAttributes memory) 
    {
        if (dendroidBool) {
            return FighterOps.FighterPhysicalAttributes(99, 99, 99, 99, 99, 99);
        } else {
            uint256[] memory finalAttributeProbabilityIndexes = new uint[](attributes.length);

            uint256 attributesLength = attributes.length;
            for (uint8 i = 0; i < attributesLength; i++) {
                if (
                  i == 0 && iconsType == 2 || // Custom icons head (beta helmet)
                  i == 1 && iconsType > 0 || // Custom icons eyes (red diamond)
                  i == 4 && iconsType == 3 // Custom icons hands (bowling ball)
                ) {
                    finalAttributeProbabilityIndexes[i] = 50;
                } else {
                    uint256 rarityRank = (dna / attributeToDnaDivisor[attributes[i]]) % 100;
                    uint256 attributeIndex = dnaToIndex(generation, rarityRank, attributes[i]);
                    finalAttributeProbabilityIndexes[i] = attributeIndex;
                }
            }
            return FighterOps.FighterPhysicalAttributes(
                finalAttributeProbabilityIndexes[0],
                finalAttributeProbabilityIndexes[1],
                finalAttributeProbabilityIndexes[2],
                finalAttributeProbabilityIndexes[3],
                finalAttributeProbabilityIndexes[4],
                finalAttributeProbabilityIndexes[5]
            );
        }
    }
```

In line 107, we can see that rarity is calculated according to *dna*, and *dna* is equal to the address of the fighter's owner.

With this design, users can write a script to generate random addresses until reaching an address that corresponds to a rarity well above the average of the users, gaining an advantage on selling this fighters.

## Proof of Concept
Create new addresses and calculate the rarity using the `createPhysicalAttributes` function, repeat until you find a satisfactory rarity.

When you find it, you can create the fighter using the `MerginPool::claimRewards` or the `FighterFarm::redeemMintPass` functions.

## Tools Used
vscode

## Recommended Mitigation Steps
Add more invariants to the rarity calculation, using factors that cannot be bruteforced by users.



## Assessed type

Other
Custom attribute parameter in `mintFromMergingPool` allows minting fighters outside designated range
Audit: 2024-02-ai-arena-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L502-L506
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L322-L330


# Vulnerability details

## Description
Due to the `_createFighterBase` function, the `element` and `weight` attributes must be within specific ranges:
- `element` should be in the range [0, 2].
- `weight` should be in the range [65, 95].

``` solidity
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);
}
```

In the `mintFromMergingPool` function, there's a possibility for players to mint a fighter with attributes outside the designated range. This is because `mintFromMergingPool` utilizes a custom attribute parameter, unlike `claimFighters` and `redeemMintPass`, which rely on a `100` flag.

``` solidity 
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
    );
}
```

In the `_createNewFighter` function, the `weight` and `element` attributes are not constrained to specific ranges.

``` solidity
function _createNewFighter(
    address to, 
    uint256 dna, 
    string memory modelHash,
    string memory modelType, 
    uint8 fighterType,
    uint8 iconsType,
    uint256[2] memory customAttributes
) 
    private 
{  
    // Some code ...
    else {
        element = customAttributes[0];
        weight = customAttributes[1];
        newDna = dna;
    }
    // Some code ...
}
```

## Impact
* Player can mint fighter with attrbutes out of standard range.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Bound `weight` and `element` attributes within a specific range, similar to the `_createFighterBase` function.


## Assessed type

Other
Tokens/fighters may earn NRN rewards after being unstaked
Audit: 2024-02-ai-arena-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/RankedBattle.sol#L342
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/StakeAtRisk.sol#L104


# Vulnerability details

## Impact
A fighter/token may be unstaked while it has a non-zero stakeAtRisk amount allowing the owner of the token to still receive rewards based on their stakeAtRisk amount.

Additionally, a malicious user may transfer the token to an address that has amountLost = 0 pn the StakeAtRisk contract which would cause the updateBattleRecord function to revert if the token wins due to an underflow in StakeAtRisk:reclaimNRN.

## Proof of Concept
When a token/fighter loses a battle and their points are 0, a portion of the stake on the token is sent to the StakeAtRisk contract. The token owner may unstake from RankedBattle to receive the remainder of the stake and make the token transferable in the round again.

The updateBattleRecord may still be called for tokens that have been unstaked for the round resulting in either (1) increase in points and potentially reclaiming stakeAtRisk if winning or (2) decrease in points if the token has gained points from wins after unstaking.

The test code below shows that the batlle record may be updated for a token that is not staked but has stakeAtRisk:

```soldity
function testUpdateBattleRecordEvenIfNotStaked() public {
        address player = vm.addr(3);
        _mintFromMergingPool(player);
        uint8 tokenId = 0;
        _fundUserWith4kNeuronByTreasury(player);
        vm.prank(player);
        _rankedBattleContract.stakeNRN(3_000 * 10 ** 18, 0);
        assertEq(_rankedBattleContract.amountStaked(0), 3_000 * 10 ** 18);
        uint256 curStakeAtRisk = (_rankedBattleContract.bpsLostPerLoss() * _rankedBattleContract.amountStaked(0)) / 10**4;
        vm.prank(address(_GAME_SERVER_ADDRESS));
        _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true);
        (,, uint256 losses) = _rankedBattleContract.fighterBattleRecord(tokenId);
        assertEq(losses, 1);
        assertEq(_stakeAtRiskContract.getStakeAtRisk(tokenId), curStakeAtRisk);

        // Mint a fighter to the second user so that they can lose a battle and have stakeAtRisk to prevent underflow in StakeAtRisk:reclaimNRN
        address player2 = vm.addr(4);
        _mintFromMergingPool(player2);
        _fundUserWith4kNeuronByTreasury(player2);
        vm.prank(player2);
        _rankedBattleContract.stakeNRN(3_000 * 10 ** 18, 1);
        assertEq(_rankedBattleContract.amountStaked(1), 3_000 * 10 ** 18);
        curStakeAtRisk = (_rankedBattleContract.bpsLostPerLoss() * _rankedBattleContract.amountStaked(1)) / 10**4;
        vm.prank(address(_GAME_SERVER_ADDRESS));
        _rankedBattleContract.updateBattleRecord(1, 50, 2, 1500, true);
        (,, losses) = _rankedBattleContract.fighterBattleRecord(1);
        assertEq(losses, 1);
        assertEq(_stakeAtRiskContract.getStakeAtRisk(1), curStakeAtRisk);


        // Unstake figher 0 a