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: 160/283
Findings: 5
Award: $13.35
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
0.5612 USDC - $0.56
Judge has assessed an item in Issue #1661 as 3 risk. The relevant finding follows:
#[L-03] Players can update the fighterType due to lack of validation
#0 - c4-judge
2024-03-05T05:04:50Z
HickupHH3 marked the issue as duplicate of #306
#1 - c4-judge
2024-03-05T05:05:18Z
HickupHH3 marked the issue as partial-50
🌟 Selected for report: t0x1c
Also found by: 0rpse, 0xAadi, 0xBinChook, 0xCiphky, 0xDetermination, 14si2o_Flint, AC000123, Aamir, Abdessamed, Blank_Space, CodeWasp, DanielArmstrong, DarkTower, Draiakoo, Honour, Kalogerone, Krace, McToady, Merulez99, MidgarAudits, MrPotatoMagic, PedroZurdo, Silvermist, Tychai0s, VAD37, Velislav4o, VrONTg, WoolCentaur, YouCrossTheLineAlfie, ZanyBonzy, alexxander, aslanbek, btk, csanuragjain, d3e4, dimulski, djxploit, erosjohn, evmboi32, fnanni, forgebyola, forkforkdog, handsomegiraffe, immeas, israeladelaja, juancito, ktg, n0kto, neocrao, ni8mare, okolicodes, peanuts, petro_1912, shaflow2, shaka, swizz, ubermensch, ubl4nk, yotov721
2.0593 USDC - $2.06
A player could earn 750 points by staking a single wei of NRN tokens due to the fact that the curStakeAtRisk
calculation truncates the results of small staked amounts. This means that the player can play for virtually risk free staking amounts and earn points.
A player can stake an amount between 1 and 999 wei and get 750 points due to the fact that the following calculation will truncate thecurStakeAtRisk
which will result in zero: curStakeAtRisk = (bpsLostPerLoss * (amountStaked[tokenId] + stakeAtRisk)) / 10 ** 4;
.
Note that the coded POC will run as is in RankedBattle.t.sol
.
function testStakeNRNPoc() public { address staker = vm.addr(3); _mintFromMergingPool(staker); _fundUserWith4kNeuronByTreasury(staker); assertEq(_neuronContract.balanceOf(staker) >= 4_000 * 10 ** 18, true); assertEq(_fighterFarmContract.ownerOf(0), staker); assertEq(_neuronContract.hasRole(keccak256("STAKER_ROLE"), address(_rankedBattleContract)), true); console.log("Points before: ", _rankedBattleContract.accumulatedPointsPerAddress(staker, 0)); // stake 1 wei vm.prank(staker); _rankedBattleContract.stakeNRN(1, 0); assertEq(_rankedBattleContract.amountStaked(0), 1); // call updatebattle record as a win vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); // assert player points are 750 assertEq(_rankedBattleContract.accumulatedPointsPerAddress(staker, 0), 750); }
Manual Review
Consider using safeTransfer
in the _addResultPoints
function, which will revert on zero transfers.
Code reference: https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L493
Decimal
#0 - c4-pre-sort
2024-02-22T17:18:07Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-22T17:18:15Z
raymondfam marked the issue as duplicate of #38
#2 - c4-judge
2024-03-07T02:58:21Z
HickupHH3 changed the severity to 3 (High Risk)
#3 - c4-judge
2024-03-07T03:38:41Z
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
A player can set custom attributes
when the claimRewards
function is called. This means that the player can affect the weight
and element
of a fighter netting an overpowered fighter with a higher chance of winning, which will result in gamified earnings.
Once a player earns a new fighter through the MergingPool
the admin calls pickWinner
which allows a player to call claimRewards
. The player can then set a custom weight
and element
for their fighters. Referring to the POC below, we were able to set a weight at type(uint256).max
which contradicts the comment from the developer “so in our game, we bound the weight to be between 65 and 95". It’s worth noting that physical attributes can be set as well, but will be overridden in _createNewFighter
.
The coded POC will run in MergingPool.t.sol
as is.
function testSetAttributes() public { address player = vm.addr(3); _mintFromMergingPool(player); _mintFromMergingPool(vm.addr(4)); _fundUserWith4kNeuronByTreasury(player); uint256[] memory win = new uint256[](2); win[0] = 0; win[1] = 1; _mergingPoolContract.pickWinner(win); uint256[2][] memory attri = new uint[2][](2); string[] memory types = new string[](2); for (uint i = 0; i < 2; i++) { attri[i][0] = type(uint256).max; attri[i][1] = type(uint256).max; types[i] = "original"; } string[] memory uris = new string[](2); uris[0] = "something"; uris[1] = "other"; vm.prank(player); _mergingPoolContract.claimRewards(uris, types, attri); (,,uint256 weight1,,,,) = _fighterFarmContract.getAllFighterInfo(2); assertEq(weight1, type(uint256).max); }
Manual Review
Add restrictions to the custom attributes. The best possible solution would be to use chainlink VRF and give the winners fighters with random stats.
Invalid Validation
#0 - c4-pre-sort
2024-02-24T09:08:07Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-24T09:08:16Z
raymondfam marked the issue as duplicate of #226
#2 - c4-judge
2024-03-11T10:23:53Z
HickupHH3 changed the severity to 2 (Med Risk)
#3 - c4-judge
2024-03-11T10:29:47Z
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
A player can manipulate the weight
and element
of their fighter through the dna
calculation. This results in a fighter with better stats and, thus a higher win rate. A higher win rate will nett a player more points, thus more claimable NRN tokens.
Referring to the dna
calculation:uint256 dna = uint256(keccak256(abi.encode(msg.sender, tokenId, numRerolls[tokenId])));
The dna
affects the weight
and element
of the fighters via the _createFighterBase
function. The player could simulate the numRerolls
function to predict at which reroll the fighter will receive the highest stats . This can be compounded by using claimFighters
where you can wait for the optimum fighters.length value
as shown here: uint256(keccak256(abi.encode(msg.sender, fighters.length)))
. This means that the player can check at what fighters.length
they will have the strongest fighters and claim at that point.
This coded POC will run in RankedBattle.t.sol
as is. It shows the result of every possible reroll before the user calls the reRoll
function.
function testDnaManipulation() public { address player = vm.addr(3); _mintFromMergingPool(player); uint8 tokenId = 0; _fundUserWith4kNeuronByTreasury(player); (,,uint256 weight,,,,) = _fighterFarmContract.getAllFighterInfo(tokenId); console.log("Owner Addr: ", player); // 0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69 console.log("Original Weight: ", weight); // 80 uint256 bestWeight = weight; // arbitrary huge number uint256 bestReroll = 99999; for (uint256 i = 1; i < 4; i++) { // How DNA is calculated // uint256 dna = uint256(keccak256(abi.encode(msg.sender, tokenId, numRerolls[tokenId]))); uint256 expectedDna = uint256(keccak256(abi.encode(player, tokenId, i))); // How weight is calculated // uint256 weight = dna % 31 + 65; uint256 expectedWeight = expectedDna % 31 + 65; if (bestWeight < expectedWeight) { bestReroll = i; bestWeight = expectedWeight; } } console.log("-----------------------------------------"); console.log("BEST REROLL: ", bestReroll); console.log("BEST WEIGHT: ", bestWeight); console.log("-----------------------------------------"); // if rerolling is even worth it if (bestReroll != 99999) { uint8 fighterType = 0; _neuronContract.addSpender(address(_fighterFarmContract)); for (uint256 i = 1; i < 4; i++) { vm.prank(player); _fighterFarmContract.reRoll(tokenId, fighterType); (,,uint256 newWeight,,,,) = _fighterFarmContract.getAllFighterInfo(tokenId); console.log("Actual Reroll: ", i); console.log("Actual Weight: ", newWeight); if (bestReroll == i) { assertEq(newWeight, bestWeight); } } } }
Console output:
Running 1 test for test/RankedBattle.t.sol:RankedBattleTest [PASS] testDnaManipulation() (gas: 768589) Logs: Owner Addr: 0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69 Original Weight: 80 ----------------------------------------- BEST REROLL: 2 BEST WEIGHT: 82 ----------------------------------------- Actual Reroll: 1 Actual Weight: 67 Actual Reroll: 2 Actual Weight: 82 Actual Reroll: 3 Actual Weight: 69 Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.28ms Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)
Manual Review
Use Chainlink’s VRF to randomize the DNA values
Math
#0 - c4-pre-sort
2024-02-24T01:58:10Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-24T01:58:21Z
raymondfam marked the issue as duplicate of #53
#2 - c4-judge
2024-03-06T03:53:08Z
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-22T04:21:55Z
HickupHH3 marked the issue as duplicate of #376
🌟 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
#[L-01] Missing length check for iconsTypes
The redeemMintPass
function does not include the iconsTypes
array in its sanity length checks. This could cause an unintended revert. Consider adding this check as per the diff:
require( mintpassIdsToBurn.length == mintPassDnas.length && mintPassDnas.length == fighterTypes.length && fighterTypes.length == modelHashes.length && modelHashes.length == modelTypes.length ++ && iconsTypes.length == modelTypes.length );
#[L-02] The claimRewards
function does not validate the lengths of the parameter arrays
There could be unintended reverts in claimRewards
due to the fact that the function relies on the input array lengths to be the same (as the index positions are compared), but does not validate this fact. Consider adding a sanity check that compares the array lengths.
++ require(modelURIs.length == modelTypes.length, "array length mismatch");
#[L-03] Players can update the fighterType
due to lack of validation
A player can change the fighterType by calling reRoll
. The reRoll
function takes two parameters namely tokenId
and fighterType
, but the function doesn’t compare what the existing fighterType was. Which means that a player could reRoll
their fighter type if they choose to do so. It will also skew the physicalAttributes of the fighter as the function will call createPhysicalAttributes
with a false dendroidBool. Consider retrieving the fighterType from the fighters object in stead of passing it as a parameter.
#[L-04] The incrementGeneration
function increases the maxRerollsAllowed
per fighter type
The incrementGeneration
function increments the generation per fighter type, but it also increases the maxRerollsAllowed
for every generation increase, thus the new generation fighters will get an added re-roll every time incrementGeneration
is called. Also consider that any existing fighter will gain a re-roll with each generation increment with reference to this line in the reRoll
function: require(numRerolls[tokenId] < maxRerollsAllowed[fighterType]);
as re-rolls are checked on a global level. Consider adding a function that is callable by the owner to specifically increment the re-rolls:
/// @notice Increases the maxRerolls for all fighters /// @dev Only the owner address is authorized to call this function. /// @param fighterType Type of fighter either 0 or 1 (champion or dendroid). function incrementMaxRerolls(uint8 fighterType) external { require(msg.sender == _ownerAddress); maxRerollsAllowed[fighterType] += 1; }
#[L-05] Admin role changes should be Two step
The owner is extremely powerful in this protocol, but the transferOwnership
function in various contracts listed below implements a change of ownership with no validation which could be dangerous. Consider implementing a two step “push” and “pull” admin transfer process such as the OpenZeppelin Ownable2Step.sol library,https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable2Step.sol
Locations:
#[I-01] Missing natspec for icontypes
inredeemMintPass
The redeemMintPass
function is missing natspec for iconsTypes
. Please consider adding the natspec for the parameter to improve the completeness of the documentation.
#[I-02] The following functions do not emit events for storage updates:
transferOwnership
addAttributeDivisor
addAttributeProbabilities
deleteAttributeProbabilities
transferOwnership
addStaker
instantiateAIArenaHelperContract
instantiateMintpassContract
instantiateNeuronContract
setMergingPoolAddress
setTokenURI
claimFighters
redeemMintPass
updateModel
mintFromMergingPool
reRoll
transferOwnership
adjustAdminAccess
instantiateNeuronContract
setAllowedBurningAddresses
setTokenURI
transferOwnership
adjustAdminAccess
updateWinnersPerPeriod
pickWinner
transferOwnership
adjustAdminAccess
transferOwnership
adjustAdminAccess
setGameServerAddress
setStakeAtRiskAddress
instantiateNeuronContract
instantiateMergingPoolContract
setRankedNrnDistribution
setBpsLostPerLoss
setNewRound
updateBattleRecord
setNewRound
transferOwnership
adjustAdminAccess
adjustAllowedVoltageSpenders
#0 - raymondfam
2024-02-26T04:16:24Z
L3/L4 to #306
#1 - c4-pre-sort
2024-02-26T04:16:28Z
raymondfam marked the issue as sufficient quality report
#2 - HickupHH3
2024-03-05T10:37:34Z
#1480: L L1/2: R L4: R L5: R couple more Rs and NCs
#3 - c4-judge
2024-03-18T04:34:32Z
HickupHH3 marked the issue as grade-b