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: 216/283
Findings: 2
Award: $1.92
π Selected for report: 0
π Solo Findings: 0
π Selected for report: ktg
Also found by: 0xCiphky, 0xDetermination, 0xRiO, 0xWallSecurity, 0xlemon, 0xvj, AlexCzm, BARW, Blank_Space, Draiakoo, FloatingPragma, Giorgio, Matue, McToady, MrPotatoMagic, Silvermist, SpicyMeatball, Tendency, Topmark, Tumelo_Crypto, _eperezok, agadzhalov, ahmedaghadi, alexxander, aslanbek, cats, d3e4, denzi_, dutra, evmboi32, fnanni, givn, handsomegiraffe, haxatron, immeas, juancito, ke1caM, kiqo, klau5, krikolkk, niser93, peter, petro_1912, pkqs90, rspadi, sl1, stakog, visualbits, vnavascues, yotov721
1.876 USDC - $1.88
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L462-L474
In MergingPool, we have the claimRewards function used to claim a fighter, in which we arbitrate the fighter's parameters, as follows:
function claimRewards( string[] calldata modelURIs, string[] calldata modelTypes, uint256[2][] calldata customAttributes ) external { uint256 winnersLength; uint32 claimIndex = 0; uint32 lowerBound = numRoundsClaimed[msg.sender]; for (uint32 currentRound = lowerBound; currentRound < roundId; currentRound++) { numRoundsClaimed[msg.sender] += 1; winnersLength = winnerAddresses[currentRound].length; for (uint32 j = 0; j < winnersLength; j++) { if (msg.sender == winnerAddresses[currentRound][j]) { _fighterFarmInstance.mintFromMergingPool( msg.sender, modelURIs[claimIndex], modelTypes[claimIndex], customAttributes[claimIndex] ); claimIndex += 1; } } } if (claimIndex > 0) { emit Claimed(msg.sender, claimIndex); } }
function mintFromMergingPool( address to, string calldata modelHash, string calldata modelType, uint256[2] calldata customAttributes ) public { require(msg.sender == _mergingPoolAddress); _createNewFighter( to, uint256(keccak256(abi.encode(msg.sender, fighters.length))), modelHash, modelType, 0, 0, customAttributes ); }
Function _createNewFighter mentioned before:
function _createNewFighter( address to, uint256 dna, string memory modelHash, string memory modelType, uint8 fighterType, uint8 iconsType, uint256[2] memory customAttributes ) private { require(balanceOf(to) < MAX_FIGHTERS_ALLOWED); uint256 element; uint256 weight; uint256 newDna; if (customAttributes[0] == 100) { (element, weight, newDna) = _createFighterBase(dna, fighterType); } else { element = customAttributes[0]; weight = customAttributes[1]; newDna = dna; } uint256 newId = fighters.length; bool dendroidBool = fighterType == 1; FighterOps.FighterPhysicalAttributes memory attrs = _aiArenaHelperInstance.createPhysicalAttributes( newDna, generation[fighterType], iconsType, dendroidBool ); fighters.push( FighterOps.Fighter( weight, element, attrs, newId, modelHash, modelType, generation[fighterType], iconsType, dendroidBool ) ); _safeMint(to, newId); FighterOps.fighterCreatedEmitter(newId, weight, element, generation[fighterType]); }
As we can see, the weight and element are completely arbitrated by the users, and can be very low or very high, breaking the logic of the game.
As the documentation mentions here, we have a small number of elements, and each element corresponds to an integer value, however, we can assign a value that is not linked to any element, thus being able to have benefits over other players.
As for weight, we can assign a value of zero, or even much greater than 100 (which is the limit).
You can use the following test case in the MerginPool.t.sol file:
function testParametersThreshold() public { _mintFromMergingPool(_ownerAddress); _mintFromMergingPool(_DELEGATED_ADDRESS); assertEq(_fighterFarmContract.ownerOf(0), _ownerAddress); 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[](2); _modelURIs[0] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; _modelURIs[1] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; string[] memory _modelTypes = new string[](2); _modelTypes[0] = "original"; _modelTypes[1] = "original"; uint256[2][] memory _customAttributes = new uint256[2][](2); _customAttributes[0][0] = uint256(1); _customAttributes[0][1] = uint256(0); _customAttributes[1][0] = uint256(100000000000); _customAttributes[1][1] = uint256(100000000000); // winners of roundId 1 are picked _mergingPoolContract.pickWinner(_winners); // winner claims rewards for previous roundIds _mergingPoolContract.claimRewards(_modelURIs, _modelTypes, _customAttributes); }
To execute, run this command:
forge test --mt testParametersThreshold -vvvv
vscode, foundry
Implement validation on attributes entered by the user, to validate upper and lower limits.
Context
#0 - c4-pre-sort
2024-02-24T09:06:24Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-24T09:06:37Z
raymondfam marked the issue as duplicate of #226
#2 - c4-judge
2024-03-11T10:29:36Z
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/main/src/AiArenaHelper.sol#L83-L121
In AiArenaHelper we have the createPhysicalAttributes function, responsible for calculating the rarity of the fighter's physical attributes. As we can see in line 107, rarity is calculated according to dna, and dna is equal to the address of the fighter's owner.
function createPhysicalAttributes( uint256 dna, uint8 generation, uint8 iconsType, bool dendroidBool ) external view returns (FighterOps.FighterPhysicalAttributes memory) { if (dendroidBool) { return FighterOps.FighterPhysicalAttributes(99, 99, 99, 99, 99, 99); } else { uint256[] memory finalAttributeProbabilityIndexes = new uint[](attributes.length); uint256 attributesLength = attributes.length; for (uint8 i = 0; i < attributesLength; i++) { 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) ) { finalAttributeProbabilityIndexes[i] = 50; } else { uint256 rarityRank = (dna / attributeToDnaDivisor[attributes[i]]) % 100; uint256 attributeIndex = dnaToIndex(generation, rarityRank, attributes[i]); finalAttributeProbabilityIndexes[i] = attributeIndex; } } return FighterOps.FighterPhysicalAttributes( finalAttributeProbabilityIndexes[0], finalAttributeProbabilityIndexes[1], finalAttributeProbabilityIndexes[2], finalAttributeProbabilityIndexes[3], finalAttributeProbabilityIndexes[4], finalAttributeProbabilityIndexes[5] ); } }
Therefore, users can automate a script to generate addresses and calculate the rarity of attributes, until reaching an address that corresponds to a rarity well above the average for legitimate users, thus gaining an advantage and selling these fighters.
Create new addresses on the blockchain and calculate the rarity using the createPhysicalAttributes function, repeat until you find a satisfactory rarity. When you find it, you can create the fighter using the claimRewards function of the MerginPool contract or the redeemMintPass function of FighterFarm, for example.
vscode
Try implementing a salt in the rarity calculation, add more variables to the calculation, taking the power of prediction out of the user's control.
Context
#0 - c4-pre-sort
2024-02-24T02:01:56Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-24T02:02:45Z
raymondfam marked the issue as duplicate of #53
#2 - c4-judge
2024-03-06T03:49:26Z
HickupHH3 changed the severity to 3 (High Risk)
#3 - c4-judge
2024-03-06T03:53:26Z
HickupHH3 marked the issue as satisfactory
#4 - c4-judge
2024-03-15T02:10:54Z
HickupHH3 changed the severity to 2 (Med Risk)
#5 - c4-judge
2024-03-22T04:23:05Z
HickupHH3 marked the issue as duplicate of #376