xanoutas/solidity-security-findings huggingface dataset:
User can freely choose the fighter's DNA in `redeemMintPass()`
Audit: 2024-02-ai-arena-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details



## Impact
User can freely set the DNA when redeeming mint passes such that the user has complete control over the choice of `element`, `weight` and `physicalAttributes` of the minted fighter.

(Note that also `iconsTypes` can be set freely by the user, which is probably by design, as well as `fighterTypes`, which on the other hand does seem to go against the [stated intention to make dendroids very rare and more difficult to obtain](https://docs.aiarena.io/gaming-competition/nft-makeup#:~:text=These%20are%20very%20rare%20and%20more%20difficult%20to%20obtain.).)

## Proof of Concept
`FighterFarm.redeemMintPass()` simply takes a `string[] calldata mintPassDnas` each element of which is then hashed as [`uint256(keccak256(abi.encode(mintPassDnas[i])))`](https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L254) and passed as the `dna` to each fighter created via `_createNewFighter()`.

In `_createNewFighter()` this `dna` is used to set [`(element, weight, newDna) = _createFighterBase(dna, fighterType)`](https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L500) which is [calculated as](https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L470-L472)
```solidity
uint256 element = dna % numElements[generation[fighterType]];
uint256 weight = dna % 31 + 65;
uint256 newDna = fighterType == 0 ? dna : uint256(fighterType);
```
[`numElements` is by default `3`](https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L110).
This is then [used to set the `FighterPhysicalAttributes`](https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L510-L515) via `AiArenaHelper.createPhysicalAttributes()`. If `fighterType == 0` and so `dendroidBool == true`, this always [returns the same result](https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/AiArenaHelper.sol#L94), otherwise the DNA may have an influence [in the following way](https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/AiArenaHelper.sol#L107-L109):
```solidity
uint256 rarityRank = (dna / attributeToDnaDivisor[attributes[i]]) % 100;
uint256 attributeIndex = dnaToIndex(generation, rarityRank, attributes[i]);
finalAttributeProbabilityIndexes[i] = attributeIndex;
```
which is iterated 6 times.
`attributeToDnaDivisor` are by default `[2, 3, 5, 7, 11, 13]` so over the 6 attributes the 6 `rarityRank` takes on at most (actually less) `2 * 3 * 5 * 7 * 11 * 13 * 100 = 3003000` different combinations.
Counting also the `element` and `weight` combinations we only need to brute force
Players can claim NRN Rewards without staking NRN which breaks protocol economic model
Audit: 2024-02-ai-arena-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

**Description:**

The `RankedBattle::claimNRN` function allows players to claim NRN rewards without first staking NRN, which violates the protocol invariant that gamers must stake NRN to win NRN rewards through Ranked Battles.

**Impact:**

The ability to claim NRN rewards without staking NRN undermines the economic model and fairness of the Protocol. It allows players to receive rewards without contributing to the staking pool, potentially leading to an imbalance in the distribution of rewards and undermining the incentive structure of the game

**Proof of Concept:**

The `RankedBattle::claimNRN` doesn't check whether a player has staked NRN before they are able to call claimNRN and mint NRN. You can see the function below :

```
function claimNRN() external {
    require(numRoundsClaimed[msg.sender] < roundId, "Already claimed NRNs for this period");
    uint256 claimableNRN = 0;
    uint256 nrnDistribution;
    uint32 lowerBound = numRoundsClaimed[msg.sender];
    for (uint32 currentRound = lowerBound; currentRound < roundId; currentRound++) {
        nrnDistribution = getNrnDistribution(currentRound);
        claimableNRN += (
            accumulatedPointsPerAddress[msg.sender][currentRound] * nrnDistribution
        ) / totalAccumulatedPoints[currentRound];
        numRoundsClaimed[msg.sender] += 1;
    }
    if (claimableNRN > 0) {
        amountClaimed[msg.sender] += claimableNRN;
        _neuronInstance.mint(msg.sender, claimableNRN);
        emit Claimed(msg.sender, claimableNRN);
    }
}
```

**Recommended Mitigation:**

To address this issue, you should add a check in the claimNRN function to ensure that the caller has staked NRN before they can claim NRN rewards. This can be done by checking the staked amount for the caller in the amountStaked mapping.
Here's how you can update the claimNRN function to include the staking check:

```diff

function claimNRN() external {
    require(numRoundsClaimed[msg.sender] < roundId, "Already claimed NRNs for this period");
+    require(amountStaked[msg.sender] > 0, "No stake found");  // Add this line to require staking
    uint256 claimableNRN = 0;
    uint256 nrnDistribution;
    uint32 lowerBound = numRoundsClaimed[msg.sender];
    for (uint32 currentRound = lowerBound; currentRound < roundId; currentRound++) {
        nrnDistribution = getNrnDistribution(currentRound);
        claimableNRN += (
            accumulatedPointsPerAddress[msg.sender][currentRound] * nrnDistribution
        ) / totalAccumulatedPoints[currentRound];
        numRoundsClaimed[msg.sender] += 1;
    }
    if (claimableNRN > 0) {
        amountClaimed[msg.sender] += claimableNRN;
        _neuronInstance.mint(msg.sender, claimableNRN);
        emit Claimed(msg.sender, claimableNRN);
    }
}

```

## Tools Used
Manual Review




## Assessed type

Other
Points are gained without staking
Audit: 2024-02-ai-arena-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details



## Impact
Points can be gained even when nothing is staked, with no risk of loss. These points earn the user claimable NRN tokens.

## Proof of Concept
If nothing is staked and the fighter wins then the number of points credited to it and its owner is [`points = stakingFactor[tokenId] * eloFactor`](https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L445), where `eloFactor > 0` is provided by the game server and [`stakingFactor[tokenId] = _getStakingFactor(tokenId, stakeAtRisk)`](https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L434). [`_getStakingFactor()` returns at least `1`](https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L530-L532).
Thus `points >= 1` always, and even an unstaked win will gain points.

If the unstaked fighter loses then either points are lost as normal, or
```solidity
bool success = _neuronInstance.transfer(_stakeAtRiskAddress, curStakeAtRisk);
if (success) {
    _stakeAtRiskInstance.updateAtRiskRecords(curStakeAtRisk, tokenId, fighterOwner);
    amountStaked[tokenId] -= curStakeAtRisk;
}
```
is executed, but [`curStakeAtRisk = (bpsLostPerLoss * (amountStaked[tokenId] + stakeAtRisk)) / 10**4`](https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L439) is `0` since nothing has been staked so nothing is transferred and nothing is lost.

These points can then be converted to NRN through [`claimNRN()`](https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L302).

This is thus a foolproof way to earn arbitrary amounts of NRN. Note also that since nothing is staked the fighter can be transferred freely and when it is [the new owner has a fresh voltage](https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L336) and battles can therefore be initiated without limit.

## Recommended Mitigation Steps
Let the range of `_getStakingFactor()` extend down to `0`.


## Assessed type

Context
User can transfer game items even if the game items are locked
Audit: 2024-02-ai-arena-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

Even if a game item is locked, anyone can transfer it because the `safeBatchTransferFrom()` is not overwritted to add the necessary check.

## Proof of Concept

The `GameItems` contract represents a collection of game items used in AI Arena. It inherits from the `ERC1155` and permits some operations of minting/transfers/burning.
We can see the AIArena team overwrite the `safeTransferFrom()` method to prevent transfers of items when they have the locked status with the `allGameItemAttributes[tokenId].transferable` set to false.

```solidity
    function safeTransferFrom(
        address from, 
        address to, 
        uint256 tokenId,
        uint256 amount,
        bytes memory data
    ) 
        public 
        override(ERC1155)
    {
        require(allGameItemAttributes[tokenId].transferable);
        super.safeTransferFrom(from, to, tokenId, amount, data);
    }
```

The issue is a locked item can be transfereable with `safeBatchTransferFrom()` even if `allGameItemAttributes[tokenId].transferable` is false because there is no override to add the check on the method:

```solidity
    function safeBatchTransferFrom(
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) public virtual override {
        require(
            from == _msgSender() || isApprovedForAll(from, _msgSender()),
            "ERC1155: caller is not token owner nor approved"
        );
        _safeBatchTransferFrom(from, to, ids, amounts, data);
    }
```

## Tools Used

Manual review

## Recommended Mitigation Steps

Overwritte the `safeBatchTransferFrom()` method to add this check:

```solidity
    require(allGameItemAttributes[tokenId].transferable);
```



## Assessed type

Token-Transfer
Locked game items can still be transferred, completely circumventing this core game item logic 
Audit: 2024-02-ai-arena-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/GameItems.sol#L126-L134


# Vulnerability details

## Impact
Specific game items can be locked/unlocked by the contract `owner`, with the intention of preventing transfers of these game items during periods when they are locked. This constraint is correctly added to the overwritten (inherited) ERC1155 function `safeTransferFrom`, which requires the item be unlocked in order to allow a transfer:

```solidity
    /// @dev Added a check to see if the game item is transferable.
    function safeTransferFrom(
        address from, 
        address to, 
        uint256 tokenId,
        uint256 amount,
        bytes memory data
    ) 
        public 
        override(ERC1155)
    {
        require(allGameItemAttributes[tokenId].transferable);
        super.safeTransferFrom(from, to, tokenId, amount, data);
    }
```
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/GameItems.sol#L290-L303

However the contract does **not** also overwrite the inherited ERC1155 function `safeBatchTransferFrom`, as it should. Therefore the entire locking/unlocking logic is rendered defunct, as locked items can be as freely transferred as unlocked items. This may have significant ramifications for the protocol, as core game item logic doesn't work as intended.

## Proof of Concept
Add the following test to the `GameItemsTest` test suite and run:
```shell
forge test --match-test testCircumventLockWithBatchTransfer -vvv
```

```solidity
    /// @notice Test cirumventing the lock by using a batch transfer
    function testCircumventLockWithBatchTransfer() public {
        _fundUserWith4kNeuronByTreasury(_ownerAddress);
        _gameItemsContract.mint(0, 1);
        console.log("make the game item non transferable");
        _gameItemsContract.adjustTransferability(0, false);     // make the game item non transferable
        (,, bool transferable,,,) = _gameItemsContract.allGameItemAttributes(0);
        assertEq(transferable, false);
        console.log("expect revert since the game item is non transferable");
        vm.expectRevert();       // expect revert since the game item is non transferable
        _gameItemsContract.safeTransferFrom(_ownerAddress, _DELEGATED_ADDRESS, 0, 1, "");
        console.log("the game item was not transferred as safe transfer lock check works as expected");
        assertEq(_gameItemsContract.balanceOf(_DELEGATED_ADDRESS, 0), 0);   // the game item was not transferred
        assertEq(_gameItemsContract.balanceOf(_ownerAddress, 0), 1);

        console.log("transfer the same locked item with a batch transfer instead");
        uint256[] memory ids = new uint256[](1);
        ids[0] = 0;
        uint256[] memory amounts = new uint256[](1);
        amounts[0] = 1;
        _gameItemsContract.safeBatchTransferFrom(_ownerAddress, _DELEGATED_ADDRESS, ids, amounts, "");
        console.log("th
It is not possible to create a new figther neither update it on new generations
Audit: 2024-02-ai-arena-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L85
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L110
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L470


# Vulnerability details

## Impact

No new fighters (both Champion and Dendroid fighter types) can be created after generation 0 due to a "Division or modulo by 0" revert on the `FighterFarm._createFighterBase` function. This private function is externally called by:

- `FighterFarm.reRoll`.
- `FighterFarm.claimFighters`.
- `FighterFarm.redeemMintPass`.
- `MergingPool.claimRewards`.

## Description

In the `FighterFarm` contract:

1. The `numElements` mapping maps a generation to a number of elements:

```solidity
/// @notice Mapping of number elements by generation.
mapping(uint8 => uint8) public numElements;
```

2. The constructor sets the elements of the generation 0 in `numElements`:

```solidity
/// @notice Sets the owner address, the delegated address.
/// @param ownerAddress Address of contract deployer.
/// @param delegatedAddress Address of delegated signer for messages.
/// @param treasuryAddress_ Community treasury address.
constructor(address ownerAddress, address delegatedAddress, address treasuryAddress_)
    ERC721("AI Arena Fighter", "FTR")
{
    _ownerAddress = ownerAddress;
    _delegatedAddress = delegatedAddress;
    treasuryAddress = treasuryAddress_;
    numElements[0] = 3; // @audit here
}
```

3. The contract has no `numElements` setter neither an assignment expression that sets `numElements` on future generations.

4. The `_createFighterBase` function calculates `element` of the given fighter type by computing the the modulo operation `mod(dna, x)`, being `x` the `numElements` of the current generation for the given fighter type:

```solidity
uint256 element = dna % numElements[generation[fighterType]];
```

5. This computation will revert due to a "Division or modulo by 0" for any generation greater than `0`.

## Proof Of Concept

Add the following tests in `FighterFarm.t.sol`:

```solidity
function testClaimFightersRevertForChampionWithGenerationGtZero() public {
    // Arrange
    uint8[2] memory numToMint = [1, 0]; // NB: fighter type is Champion
    bytes memory claimSignature = abi.encodePacked(
        hex"407c44926b6805cf9755a88022102a9cb21cde80a210bc3ad1db2880f6ea16fa4e1363e7817d5d87e4e64ba29d59aedfb64524620e2180f41ff82ca9edf942d01c"
    );
    string[] memory claimModelHashes = new string[](1);
    claimModelHashes[0] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m";

    string[] memory claimModelTypes = new string[](1);
    claimModelTypes[0] = "original";

    // NB: increment Champion generation from 0 to 1
    _fighterFarmContract.incrementGeneration(0);
    assertEq(_fighterFarmContract.generation(0), 1);

    // Act
    vm.expectRevert();
    _fighterFarmContract.claimFighters(numToMint, claimSignature, claimModelHashes, cl
Missing ERC1155:safeBatchTransferFrom override in GameItems allows AGI tokens to be traded while marked as non-transferable
Audit: 2024-02-ai-arena-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Due to a missing override for `ERC1155:safeBatchTransferFrom` in the `GameItems` contract, it is possible to transfer AGI game token items while they are marked as non-transferable via `allGameItemAttributes[tokenId].transferable`.

This can be abused to transfer or trade game items that are marked as non-tradeable by the `GameItems` contract owner.

## Detail
The [`ERC1155:safeBatchTransferFrom`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/ecd2ca2cd7cac116f7a37d0e474bbb3d7d5e1c4d/contracts/token/ERC1155/ERC1155.sol#L134) function inherited from the [codebase's OpenZeppelin `ERC1155` v4.7.3 import](https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/GameItems.sol#L5) is not overridden in the [`GameItems`](https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/GameItems.sol) contract. 

Only the [`ERC1155:safeTransferFrom`](https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/GameItems.sol#L291) transfer function is overridden in `GameItems`, which includes a check for the [`allGameItemAttributes[tokenId].transferable`](https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/GameItems.sol#L38) flag that prevents AGI tokens from being transferred while their respective `transferable` flag is set to `false`.

```solidity

//File: src/GameItems.sol

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";

contract GameItems is ERC1155 {

...

    struct GameItemAttributes {
        string name;
        bool finiteSupply;
        bool transferable;
        uint256 itemsRemaining;
        uint256 itemPrice;
        uint256 dailyAllowance;
    }  

...

    GameItemAttributes[] public allGameItemAttributes;

...

      function safeTransferFrom(
        address from, 
        address to, 
        uint256 tokenId,
        uint256 amount,
        bytes memory data
    ) 
        public 
        override(ERC1155)
    {
        require(allGameItemAttributes[tokenId].transferable);
        super.safeTransferFrom(from, to, tokenId, amount, data);
    }
}
```
It is therefore possible to transfer a currently non-transferable AGI token by directly calling `GameItems:safeBatchTransferFrom`, bypassing the `allGameItemAttributes[tokenId].transferable` check.

```solidity

//File: lib/openzeppelin-contracts/contracts/token/ERC1155/ERC1155.sol

    function safeBatchTransferFrom(
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) public virtual override {
        require(
            from == _msgSender() || isApprovedForAll(from, _msgSender()),
            "ERC1155: caller is not token owner nor approved"
        );
        _saf
Rounding Issue will cause curStakeAtRisk to be zero
Audit: 2024-02-ai-arena-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The curStakeAtRisk will get truncated to zero because of the rounding issue that occurs during the division.


## Proof of Concept
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

## Tools Used
Manual Inspection

## Recommended Mitigation Steps


## Assessed type

Decimal
Failure to penalize users in RankedBattle contract due to unsuccessful NRN transfer
Audit: 2024-02-ai-arena-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

The issue lies in the `RankedBattle` contract, specifically within the `_addResultPoints` function. When a fighter loses match without having any points or stake at risk for that round, the contract attempts to transfer NRN tokens (Neuron tokens) to the `_stakeAtRiskAddress`. However, if this transfer fails, the function quietly proceeds without penalizing the user. This behavior contradicts the intended game mechanics, where a losing fighter without points should have a portion of their staked NRNs moved to a stake-at-risk pool. As a result, fighters can avoid penalties for losing, disrupting the fairness and balance of the game.

## Proof of Concept

Preconditions:

1. A fighter with a token ID (tokenId) is registered in the `RankedBattle` contract.
2. The fighter has no points accumulated for the current round.
3. The fighter has no NRNs in the stake-at-risk pool.
4. The fighter engages in a battle and loses.

Steps to Reproduce:

1. The `_addResultPoints` function is invoked due to the fighter losing a battle.
2. The contract attempts to transfer NRNs to `_stakeAtRiskAddress`.
3. Assume the transfer fails (e.g., due to insufficient balance, ERC20 transfer function returning false). The success variable is set to false. The subsequent if (success) block is skipped.
4. As a result, no NRNs are moved to the stake-at-risk pool, and the fighter effectively faces no penalty for losing.

Outcome:

Users can exploit this by ensuring the transfer fails (e.g., by depleting NRN balance) to avoid penalties for losing, thereby gaining an unfair advantage.

## Tools Used

## Recommended Mitigation Steps

To mitigate the issue, modifying the contract to revert the transaction if the NRN transfer fails is recommended. This ensures that either the transfer is successful, or the entire transaction is rolled back, maintaining the game's integrity.


## Assessed type

Invalid Validation