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: 179/283
Findings: 7
Award: $6.32
π 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
ERC721 has 3 public functions that transfer tokens between accounts:
FighterFarm overrides 1st and 2nd functions with _ableToTransfer
check, which reverts the transfer if the receiver would exceed MAX_FIGHTERS_ALLOWED, or if the transferred Fighter is staked.
But, because ERC721's safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data)
is inherited but not overridden, both of these checks can be bypassed.
By transferring fighters between accounts, owners of staked FighterNFTs can initiate unlimited number of games for free, as every address has 100 free daily voltage, and the number of addresses is practically unlimited.
Override safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data)
with _ableToTransfer
check.
ERC721
#0 - c4-pre-sort
2024-02-23T04:04:57Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-23T04:05:00Z
raymondfam marked the issue as primary issue
#2 - c4-pre-sort
2024-02-23T04:47:39Z
raymondfam marked the issue as duplicate of #739
#3 - c4-pre-sort
2024-02-23T04:47:49Z
raymondfam marked the issue as sufficient quality report
#4 - c4-judge
2024-03-11T02:41:14Z
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
Every GameItem can be made "untransferable" via createGameItem
or adjustTransferability
. To achieve that, ERC1155's safeTransferFrom
is overridden to check for transferable
value of the item:
function safeTransferFrom( address from, address to, uint256 tokenId, uint256 amount, bytes memory data ) public override(ERC1155) { require(allGameItemAttributes[tokenId].transferable); super.safeTransferFrom(from, to, tokenId, amount, data); }
But the same was not done for safeBatchTransferFrom
, so it will be possible to transfer tokens that should not be transferable.
Users are expected to be limited in daily number of initiated games by their voltage, and voltage could be replenished only by batteries. In the deployment script, Batteries will be non-transferrable and have a daily limit of 5. So each user should be able to initiate only (100 + 5*100) / 10 = 60
battles per day regardless of their capital. However, because of this vulnerability, users will be able to initiate unlimited number of games per day (as long as they have NRN to buy batteries).
Override safeBatchTransferFrom
with transferrable
check, same as in safeTransferFrom
.
Other
#0 - c4-pre-sort
2024-02-22T03:29:15Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T03:29:23Z
raymondfam marked the issue as duplicate of #18
#2 - c4-pre-sort
2024-02-26T00:27:15Z
raymondfam marked the issue as duplicate of #575
#3 - c4-judge
2024-03-05T04:50:35Z
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
reRoll
, instead of setting traits pseudo-randomly, always sets them to 1, if the caller specified fighterType = 1
. This can be done regardless if the rerolled fighter is a rare Dendroid (fighterType 1) or a normal Champion (fighterType 0).
Let's trace the behaviour of reRoll
, if the user specifies fighterType = 1
:
function reRoll(uint8 tokenId, uint8 fighterType) public { /*...*/ (uint256 element, uint256 weight, uint256 newDna) = _createFighterBase(dna, fighterType); /*...*/
function _createFighterBase( uint256 dna, uint8 fighterType ) private view returns (uint256, uint256, uint256) { uint256 element = dna % numElements[generation[fighterType]]; uint256 weight = dna % 31 + 65; uint256 newDna = fighterType == 0 ? dna : uint256(fighterType); return (element, weight, newDna); }
As you can see, newDna
is set to 1 if the caller specified fighterType = 1
.
Then, reRoll calls createPhysicalAttributes
with the newDna, generates traits based on the dna modulo, which will always be 1.
function testRerollPoc() public { _mintFromMergingPool(_ownerAddress); _fundUserWith4kNeuronByTreasury(_ownerAddress); uint8 tokenId = 0; // normal Champion, not a Dendroid (, , , , , , , , bool isDendroid) = _fighterFarmContract.fighters( tokenId ); assertEq(isDendroid, false); _neuronContract.addSpender(address(_fighterFarmContract)); _fighterFarmContract.reRoll(tokenId, 1); ( , , FighterOps.FighterPhysicalAttributes memory physicalAttributes, , , , , , ) = _fighterFarmContract.fighters(tokenId); assertEq(physicalAttributes.head, 1); assertEq(physicalAttributes.eyes, 1); assertEq(physicalAttributes.mouth, 1); assertEq(physicalAttributes.body, 1); assertEq(physicalAttributes.hands, 1); assertEq(physicalAttributes.feet, 1); }
Foundry
a) If only Dendroids are supposed to have all physical attributes == 1, fighterType should be validated in reRoll
:
function reRoll(uint8 tokenId, uint8 fighterType) public { + require( + fighters[tokenId].dendroidBool ? fighterType == 1 : fighterType == 0 + );
b) If neither Dendroids nor Champions should have all physical attributes equal to 1, then _createFighterBase
should not alter the dna
.
Other
#0 - c4-pre-sort
2024-02-22T01:35:36Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T01:35:43Z
raymondfam marked the issue as duplicate of #306
#2 - c4-judge
2024-03-05T04:33:04Z
HickupHH3 marked the issue as satisfactory
#3 - c4-judge
2024-03-19T09:05:07Z
HickupHH3 changed the severity to 3 (High Risk)
π 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
In RankedBattle#updateBattleRecord
, _addResultPoints is only entered if the user has positive stake
or stakeAtRisk
:
if (amountStaked[tokenId] + stakeAtRisk > 0) { _addResultPoints(battleResult, tokenId, eloFactor, mergingPortion, fighterOwner); }
With bpsLostPerLoss = 10
, staking 1-999 wei results in curStakeAtRisk
rounding down 0 due to integer division:
curStakeAtRisk = (bpsLostPerLoss * (amountStaked[tokenId] + stakeAtRisk)) / 10**4;
So if a user stakes 1 wei of NRN (mergingPortion = 0), all his battles will have one of these 3 outcomes:
Which is unfair towards other stakers, whose wins and losses are always rewarded / penalized symmetrically.
Moreover, if instead the user decides to forward all points to the merging pool, there will only be two possible outcomes:
These strategies can be employed either by low-budget / low-winrate players; or by high-winrate / high-budget players after they are done for the round and simply want to preserve their current accumulatedPoints
.
The only users who do not want to employ this strategy are active high-winrate high-budget players, as they are the only ones who would gain more by staking significant NRN amounts and playing fair.
_addResultPoints
will not change her stakeAtRisk, because curStakeAtRisk
rounds down to zero.Alice ends up accumulating more merging pool points, and has a higher chance of winning the new fighter in comparison to other players.
In updateBattleRecord
, _addResultPoints
should be entered only if user's stake + stakeAtRisk
are above a certain value, which would prevent rounding down to zero.
- if (amountStaked[tokenId] + stakeAtRisk > 0) { + if (amountStaked[tokenId] + stakeAtRisk > MIN_STAKE) { _addResultPoints(battleResult, tokenId, eloFactor, mergingPortion, fighterOwner); }
Other
#0 - c4-pre-sort
2024-02-22T16:36:36Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-22T16:36:43Z
raymondfam marked the issue as duplicate of #38
#2 - c4-judge
2024-03-07T02:49:49Z
HickupHH3 changed the severity to 2 (Med Risk)
#3 - c4-judge
2024-03-07T02:58:22Z
HickupHH3 changed the severity to 3 (High Risk)
#4 - c4-judge
2024-03-07T03:20:06Z
HickupHH3 marked the issue as satisfactory
π Selected for report: haxatron
Also found by: 0xAlix2, 0xCiphky, 0xStriker, 0xaghas, 0xbranded, 0xlamide, 0xmystery, 0xvj, 14si2o_Flint, Aamir, AgileJune, Aymen0909, DanielArmstrong, DarkTower, Draiakoo, EagleSecurity, Giorgio, Krace, KupiaSec, MidgarAudits, MrPotatoMagic, PoeAudits, Ryonen, SpicyMeatball, Topmark, Tychai0s, VAD37, Varun_05, VrONTg, WoolCentaur, _eperezok, alexxander, alexzoid, aslanbek, blutorque, btk, cartlex_, d3e4, devblixt, evmboi32, fnanni, immeas, jesjupyter, juancito, ke1caM, klau5, ktg, lil_eth, merlinboii, nuthan2x, peter, petro_1912, pkqs90, pynschon, radin100, sandy, sashik_eth, shaka, sl1, soliditywala, t0x1c, ubl4nk, visualbits, vnavascues
1.1225 USDC - $1.12
incrementGeneration
:function incrementGeneration(uint8 fighterType) external returns (uint8) { require(msg.sender == _ownerAddress); generation[fighterType] += 1; maxRerollsAllowed[fighterType] += 1; return generation[fighterType]; }
numElements
doesn't have a setter. Only numElements[0]
is set in the constructor:constructor( address ownerAddress, address delegatedAddress, address treasuryAddress_ ) ERC721("AI Arena Fighter", "FTR") { _ownerAddress = ownerAddress; _delegatedAddress = delegatedAddress; treasuryAddress = treasuryAddress_; numElements[0] = 3; }
reRoll
, which calls _createFighterBase
:function _createFighterBase( uint256 dna, uint8 fighterType ) private view returns (uint256, uint256, uint256) { uint256 element = dna % numElements[generation[fighterType]];
Once the generation for a FighterType is increased to 1, all reRolls
for this type will revert in _createFighterBase
with 0x12 panic due to modulo by zero, as numElements[generation[fighterType]] = numElements[1] = 0
.
Fighters with gen > 0
can not be rerolled.
function testPanic() public { deal(address(_neuronContract), _ownerAddress, 1000e18); vm.prank(address(_mergingPoolContract)); _fighterFarmContract.mintFromMergingPool( _ownerAddress, "_neuralNetHash", "original", [uint256(1), uint256(80)] ); vm.startPrank(_ownerAddress); _fighterFarmContract.incrementGeneration(0); _neuronContract.addSpender(address(_fighterFarmContract)); vm.expectRevert(stdError.divisionError); _fighterFarmContract.reRoll(0, 0); }
Add a separate setter for numElements
mapping; or set it from incrementGeneration
.
Other
#0 - c4-pre-sort
2024-02-22T18:19:56Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T18:20:05Z
raymondfam marked the issue as duplicate of #45
#2 - c4-judge
2024-03-07T06:53:32Z
HickupHH3 changed the severity to 3 (High Risk)
#3 - c4-judge
2024-03-07T06:53:36Z
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
MergingPool#claimRewards allows the winner to specify customAttirubutes
that the new fighter will have, which are element
and weight
. Then, FighterFarm#mintFromMergingPool calls _createNewFighter
, which checks for customAttributes
:
if (customAttributes[0] == 100) { (element, weight, newDna) = _createFighterBase(dna, fighterType); } else { element = customAttributes[0]; weight = customAttributes[1]; newDna = dna; }
According to sponsor, the elements should be in range [0;2] (for the generation 0), and weight should be in range of [65; 95]. However, there's no such check in the else
bracket. As these factors directly affect the outcome of battles, the winner of merging pool NFT can choose any value for element
and weight
, getting a fighter with extreme competitive advantage.
function testClaimRewardsPoC() public { testPickWinner(); 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(1000); _customAttributes[0][1] = uint256(1000); _customAttributes[1][0] = uint256(1); _customAttributes[1][1] = uint256(80); _mergingPoolContract.claimRewards( _modelURIs, _modelTypes, _customAttributes ); (uint256 weight, uint256 element, , , , , , , ) = _fighterFarmContract .fighters(2); assertEq(weight, 1000); assertEq(element, 1000); }
else { + require(customAttributes[0] < numElements[generation[fighterType]], "invalid element"); + require(weight >= 65 && weight <= 95, "invalid weight"); element = customAttributes[0]; weight = customAttributes[1]; newDna = dna; }
Invalid Validation
#0 - c4-pre-sort
2024-02-24T09:04:06Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-24T09:04:18Z
raymondfam marked the issue as duplicate of #226
#2 - c4-judge
2024-03-11T10:23:52Z
HickupHH3 changed the severity to 2 (Med Risk)
#3 - c4-judge
2024-03-11T10:28: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
According to docs,
There are 8 categories of randomized attributes which make up the appearance
fighter traits are meant to be pseudo-random, uncontrolled by users. However, this is not the case.
FighterFarm allows users to reroll the traits for their FighterNFT. The seed for the reroll is derived from msg.sender
:
uint256 dna = uint256(keccak256(abi.encode(msg.sender, tokenId, numRerolls[tokenId]))); (uint256 element, uint256 weight, uint256 newDna) = _createFighterBase(dna, fighterType); fighters[tokenId].element = element; fighters[tokenId].weight = weight; fighters[tokenId].physicalAttributes = _aiArenaHelperInstance.createPhysicalAttributes( newDna, generation[fighterType], fighters[tokenId].iconsType, fighters[tokenId].dendroidBool );
If the NFT is not staked, it can be transferred to another address, for which reroll
would yield a different result.
Therefore, users can bruteforce EOA addresses of their mnemonic phrase, or bruteforce create2 addresses of their accounts, until they get the address that would get the desirable traits from reRoll
. Then, they would send their Fighter to that address and execute reRoll
.
Thus, sophisticated users will get an unfair advantage over the normal users, as they will be extremely efficient in getting the desirable traits.
Essentially, reRoll
will be used the following way:
Ordinary players - spend NRN on one or multiple rerolls, have no control over their traits.
Sophisticated players - compute the address off-chain, spend NRN on one reroll, get the desired traits.
It is impossible to make randomness non-manipulatable with on-chain seeds. Consider returning to Chainlink VRF.
Other
#0 - c4-pre-sort
2024-02-23T03:35:08Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-23T03:35:13Z
raymondfam marked the issue as primary issue
#2 - raymondfam
2024-02-23T03:35:46Z
Good suggestion on the intended design.
#3 - c4-pre-sort
2024-02-23T03:42:12Z
raymondfam marked the issue as sufficient quality report
#4 - brandinho
2024-03-04T00:55:38Z
This is a valid concern. However, we are not going to use Chainlink VRF - instead we will remove msg.sender
from the dna generation to solve this problem.
#5 - c4-sponsor
2024-03-04T00:55:46Z
brandinho (sponsor) confirmed
#6 - c4-judge
2024-03-06T03:44:43Z
HickupHH3 marked the issue as selected for report
#7 - c4-judge
2024-03-06T03:44:47Z
HickupHH3 marked the issue as satisfactory
#8 - c4-judge
2024-03-06T03:49:36Z
HickupHH3 changed the severity to 3 (High Risk)
#9 - c4-judge
2024-03-14T10:57:41Z
HickupHH3 marked issue #1017 as primary and marked this issue as a duplicate of 1017
#10 - c4-judge
2024-03-15T02:10:54Z
HickupHH3 changed the severity to 2 (Med Risk)
#11 - c4-judge
2024-03-20T01:04:02Z
HickupHH3 marked the issue as duplicate of #376