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: 1/283
Findings: 12
Award: $7,568.05
π Selected for report: 0
π Solo Findings: 0
π Selected for report: CodeWasp
Also found by: 0x13, 0xAlix2, 0xAsen, 0xCiphky, 0xE1, 0xLogos, 0xaghas, 0xlemon, 0xlyov, 0xvj, ADM, Aamir, BARW, Bauchibred, DMoore, DanielArmstrong, Draiakoo, Fulum, GhK3Ndf, Josh4324, Kalogerone, KmanOfficial, Krace, KupiaSec, Limbooo, MidgarAudits, MrPotatoMagic, PedroZurdo, Pelz, Tendency, _eperezok, adam-idarrha, al88nsk, alexxander, alexzoid, aslanbek, blutorque, btk, c0pp3rscr3w3r, cartlex_, cats, d3e4, deadrxsezzz, denzi_, devblixt, dimulski, erosjohn, evmboi32, fnanni, grearlake, hulkvision, immeas, israeladelaja, jaydhales, jesjupyter, jnforja, juancito, klau5, korok, ktg, ladboy233, matejdb, merlinboii, novamanbg, nuthan2x, oualidpro, peanuts, petro_1912, pkqs90, pynschon, radev_sw, rouhsamad, sashik_eth, shaka, sobieski, soliditywala, stackachu, tallo, thank_you, ubl4nk, vnavascues, web3pwn, xchen1130, zhaojohnson
0.1044 USDC - $0.10
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/ecd2ca2cd7cac116f7a37d0e474bbb3d7d5e1c4d/contracts/token/ERC721/ERC721.sol#L175-L183 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L333-L365
Fighter holders can bypass the transfer restrictions by calling safeTransferFrom(from, to, tokenId, data)
, which is not overriden in FighterFarm.sol. Since it's not overriden, it is not checked if the fighter NFT has been staked and if the receiver will surpass the MAX_FIGHTERS_ALLOWED balance.
FighterFarm.sol overrides the ERC721 transfer functions in order to add the checks at _ableToTransfer(). This is done for transferFrom(from, to, tokenId) and safeTransferFrom(from, to, tokenId) but not for safeTransferFrom(from, to, tokenId, data).
Manual Review
Add the following to the FighterFarm contract:
/// @notice Safely transfers an NFT from one address to another. /// @dev Add a custom check for an ability to transfer the fighter. /// @param from Address of the current owner. /// @param to Address of the new owner. /// @param tokenId ID of the fighter being transferred. function safeTransferFrom( address from, address to, uint256 tokenId, bytes memory data ) public override(ERC721, IERC721) { require(_ableToTransfer(tokenId, to)); _safeTransfer(from, to, tokenId, ""); }
ERC721
#0 - c4-pre-sort
2024-02-23T04:35:31Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-23T04:35:41Z
raymondfam marked the issue as duplicate of #54
#2 - c4-pre-sort
2024-02-23T04:47:19Z
raymondfam marked the issue as duplicate of #739
#3 - c4-pre-sort
2024-02-23T04:53:11Z
raymondfam marked the issue as sufficient quality report
#4 - c4-judge
2024-03-11T02:40:33Z
HickupHH3 marked the issue as satisfactory
π Selected for report: Aamir
Also found by: 0rpse, 0x11singh99, 0x13, 0xAlix2, 0xAsen, 0xBinChook, 0xCiphky, 0xE1, 0xKowalski, 0xLogos, 0xWallSecurity, 0xaghas, 0xbranded, 0xlemon, 0xlyov, 0xpoor4ever, 0xprinc, 0xvj, ADM, Aymen0909, BARW, Bauchibred, Breeje, CodeWasp, DMoore, DeFiHackLabs, Draiakoo, Fulum, GhK3Ndf, Greed, Jorgect, Josh4324, Kalogerone, KmanOfficial, Krace, Limbooo, McToady, MidgarAudits, MrPotatoMagic, PedroZurdo, Pelz, Ryonen, SovaSlava, SpicyMeatball, Tendency, Timenov, ZanyBonzy, _eperezok, al88nsk, alexxander, alexzoid, aslanbek, blutorque, btk, cartlex_, cats, csanuragjain, deadrxsezzz, denzi_, devblixt, dimulski, djxploit, erosjohn, evmboi32, fnanni, grearlake, haxatron, hulkvision, immeas, israeladelaja, jaydhales, jesjupyter, jnforja, josephdara, juancito, kiqo, klau5, korok, krikolkk, ktg, kutugu, ladboy233, lil_eth, m4ttm, matejdb, merlinboii, n0kto, ni8mare, novamanbg, nuthan2x, oualidpro, pa6kuda, peter, petro_1912, pkqs90, pynschon, sandy, sashik_eth, shaflow2, shaka, sobieski, soliditywala, solmaxis69, stackachu, tallo, thank_you, tpiliposian, ubl4nk, visualbits, vnavascues, web3pwn, xchen1130, zhaojohnson
0.0037 USDC - $0.00
Game items that are set to non-transferable can be transferred.
GameItems.sol inherits from ERC1155 which contains two transfer functions: safeBatchTransferFrom and safeTransferFrom.
The contract overrides safeTransferFrom in order to check if the item is transferable or not: require(allGameItemAttributes[tokenId].transferable);
. However, safeBatchTransferFrom is not overriden and can still be called to transfer tokens without this restriction.
Place the following test inside GameItems.t.sol to see the bug.
/// @notice Test transferring non-transferable items. function testTransferability() public { _gameItemsContract.adjustTransferability(0, false); _fundUserWith4kNeuronByTreasury(_ownerAddress); _gameItemsContract.mint(0, 1); vm.expectRevert(); _gameItemsContract.safeTransferFrom(_ownerAddress, _DELEGATED_ADDRESS, 0, 1, ""); // Doesn't revert, ignoring the transferability check 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, ""); assertEq(_gameItemsContract.balanceOf(_DELEGATED_ADDRESS, 0), 1); assertEq(_gameItemsContract.balanceOf(_ownerAddress, 0), 0); }
Manual Review
Override safeBatchTransferFrom adding
require(allGameItemAttributes[tokenId].transferable);
Token-Transfer
#0 - c4-pre-sort
2024-02-22T03:49:51Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T03:49:59Z
raymondfam marked the issue as duplicate of #18
#2 - c4-pre-sort
2024-02-26T00:27:54Z
raymondfam marked the issue as duplicate of #575
#3 - c4-judge
2024-03-05T04:47:39Z
HickupHH3 changed the severity to 3 (High Risk)
#4 - c4-judge
2024-03-05T04:53:14Z
HickupHH3 marked the issue as satisfactory
π Selected for report: Abdessamed
Also found by: 0rpse, 0xAlix2, 0xAsen, 0xCiphky, 0xlemon, 0xmystery, 0xvj, ADM, Aamir, Archime, BARW, DarkTower, Draiakoo, FloatingPragma, JCN, McToady, MrPotatoMagic, OMEN, PetarTolev, Ryonen, SpicyMeatball, Tendency, VAD37, Velislav4o, VrONTg, Zac, adamn000, ahmedaghadi, alexxander, alexzoid, bhilare_, btk, cats, d3e4, denzi_, devblixt, dimulski, evmboi32, fnanni, givn, haxatron, immeas, jesjupyter, juancito, ke1caM, klau5, korok, krikolkk, matejdb, n0kto, niser93, peter, pkqs90, radin100, shaka, sl1, soliditywala, stackachu, stakog, t0x1c, vnavascues, yotov721, zhaojohnson
1.2667 USDC - $1.27
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L233-L261 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L484-L515 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/AiArenaHelper.sol#L107-L109
Users redeeming their mint passes for fighters can brute force mintPassDnas
values to get particular weights, elements and attributes even if rare, taking an unfair advantage and breaking the rarity model given by the attributes probabilities.
redeemMintPass takes mintPassDnas
as input without imposing any restrictions nor input value validation. Then the function uses these values to get the DNA of the new fighter given by keccak256(abi.encode(mintPassDnas[i]))
. This is a deterministic value that can be known in advance. Even though it's not possible to determine which mintPassDna will output a certain DNA, a simple script can be implemented to get DNAs of certain characteristics through trial and error. Here is a python pseudo code for that:
while true: randomMintPassDna = getRandomText() dna = Web3.keccak(text=randomMintPassDna) if dnaHasWantedFeatures(dna): break
To predict the weight and element, the following formulas used in _createFighterBase have to be checked:
uint256 element = dna % numElements[generation[fighterType]]; uint256 weight = dna % 31 + 65;
To predict the attributes of the fighter, the formula used in createPhysicalAttributes and dnaToIndex have to be checked:
uint256 rarityRank = (dna / attributeToDnaDivisor[attributes[i]]) % 100; uint256 attributeIndex = dnaToIndex(generation, rarityRank, attributes[i]); finalAttributeProbabilityIndexes[i] = attributeIndex;
Additionally, an existing fighter's dna could be replicated by simply using a mintPassDna string which bytes match the source of randomness of the fighter being replicated. In the case of fighters minted from the MergingPool or from a delegated signature, the string would have to match abi.encode(address, fighters.length)
. In the case of replicating a fighter minted using a mint pass, just use the same string and that's it.
Manual Review
Add stronger randomness into the DNA selection and require DNAs to be unique.
Other
#0 - c4-pre-sort
2024-02-22T07:35:10Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T07:35:18Z
raymondfam marked the issue as duplicate of #33
#2 - c4-pre-sort
2024-02-26T00:53:07Z
raymondfam marked the issue as duplicate of #1626
#3 - c4-judge
2024-03-05T10:56:58Z
HickupHH3 marked the issue as satisfactory
π Selected for report: klau5
Also found by: 0xAleko, 0xAlix2, 0xAsen, 0xCiphky, 0xKowalski, 0xlemon, 0xvj, 14si2o_Flint, Aamir, AlexCzm, Aymen0909, BARW, Blank_Space, DanielArmstrong, Davide, Draiakoo, Giorgio, McToady, MrPotatoMagic, PoeAudits, Ryonen, Silvermist, SpicyMeatball, Tychai0s, VAD37, Varun_05, alexxander, alexzoid, aslanbek, blutorque, btk, cats, d3e4, denzi_, evmboi32, fnanni, givn, grearlake, haxatron, jesjupyter, juancito, ke1caM, ktg, lanrebayode77, linmiaomiao, matejdb, merlinboii, n0kto, novamanbg, nuthan2x, petro_1912, pynschon, radin100, sashik_eth, shaka, sl1, soliditywala, solmaxis69, t0x1c, ubl4nk, vnavascues, xchen1130, yotov721, zhaojohnson
1.1225 USDC - $1.12
Not checking the fighterType in reRoll has the following consequences:
maxRerollsAllowed
can be exceeded if opposite fighter type has assigned a greater value of maxRerollsAllowed
.generation
is smaller for dendroids than for champions.The consequences mentioned above can happen because nowhere along the reRoll() function it is checked whether the fighterType provided as input is correct. A corrupted fighter type will have an effect in the following parts of the code:
Add the following test to FighterFarm.t.sol to check how a champion DNA can be manipulated to uint256(1) using a corrupted dendroid type. Note that there's an additional bug preventing the creation of new fighters when the generation > 0, which makes it difficult to test the other ramifications of this issue. Said generation bug is out of scope of this submission.
/// @notice Test rerolling a fighter with an incorrect fighter type function testRerollCorrupted() public { _mintFromMergingPool(_ownerAddress); // get 4k neuron from treasury _fundUserWith4kNeuronByTreasury(_ownerAddress); // after successfully minting a fighter, update the model if (_fighterFarmContract.ownerOf(0) == _ownerAddress) { uint256 neuronBalanceBeforeReRoll = _neuronContract.balanceOf(_ownerAddress); uint8 tokenId = 0; uint8 corruptedFighterType = 1; ( uint256 weight, uint256 element, FighterOps.FighterPhysicalAttributes memory physicalAttributes, uint256 id,,, uint8 generation,, bool dendroidBool ) = _fighterFarmContract.fighters(tokenId); assertEq(dendroidBool, false); _neuronContract.addSpender(address(_fighterFarmContract)); // User dendroid fighter type for a token id corresponding to a champion _fighterFarmContract.reRoll(tokenId, corruptedFighterType); ( weight, element, physicalAttributes, id,,, generation,, dendroidBool ) = _fighterFarmContract.fighters(tokenId); // DNA = uint256(1) results in attributeProbabilityIndex of 1 for all its attributes assertEq(physicalAttributes.head, 1); assertEq(physicalAttributes.eyes, 1); assertEq(physicalAttributes.mouth, 1); assertEq(physicalAttributes.body, 1); assertEq(physicalAttributes.hands, 1); assertEq(physicalAttributes.feet, 1); assertEq(_fighterFarmContract.numRerolls(0), 1); assertEq(neuronBalanceBeforeReRoll > _neuronContract.balanceOf(_ownerAddress), true); } }
Manual Review
Instead of letting the caller of reRoll() provide the fighterType, read it from fighter.dendroidBool.
Invalid Validation
#0 - c4-pre-sort
2024-02-22T01:24:45Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T01:25:01Z
raymondfam marked the issue as duplicate of #306
#2 - c4-judge
2024-03-05T04:30:23Z
HickupHH3 changed the severity to 2 (Med Risk)
#3 - c4-judge
2024-03-05T04:32:17Z
HickupHH3 marked the issue as satisfactory
#4 - c4-judge
2024-03-19T09:05:07Z
HickupHH3 changed the severity to 3 (High Risk)
π Selected for report: t0x1c
Also found by: 0rpse, 0xAadi, 0xBinChook, 0xCiphky, 0xDetermination, 14si2o_Flint, AC000123, Aamir, Abdessamed, Blank_Space, CodeWasp, DanielArmstrong, DarkTower, Draiakoo, Honour, Kalogerone, Krace, McToady, Merulez99, MidgarAudits, MrPotatoMagic, PedroZurdo, Silvermist, Tychai0s, VAD37, Velislav4o, VrONTg, WoolCentaur, YouCrossTheLineAlfie, ZanyBonzy, alexxander, aslanbek, btk, csanuragjain, d3e4, dimulski, djxploit, erosjohn, evmboi32, fnanni, forgebyola, forkforkdog, handsomegiraffe, immeas, israeladelaja, juancito, ktg, n0kto, neocrao, ni8mare, okolicodes, peanuts, petro_1912, shaflow2, shaka, swizz, ubermensch, ubl4nk, yotov721
2.0593 USDC - $2.06
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L244-L265 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/RankedBattle.sol#L416-L500
Players can stake amounts of NRN as small as 1 wei and they will still get the minimum staking factor of 1, risking a minimum amount of stake at any battle.
Additionally, if less than $\frac{9999}{bpsLostPerLoss}$ is staked, it's possible to battle without ever getting stake at risk after losing a battle. In other words, it's possible to battle for points only.
When a battle result is updated, the amount of stake that will become "at risk" can be zero even if the amount staked is greater than zero. This happens for stake amounts in the range [0 ; $\frac{9999}{bpsLostPerLoss}$] because curStakeAtRisk will round down to zero after dividing by $10000$.
In this scenario, the fighter will earn points as usually after winning a battle, but won't lose stake after losing a battle with zero points accumulated because curStakeAtRisk
is zero.
What this means is that fighters can participate for points only, without risking stake and without entering the "at risk" zone in which at least one win is needed before the fighter can battle for points again.
The opposite scenario, though harmless, is possible as well: a fighter having at some point $stakeAtRisk < \frac{9999}{bpsLostPerLoss} - 1$ could accidentally unstake everything except for 1 wei. This will cause the rounding of curStakeAtRisk
towards zero, but in this case battles will have zero effect on this fighter during the current round because $stakeAtRisk > 0$.
Manual Review
Require stakes to be greater or equal than 10^18.
Other
#0 - c4-pre-sort
2024-02-22T15:46:49Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-22T15:46:56Z
raymondfam marked the issue as duplicate of #38
#2 - c4-judge
2024-03-07T02:58:22Z
HickupHH3 changed the severity to 3 (High Risk)
#3 - c4-judge
2024-03-07T03:14:53Z
HickupHH3 marked the issue as satisfactory
π Selected for report: abhishek_thaku_r
Also found by: 0xAlix2, 0xDetermination, 0xShitgem, Draiakoo, Fulum, Greed, MrPotatoMagic, PoeAudits, Tychai0s, ahmedaghadi, alexzoid, dimulski, fnanni, givn, iamandreiski, immeas, kartik_giri_47538, kiqo, klau5, korok, ktg, maxim371, offside0011, pontifex, sashik_eth, stakog, swizz, yotov721
111.676 USDC - $111.68
The rerolling feature is unavailable for fighters with token ids greater than 255, potentially making them less valuable.
The data type of the tokenId input parameter of reRoll is uint8, which means that only values in the range [0-255] are allowed. Providing greater token ids will revert with a value out-of-bounds error.
Manual Review
Change
function reRoll(uint8 tokenId, uint8 fighterType) public {
to
function reRoll(uint256 tokenId, uint8 fighterType) public {
Under/Overflow
#0 - c4-pre-sort
2024-02-22T01:25:48Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T01:25:54Z
raymondfam marked the issue as duplicate of #68
#2 - c4-judge
2024-03-05T01:57:08Z
HickupHH3 marked the issue as satisfactory
π Selected for report: haxatron
Also found by: 0xAlix2, 0xCiphky, 0xStriker, 0xaghas, 0xbranded, 0xlamide, 0xmystery, 0xvj, 14si2o_Flint, Aamir, AgileJune, Aymen0909, DanielArmstrong, DarkTower, Draiakoo, EagleSecurity, Giorgio, Krace, KupiaSec, MidgarAudits, MrPotatoMagic, PoeAudits, Ryonen, SpicyMeatball, Topmark, Tychai0s, VAD37, Varun_05, VrONTg, WoolCentaur, _eperezok, alexxander, alexzoid, aslanbek, blutorque, btk, cartlex_, d3e4, devblixt, evmboi32, fnanni, immeas, jesjupyter, juancito, ke1caM, klau5, ktg, lil_eth, merlinboii, nuthan2x, peter, petro_1912, pkqs90, pynschon, radin100, sandy, sashik_eth, shaka, sl1, soliditywala, t0x1c, ubl4nk, visualbits, vnavascues
1.1225 USDC - $1.12
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L129 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L462-L474
Once incrementGeneration is called for the first time for a fighter type, new fighters of this type cannot be created. In other words, fighters of generation > 0 can't be created.
numElements stores the number of elements for every generation. The number of elements for generation zero is hardcoded in the constructor, but it's not updated when the generation is incremented in incrementGeneration()
and there's no way to update it otherwise.
When a new fighter with generation > 0 is created, _createFighterBase is called, which performs dna % numElements[generation[fighterType]]
. Here $numElements[generation[fighterType]]=0$, causing a revert when trying to perform a modulo operation by 0. Therefore, the following test added in FighterFarm.t.sol will result in the error message "FAIL. Reason: Division or modulo by 0":
/// @notice Test whether the correct sender of the signature can claim a fighter. function testClaimFightersGeneration1() public { _fighterFarmContract.incrementGeneration(0); _fighterFarmContract.incrementGeneration(1); uint8[2] memory numToMint = [1, 0]; 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"; // Right sender of signature should be able to claim fighter _fighterFarmContract.claimFighters(numToMint, claimSignature, claimModelHashes, claimModelTypes); assertEq(_fighterFarmContract.balanceOf(_ownerAddress), 1); assertEq(_fighterFarmContract.ownerOf(0), _ownerAddress); assertEq(_fighterFarmContract.totalSupply(), 1); }
Manual Review
Update numElements in incrementGeneration.
Other
#0 - c4-pre-sort
2024-02-22T18:30:21Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T18:30:28Z
raymondfam marked the issue as duplicate of #45
#2 - c4-judge
2024-03-07T06:58:03Z
HickupHH3 marked the issue as satisfactory
5012.4807 USDC - $5,012.48
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/AiArenaHelper.sol#L107 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/AiArenaHelper.sol#L20
Choosing an attribute divisor array that doesn't follow a geometric progression of the form 1, 100, 10000, 1000000, etc. will break the rarity system. In other words, the attributeProbabilities
won't work as expected and rarity of attributes will be corrupted, even making certain combinations of attributes unattainable.
In particular, it is concerning that the defaultAttributeDivisor
introduces this behavior.
Another consequence is that users could get deceived into thinking they got a rare fighter when it's actually very common and viceversa, misestimating the value of their fighters and potentially trading them for under/overestimated prices.
The patterns that we get from
uint256 rarityRank = (dna / attributeToDnaDivisor[attributes[i]]) % 100;
are repeated every time $dna\ mod (100 * lcm(attributeToDnaDivisor)) = 0$. What does this mean? Let's look for example at defaultAttributeDivisor
. For $divisor=2$, the rarityRank goes through every possible value every 200 increments. In other words, $\frac{dna}{2}\ mod\ 100 = 0$ for all $dna = 200*k$.
We equivalently can say the same for every other divisors 3, 5, 7, 11, 13. Using divisor 3 repeats the pattern every 300 increments, 5 every 500, etc. All of these individual patterns combined reset every $100 * lcm(2, 3, 5, 7, 11, 13)) = 3003000$. It's easy to see that all possible combinations of rarity ranks is actually $100^6 = 1\ trillion$, which is MUCH more than $3003000$. Additionally, not every $3003000$ output is unique. For instance $dna=0$ and $dna=1$ result in the same rarity rank pattern, which means that the amount of combinations is actually a lot less than $3003000$.
As a simplified exercise, let there be 2 attributes A and B with divisors 2 and 3, and let's calculate rarityRank using %10 instead of %100. Can you find a dna value for which $rarityRank(A)=9$ and $rarityRank(B)=0$?
Manual review
Change AiArenaHelper.sol L107 from
uint256 rarityRank = (dna / attributeToDnaDivisor[attributes[i]]) % 100;
to
uint256 rarityRank = (dna / 100 ** i) % 100;
Math
#0 - c4-pre-sort
2024-02-25T04:34:52Z
raymondfam marked the issue as insufficient quality report
#1 - raymondfam
2024-02-25T04:35:55Z
The divisors are protocol-determined and will be trusted.
#2 - c4-pre-sort
2024-02-25T04:35:59Z
raymondfam marked the issue as primary issue
#3 - c4-judge
2024-03-15T06:33:53Z
HickupHH3 marked issue #1979 as primary and marked this issue as a duplicate of 1979
#4 - c4-judge
2024-03-15T06:34:02Z
HickupHH3 marked the issue as satisfactory
#5 - c4-judge
2024-03-15T06:34:13Z
HickupHH3 changed the severity to 2 (Med Risk)
#6 - c4-judge
2024-03-20T03:26:02Z
HickupHH3 changed the severity to 3 (High Risk)
#7 - c4-judge
2024-03-22T13:29:00Z
HickupHH3 changed the severity to 2 (Med Risk)
#8 - c4-judge
2024-03-22T13:29:59Z
HickupHH3 marked the issue as not a duplicate
#9 - c4-judge
2024-03-22T13:30:07Z
HickupHH3 marked the issue as duplicate of #1979
π 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/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/MergingPool.sol#L139-L163 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L313-L331 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L502-L506
NFT winners claiming a fighter from the MergingPool can choose any value for the fighter's weight and element.
customAttributes
is provided as input in claimRewards and then passed without any validation to the FighterFarm contract for minting
_fighterFarmInstance.mintFromMergingPool( msg.sender, modelURIs[claimIndex], modelTypes[claimIndex], customAttributes[claimIndex] );
The FighterFarm contract takes these values as they are and creates a fighter with them only except for the case in which the element value equals 100. Note that in this latter case, element is in the range [0; numElements[..]] and weight is in the range [65; 96].
if (customAttributes[0] == 100) { element = dna % numElements[generation[fighterType]]; weight = dna % 31 + 65; newDna = fighterType == 0 ? dna : uint256(fighterType); } else { element = customAttributes[0]; weight = customAttributes[1]; newDna = dna; }
Manual Review
Add input validation to the weight and element values provided inside customAttributes when calling claimRewards.
Invalid Validation
#0 - c4-pre-sort
2024-02-24T08:55:18Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-24T08:55:35Z
raymondfam marked the issue as duplicate of #226
#2 - c4-judge
2024-03-11T10:24:32Z
HickupHH3 marked the issue as satisfactory
π Selected for report: ahmedaghadi
Also found by: 0x13, 0xAleko, 0xDetermination, 0xKowalski, 0xPluto, 0xRiO, 0xvj, AlexCzm, Avci, BARW, BigVeezus, Cryptor, DeFiHackLabs, Draiakoo, Fitro, Giorgio, GoSlang, Greed, Honour, Kalogerone, KmanOfficial, Krace, McToady, MidgarAudits, MrPotatoMagic, Nyxaris, ReadyPlayer2, Ryonen, SovaSlava, SpicyMeatball, VAD37, _eperezok, alexzoid, almurhasan, btk, cu5t0mpeo, deadrxsezzz, djxploit, dvrkzy, emrekocak, erosjohn, evmboi32, fnanni, grearlake, inzinko, jesjupyter, jesusrod15, josephdara, ke1caM, klau5, ktg, ladboy233, merlinboii, nuthan2x, peanuts, pipidu83, pontifex, radev_sw, sl1, sobieski, soliditywala, t0x1c, taner2344, vnavascues, y4y, yovchev_yoan, zaevlad
0.2347 USDC - $0.23
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/MergingPool.sol#L149-L162 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L495
A user might not be able to claim a fighter won on the MergingPool due to the MAX_FIGHTERS_ALLOWED
limit even when he hasn't reached the limit.
NFTs in the MergingPool are claimed on batch. If a user has won more than one fighter and has not yet claimed any of them, it will only be possible for him to claim all of them at once.
The issue is that the minting of any fighter will update the balance of the beneficiary and will revert if the MAX_FIGHTERS_ALLOWED
limit has been reached.
For example, a user who has 2 unclaimed fighters in the MergingPool and is currently holding 9 fighters, won't be able to claim the first fighter even if he should, because the second fighter will get him to a balance=11 and will make the claiming call revert.
Manual Review
Allow users to claim fighters up until a given round id lower than the current one.
Invalid Validation
#0 - c4-pre-sort
2024-02-22T08:51:32Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T08:52:08Z
raymondfam marked the issue as duplicate of #216
#2 - c4-judge
2024-03-11T12:50:03Z
HickupHH3 marked the issue as satisfactory
π Selected for report: klau5
Also found by: 0rpse, 0xBinChook, 0xDetermination, 0xGreyWolf, 0xLogos, 0xWallSecurity, 0xaghas, 0xgrbr, 0xkaju, 0xlyov, AlexCzm, BARW, Blank_Space, BoRonGod, Daniel526, DanielArmstrong, Draiakoo, FloatingPragma, Giorgio, Greed, Jorgect, Matue, McToady, MidgarAudits, Nyxaris, PUSH0, PedroZurdo, Pelz, PoeAudits, Silvermist, SpicyMeatball, Tekken, Tricko, Tumelo_Crypto, VAD37, WoolCentaur, Zac, alexzoid, andywer, aslanbek, bgsmallerbear, cats, d3e4, desaperh, dimulski, dutra, erosjohn, evmboi32, favelanky, fnanni, forkforkdog, gesha17, givn, grearlake, haxatron, honey-k12, iamandreiski, immeas, juancito, kaveyjoe, ke1caM, kiqo, klau5, korok, lil_eth, lsaudit, n0kto, ni8mare, niser93, pa6kuda, peanuts, peter, shaka, sl1, soliditywala, solmaxis69, t0x1c, tallo, thank_you, tpiliposian, visualbits, vnavascues, web3pwn, yotov721
0.0352 USDC - $0.04
Fighter owners can calculate in advance what weight, element and attributes will their rerolled fighter get. They can do this for any number of rerolls up to maxRerollsAllowed[fighterType]
. This makes the process transparent instead of random, because owners can simply choose and buy their fighter from the pool of rerolled fighters.
Additionally, owners can create a wallet address through brute force to get a DNA they want with specific features.
The DNA of rerolled fighters is given by
keccak256(abi.encode(msg.sender, tokenId, numRerolls[tokenId]));
Here msg.sender
is the owner of the fighter NFT. The first problem with this is that the DNA can be deterministically calculated for each rerolled number to understand whether it makes sense to reroll a fighter or not. More concerning yet, this knowledge can be used to manipulate the address of the fighter owner.
Even though it's not possible to determine which address will output a certain DNA, a simple script can be implemented to get DNAs of certain characteristics through trial and error. The idea is to randomly create private keys until one corresponds to a wallet address that results in the DNA we want. Once we have the private key, we can transfer the fighter and NRN to the new wallet, call reRoll() and transfer the rerolled fighter back. Here is a python pseudo code to find the wallet:
acc = web3.eth.Account() while true: newWallet = acc.create(getRandomText()) dna = Web3.keccak(newWallet, tokenId, numRerolls[tokenId]) if dnaHasWantedFeatures(dna): break
To predict the weight and element, the following formulas used in _createFighterBase have to be checked:
uint256 element = dna % numElements[generation[fighterType]]; uint256 weight = dna % 31 + 65;
To predict the attributes of the fighter, the formula used in createPhysicalAttributes and dnaToIndex have to be checked:
uint256 rarityRank = (dna / attributeToDnaDivisor[attributes[i]]) % 100; uint256 attributeIndex = dnaToIndex(generation, rarityRank, attributes[i]); finalAttributeProbabilityIndexes[i] = attributeIndex;
Manual Review
Use a DNA randomness source that cannot be manipulated.
Other
#0 - c4-pre-sort
2024-02-24T01:34:42Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-24T01:34:52Z
raymondfam marked the issue as duplicate of #53
#2 - c4-judge
2024-03-06T03:49:47Z
HickupHH3 marked the issue as satisfactory
#3 - c4-judge
2024-03-15T02:10:54Z
HickupHH3 changed the severity to 2 (Med Risk)
#4 - c4-judge
2024-03-20T01:04:23Z
HickupHH3 marked the issue as duplicate of #376
π Selected for report: haxatron
Also found by: BARW, DanielArmstrong, fnanni, juancito
2436.0656 USDC - $2,436.07
1 point is mistakenly added to the first element of attributeProbabilities[generation][attribute]
while the last element is reduced in 1 point. This means that the intended rarity for attributes is miscalculated. In the edge case in which the last element of attributeProbabilities[generation][attribute]
is 1, this index is missed completely, because it becomes unreachable.
We can visualize this issue with two simple examples.
Let's say that for a given generation and attribute:
attributeProbabilities[generation][attribute] = [10,10,10,10,10,10,10,10,10,10]
The range of values of rarityRank
that fall into the cumulative probability of the first iteration of dnaToIndex is [0 - 10]. Note that cumProb=10 >= rarityRank=10
is true. This means that there are eleven values (11%) for which rarityRank will output attributeProbabilityIndex
equal to 1.
The range of values of rarityRank
that fall into the cumulative probability of the second iteration of dnaToIndex
is [11 - 20], i.e. 10%. This is also true for all the following indexes except for the last one, which matches with 9 rarityRank values [91 - 99] (note that rarityRank <= 99). This means that the code is interpreting
attributeProbabilities[generation][attribute] = [10,10,10,10,10,10,10,10,10,10]
as
attributeProbabilities[generation][attribute] = [11,10,10,10,10,10,10,10,10,9]
Let's say that for a given generation and attribute:
attributeProbabilities[generation][attribute] = [99,1]
In this case, cumProb=99 >= rarityRank
is always true, which means that the second probability is ignored and in practice we get the following:
attributeProbabilities[generation][attribute] = [100,0]
It could be argued that this is a setup issue. After all, tweaking the probabilities to [9,10,10,10,10,10,10,10,10,10] and [98,1] in the previous examples and making sure interfaces and contracts potentially querying attributeProbabilities
parsed the data correctly according to this hacky fix would solve the issue. However, I think this lucky, counter-intuitive fix shouldn't be a reason to ignore the error. Probability values should mean the same across the array.
Manual Review
Use if (cumProb > rarityRank)
instead of if (cumProb >= rarityRank)
.
Math
#0 - c4-pre-sort
2024-02-24T03:42:00Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-24T03:42:45Z
raymondfam marked the issue as duplicate of #15
#2 - c4-judge
2024-03-05T03:28:22Z
HickupHH3 marked the issue as not a duplicate
#3 - c4-judge
2024-03-05T03:28:28Z
HickupHH3 marked the issue as duplicate of #112
#4 - c4-judge
2024-03-15T06:22:34Z
HickupHH3 marked the issue as satisfactory