Platform: Code4rena
Start Date: 09/02/2024
Pot Size: $60,500 USDC
Total HM: 17
Participants: 283
Period: 12 days
Judge:
Id: 328
League: ETH
Rank: 163/283
Findings: 2
Award: $10.69
π Selected for report: 0
π Solo Findings: 0
π Selected for report: ktg
Also found by: 0xCiphky, 0xDetermination, 0xRiO, 0xWallSecurity, 0xlemon, 0xvj, AlexCzm, BARW, Blank_Space, Draiakoo, FloatingPragma, Giorgio, Matue, McToady, MrPotatoMagic, Silvermist, SpicyMeatball, Tendency, Topmark, Tumelo_Crypto, _eperezok, agadzhalov, ahmedaghadi, alexxander, aslanbek, cats, d3e4, denzi_, dutra, evmboi32, fnanni, givn, handsomegiraffe, haxatron, immeas, juancito, ke1caM, kiqo, klau5, krikolkk, niser93, peter, petro_1912, pkqs90, rspadi, sl1, stakog, visualbits, vnavascues, yotov721
1.876 USDC - $1.88
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/MergingPool.sol#L142 https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L504
MergingPool
can pass any amount of weight
custom attribute to _createNewFighter
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/MergingPool.sol#L142 https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L504
Players are able to divert part of their points to MergingPool
and potentially earn a new fighter NFT. There's withdrawal mechanism for a player to claim his prize figther NFT by calling claimRewards()
. A player who claims his prize can easily pass any number for the weight
custom attribute if he passes element
custom attribute less than 100
to bypass validation in FighterFarm._createNewFighter()
.
A player who claims his prize can easily pass customAttribute
= [99
, type(uint256).max
]. Passing element
of 99
would bypass the validation if (customAttributes[0] == 100)
executing the else
logic and set the weight
to his desirable amount.
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L499
if (customAttributes[0] == 100) { (element, weight, newDna) = _createFighterBase(dna, fighterType); } else { // it will eneter here element = customAttributes[0]; weight = customAttributes[1]; // any number up to type(uint256).max newDna = dna; }
function testClaimRewardsForWinnerWithAnyWeight() public { address playerOne = makeAddr("playerOne"); address playerTwo = makeAddr("playerTwo"); _mintFromMergingPool(playerOne); _mintFromMergingPool(playerTwo); assertEq(_fighterFarmContract.ownerOf(0), playerOne); assertEq(_fighterFarmContract.ownerOf(1), playerTwo); uint256[] memory _winners = new uint256[](2); _winners[0] = 0; _winners[1] = 1; // winners of roundId 0 are picked _mergingPoolContract.pickWinner(_winners); assertEq(_mergingPoolContract.isSelectionComplete(0), true); assertEq(_mergingPoolContract.winnerAddresses(0, 0) == playerOne, true); assertEq(_mergingPoolContract.winnerAddresses(0, 1) == playerTwo, true); string[] memory _modelURIs = new string[](2); _modelURIs[0] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; _modelURIs[1] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; string[] memory _modelTypes = new string[](2); _modelTypes[0] = "original"; _modelTypes[1] = "original"; uint256[2][] memory _customAttributes = new uint256[2][](2); _customAttributes[0][0] = uint256(1); // Claimer can pass any value up to type(uint256).max _customAttributes[0][1] = UINT256_MAX; _customAttributes[1][0] = uint256(1); _customAttributes[1][1] = UINT256_MAX; // claimRewards can be executed by any user vm.prank(playerOne); // playerOne calaims his reward _mergingPoolContract.claimRewards(_modelURIs, _modelTypes, _customAttributes); // Get the first of the claimed NFTs and checks its weight is type(uint256).max uint256 indexOfClaimedNft = 2; (address a ,uint256[6] memory b, uint256 weight, uint256 element,string memory c,string memory d,uint16 e) = _fighterFarmContract.getAllFighterInfo(indexOfClaimedNft); assertEq(UINT256_MAX, weight); assertEq(_fighterFarmContract.ownerOf(indexOfClaimedNft), playerOne); }
How to run:
Copy and paste the test into MergingPool.t.sol
and execute forge test --match-test testClaimRewardsForWinnerWithAnyWeight -vvv
Player can gain unfair advantege over other players with fighters with less weight.
Implement validation in _createNewFighter
for checking maximum of weight
custom attribute.
Invalid Validation
#0 - c4-pre-sort
2024-02-24T09:03:43Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-24T09:03:53Z
raymondfam marked the issue as duplicate of #226
#2 - c4-judge
2024-03-11T10:28:27Z
HickupHH3 marked the issue as satisfactory
π Selected for report: givn
Also found by: 0x11singh99, 0xAkira, 0xBinChook, 0xDetermination, 0xMosh, 0xStriker, 0xmystery, 14si2o_Flint, 7ashraf, Aamir, AlexCzm, BARW, Bauchibred, BenasVol, BigVeezus, Blank_Space, Bube, DarkTower, DeFiHackLabs, EagleSecurity, KmanOfficial, Krace, McToady, MrPotatoMagic, PetarTolev, Rolezn, SHA_256, SpicyMeatball, Tekken, Timenov, ZanyBonzy, agadzhalov, alexzoid, boredpukar, btk, cartlex_, dimulski, forkforkdog, haxatron, immeas, jesjupyter, juancito, kartik_giri_47538, klau5, lsaudit, merlinboii, nuthan2x, offside0011, oualidpro, peter, radev_sw, rekxor, rspadi, shaflow2, shaka, swizz, vnavascues, yotov721, yovchev_yoan
8.8123 USDC - $8.81
attributeProbabilities
is updated twicehttps://github.com/code-423n4/2024-02-ai-arena/blob/main/src/AiArenaHelper.sol#L47
In constructor attributeProbabilities
are updated twice. Once by calling addAttributeProbabilities(0, probabilities)
and once by implementing for loop.
constructor(uint8[][] memory probabilities) { _ownerAddress = msg.sender; // Initialize the probabilities for each attribute -> addAttributeProbabilities(0, probabilities); // first update is here uint256 attributesLength = attributes.length; -> for (uint8 i = 0; i < attributesLength; i++) { // second update is here attributeProbabilities[0][attributes[i]] = probabilities[i]; -> attributeToDnaDivisor[attributes[i]] = defaultAttributeDivisor[i]; // this is missed in addAttributeProbabilities() } }
Update attributeProbabilities
only once by calling addAttributeProbabilities(0, probabilities);
and most likely also implementing attributeToDnaDivisor[attributes[i]] = defaultAttributeDivisor[i]
in addAttributeProbabilities()
attributesLength
is uint256
and i
is uint8
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/AiArenaHelper.sol#L73
attributesLength
is from type uint256
and i
is from type uint
8.
function addAttributeDivisor(uint8[] memory attributeDivisors) external { require(msg.sender == _ownerAddress); require(attributeDivisors.length == attributes.length); -> uint256 attributesLength = attributes.length; -> for (uint8 i = 0; i < attributesLength; i++) { attributeToDnaDivisor[attributes[i]] = attributeDivisors[i]; } }
function testArr() public view { uint256 l = 256; vm.expectRevert() for(uint8 i=0; i < l; i++) {} }
In case attributesLength
is bigger than 255
the method will revert
Keep attributesLength
and i
from the same type or use casting.
generation
is not consistenthttps://github.com/code-423n4/2024-02-ai-arena/blob/main/src/AiArenaHelper.sol#L85 https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/AiArenaHelper.sol#L144
generation
is set to be of type uint256
and there are multiple instances where generation
is used as uint8
/// @notice Mapping tracking fighter generation to attribute probabilities mapping(uint256 => mapping(string => uint8[])) public attributeProbabilities;
-> function deleteAttributeProbabilities(uint8 generation) public { //@audit-issue QA generation here is also uint8 require(msg.sender == _ownerAddress); //@ok uint256 attributesLength = attributes.length; //@ok for (uint8 i = 0; i < attributesLength; i++) { //@ok attributeProbabilities[generation][attributes[i]] = new uint8[](0); } }
It doesn't make sense to have generation
of uint256 and then pass only uint8
to get attributeProbabilities[generation]
.
Keep generation
from the same type or use casting.
uint256
to a loop iterating only to type(uint16).max
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L234 https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L249
Array of is passed to In case the array is bigger consists of elements more than type(uint16).max
method would revert.
Revert
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/MergingPool.sol#L50
Mapping is not of address
but of tokenId
.
-> /// @notice Mapping of address to fighter points. mapping(uint256 => uint256) public fighterPoints;
Changed comment
Mapping of address
to fighter points. -> Mapping of tokenId
to fighter points.
getUnclaimedRewards
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/MergingPool.sol#L169
It's not getting the unclaimed rewards
, but it gets the amount
(uint256) of unclaimed rewards. Also the method is view
and users can mistake it for a method that changes the state.
-> /// @notice Gets the unclaimed rewards for a specific address. /// @param claimer The address of the claimer. /// @return numRewards The amount of unclaimed fighters. -> function getUnclaimedRewards(address claimer) external view returns(uint256) { uint256 winnersLength; uint256 numRewards = 0; uint32 lowerBound = numRoundsClaimed[claimer]; for (uint32 currentRound = lowerBound; currentRound < roundId; currentRound++) { winnersLength = winnerAddresses[currentRound].length; for (uint32 j = 0; j < winnersLength; j++) { if (claimer == winnerAddresses[currentRound][j]) { numRewards += 1; } } } return numRewards; }
Use better naming for both method name and refactor comment to be more meaninfut that actually we retrieve the number of unclaimed rewards.
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/Neuron.sol#L62
This comment is wrong. The roles are not granted by the constructor
but by methods like addMinter
, addStaker
, addSpender
etc
-> /// @notice Grants roles to the ranked battle contract. /// @notice Mints the initial supply of tokens. /// @param ownerAddress The address of the owner who deploys the contract /// @param treasuryAddress_ The address of the community treasury /// @param contributorAddress The address that will distribute NRNs to early contributors when /// the initial supply is minted. constructor(address ownerAddress, address treasuryAddress_, address contributorAddress) ERC20("Neuron", "NRN") { _ownerAddress = ownerAddress; treasuryAddress = treasuryAddress_; isAdmin[_ownerAddress] = true; _mint(treasuryAddress, INITIAL_TREASURY_MINT); _mint(contributorAddress, INITIAL_CONTRIBUTOR_MINT); }
Refactor comment.
#0 - raymondfam
2024-02-26T05:19:29Z
Adequate amount of L and NC entailed.
#1 - c4-pre-sort
2024-02-26T05:19:32Z
raymondfam marked the issue as sufficient quality report
#2 - HickupHH3
2024-03-18T01:03:15Z
QA-1: L/R QA-2: R QA-3: R QA-4: R QA-5: L/R QA-6: R⨠QA-7: L/R
#3 - c4-judge
2024-03-18T01:03:18Z
HickupHH3 marked the issue as grade-b