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: 102/283
Findings: 3
Award: $61.36
π 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
1.1225 USDC - $1.12
The FighterFarm.reRoll() allows users to change attributes of their fighter like; element
, weight
, and physicalAttributes
. The only constraint with this function call is that numRerolls[tokenId] < maxRerollsAllowed[fighterType]
.
However, even with the present constraint, user can still reroll fighter beyond the actual maxRerollsAllowed of the original fighterType
Originl Fighter type might be 0 or 1; (champion or dendroid).
Initially, maxRerollsAllowed = [3, 3]
/// @notice The maximum amount of rerolls for each fighter. uint8[2] public maxRerollsAllowed = [3, 3];
But any of the generations could be changed and maxRerollsAllowed
will increment.
function incrementGeneration(uint8 fighterType) external returns (uint8) { ... maxRerollsAllowed[fighterType] += 1; ... }
If dendroid generation was changed and maxRerollsAllowed[dendroid] increased to 4, a user could claim the fighter type to be dendroid when it is champion originally, so when the numRerolls[tokenId] = 3
, the check will pass as it checks maxRerollsAllowed[dendroid]
which is now 4.
function reRoll(uint8 tokenId, uint8 fighterType) public { require(msg.sender == ownerOf(tokenId)); require(numRerolls[tokenId] < maxRerollsAllowed[fighterType]); require(_neuronInstance.balanceOf(msg.sender) >= rerollCost, "Not enough NRN for reroll"); ...
Manual review.
Validate that the fighter type is originally what the user claims it to be.
Invalid Validation
#0 - c4-pre-sort
2024-02-22T02:38:12Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T02:38:18Z
raymondfam marked the issue as duplicate of #306
#2 - c4-judge
2024-03-05T04:37:01Z
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: klau5
Also found by: 0xAlix2, 0xCiphky, 0xDetermination, 0xG0P1, 0xMosh, 0xabhay, 14si2o_Flint, AlexCzm, Aymen0909, CodeWasp, DanielArmstrong, FloatingPragma, Giorgio, JCN, Jorgect, Kalogerone, KmanOfficial, Kow, KupiaSec, McToady, SpicyMeatball, VAD37, WoolCentaur, ZanyBonzy, alexxander, alexzoid, almurhasan, blutorque, csanuragjain, denzi_, dipp, djxploit, evmboi32, handsomegiraffe, haxatron, immeas, jesjupyter, ke1caM, klau5, lanrebayode77, lil_eth, merlin, merlinboii, nuthan2x, peanuts, shaflow2, shaka, sl1, solmaxis69, stakog, swizz, t0x1c, tallo, ubermensch, vnavascues, yotov721
1.0089 USDC - $1.01
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L342-L344 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L475-L478
When a player losses a battle with a staked fighter, _addResultPoints
checks if the fighter has points, else some of the amountStaked[tokenId]
is sent to _stakeAtRiskAddress
. The user needs to win battles to recover stakeAtRisk
. If the user stakeAtRisk
is not recovered before the end of the round, it will be sent to treasury.
However, a player can avoid losing stakeAtRisk
to treasury, recover them all, and accrue points with zero stakedAmounts
_stakeAtRiskAddress
./// Potential amount of NRNs to put at risk or retrieve from the stake-at-risk contract curStakeAtRisk = (bpsLostPerLoss * (amountStaked[tokenId] + stakeAtRisk)) / 10**4;
From the snippet above, Alice curStakeAtRisk = 1e21
1_000 NRN tokens.
3. After this result,stakeAtRisk = 1000
} else { /// If the fighter does not have any points for this round, NRNs become at risk of being lost bool success = _neuronInstance.transfer(_stakeAtRiskAddress, curStakeAtRisk); if (success) { _stakeAtRiskInstance.updateAtRiskRecords(curStakeAtRisk, tokenId, fighterOwner); amountStaked[tokenId] -= curStakeAtRisk; }
Alice unstakes the rest(the only implication to this is the inability to stake in the same round, but the player will still participate in battles).
4. After unstaking the entire amount, the next battle result was another loss, which means Alice stakeAtRisk
ought to increase since there is no points to deduct from, but curStakeAtRisk
has been turned to zero due to a check;
/// Do not allow users to lose more NRNs than they have in their staking pool if (curStakeAtRisk > amountStaked[tokenId]) { curStakeAtRisk = amountStaked[tokenId]; //@audit including zero? }
Since amountStaked[tokenId]
is now zero, curStakeAtRisk = (10 * (0+1000e18))/10e4 = 4e18
, curStakeAtRisk is then set to zero.
/// If the fighter does not have any points for this round, NRNs become at risk of being lost bool success = _neuronInstance.transfer(_stakeAtRiskAddress, curStakeAtRisk);
from the snippet above, zero will be transferred to _stakeAtRiskAddress
and only zero is deducted from amountStaked[tokenId]
, so nothing will revert, since both values are zero.
Even though Alice cannot have more stake put at risk, if a battle is won part of the stake at risk will be recovered. here
/// If the user has stake-at-risk for their fighter, reclaim a portion /// Reclaiming stake-at-risk puts the NRN back into their staking pool if (curStakeAtRisk > 0) { _stakeAtRiskInstance.reclaimNRN(curStakeAtRisk, tokenId, fighterOwner); amountStaked[tokenId] += curStakeAtRisk; }
If the Alice can fully recover the stake at risk, points can begin to accumulate and can be used to claim rewards.
Paste the test in RankedBattle.t.sol
function test_AvoidLosingStakeAndRecoverLostStake() public { address player1 = vm.addr(3); _mintFromMergingPool(player1); vm.prank(_treasuryAddress); _neuronContract.transfer(player1, 1_000_000 * 10 ** 18); vm.prank(player1); _rankedBattleContract.stakeNRN(1_000_000 * 10 ** 18, 0); assertEq(_rankedBattleContract.amountStaked(0), 1_000_000 * 10 ** 18); //After the Battle, Update is done vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); uint256 StakeFactor_Player1 = _rankedBattleContract.stakingFactor(0); console.log( "===============================================================================" ); console.log("s.F Player1", StakeFactor_Player1); uint256 player1_Point = _rankedBattleContract .accumulatedPointsPerFighter(0, 0); console.log("Player1 POINT:", player1_Point); uint256 initial_StakeAtRisk_Player1 = _stakeAtRiskContract .getStakeAtRisk(0); console.log("Player1 Stake at Risk:", initial_StakeAtRisk_Player1); console.log( "===============================================================================" ); //Player1 enter a battle and saw that this is a loss, then frontruns the updateBattleRecord() ustake leaving 1wei vm.prank(player1); _rankedBattleContract.unstakeNRN(1_000_000 * 10 ** 18, 0); //Loaned/flashloan/personal funds assertEq(_rankedBattleContract.amountStaked(0), 0); vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); console.log( "Player1 Stake at Risk After the second lost:", _stakeAtRiskContract.getStakeAtRisk(0) ); console.log( "Amount STAKED after Loss and Unstake:", _rankedBattleContract.amountStaked(0) ); console.log( "===============================================================================" ); for (uint256 i = 0; i < 8; i++) { vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); } console.log( "Player1 Stake at Risk After 8 Wins:", _stakeAtRiskContract.getStakeAtRisk(0) ); console.log( "Amount Recovered after 8 wins:", initial_StakeAtRisk_Player1 - _stakeAtRiskContract.getStakeAtRisk(0) ); console.log( "Amount STAKED Now:", _rankedBattleContract.amountStaked(0) ); console.log( "===============================================================================" ); }
Logs: =============================================================================== s.F Player1 1000 Player1 POINT: 0 Player1 Stake at Risk: 1000000000000000000000 =============================================================================== Player1 Stake at Risk After the second lost: 1000000000000000000000 Amount STAKED after Loss and Unstake: 0 =============================================================================== Player1 Stake at Risk After 8 Wins: 992000000000000000000 Amount Recovered after 8 wins: 8000000000000000000 Amount STAKED Now: 8000000000000000000 ===============================================================================
Manual review and foundry
Since, curStakeAtRisk
will be set to zero if no stakedAmount, amount to be recovered must be the least balance of amountStaked[tokenId], else all stake at risk should be sent to treasury after the round, to avoid players using zero stke to recover stake at risk gradually and having nothing to loss doing so.
/// Do not allow users to lose more NRNs than they have in their staking pool if (curStakeAtRisk > amountStaked[tokenId]) { curStakeAtRisk = amountStaked[tokenId]; //@audit including zero? }
/// If the user has stake-at-risk for their fighter, reclaim a portion /// Reclaiming stake-at-risk puts the NRN back into their staking pool if (curStakeAtRisk > 0) { //@audit stake must be equal/greater than recovery +++ require(amountStaked[tokenId] >= curStakeAtRisk); _stakeAtRiskInstance.reclaimNRN(curStakeAtRisk, tokenId, fighterOwner); amountStaked[tokenId] += curStakeAtRisk; }
Invalid Validation
#0 - c4-pre-sort
2024-02-23T03:06:19Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-23T03:06:37Z
raymondfam marked the issue as duplicate of #51
#2 - c4-pre-sort
2024-02-26T02:23:52Z
raymondfam marked the issue as duplicate of #136
#3 - c4-judge
2024-03-08T04:05:44Z
HickupHH3 marked the issue as unsatisfactory: Invalid
#4 - c4-judge
2024-03-08T04:09:32Z
HickupHH3 marked the issue as unsatisfactory: Invalid
#5 - c4-judge
2024-03-08T04:09:34Z
HickupHH3 marked the issue as unsatisfactory: Invalid
#6 - c4-judge
2024-03-13T14:38:32Z
HickupHH3 marked the issue as not a duplicate
#7 - HickupHH3
2024-03-13T14:39:06Z
dup #833
#8 - c4-judge
2024-03-13T14:39:11Z
HickupHH3 marked the issue as duplicate of #1641
#9 - c4-judge
2024-03-13T14:39:16Z
HickupHH3 marked the issue as satisfactory
#10 - c4-judge
2024-03-19T09:00:15Z
HickupHH3 changed the severity to 2 (Med Risk)
π Selected for report: t0x1c
Also found by: 0xCiphky, 0xDetermination, Draiakoo, Greed, Kalogerone, MatricksDeCoder, MidgarAudits, MrPotatoMagic, PedroZurdo, Shubham, SpicyMeatball, VAD37, Velislav4o, ZanyBonzy, btk, cats, djxploit, forkforkdog, givn, ladboy233, lanrebayode77, lil_eth, visualbits, zaevlad
59.2337 USDC - $59.23
Within the GameItems contract, administrators possess the authority to generate game items and establish their prices. Furthermore, administrators dictate whether a game item is finite or infinite. To regulate the fair and balanced usage of game items, administrators also define the daily allowance for each item. If a player surpasses this daily limit, they must wait until the subsequent 24-hour period to make additional purchases.
However, the capability to transfer game items between addresses presents a challenge. Currently, there is no validation in place to ensure that the recipient address remains within the daily allowance limit set by the administrator. This oversight allows an address to acquire more than the prescribed daily allowance within a single day.
Moreover, this lack of validation carries additional implications, particularly concerning items with limited availability (finite). In scenarios where such items possess extraordinary attributes, resourceful users may purchase the entire supply and use them to climb leaderboard easily. This issue is particularly pronounced when these rare items confer significant advantages, such as protective shields or super gloves, effectively guaranteeing victory in battles.
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); }
Manual review
function safeTransferFrom( address from, address to, uint256 tokenId, uint256 amount, bytes memory data ) public override(ERC1155) { require(allGameItemAttributes[tokenId].transferable); +++ require(amount + balanceOf(to, tokenId) <= allowanceRemaining[msg.sender][tokenId], "Daily limit exceeded for the receiver"); super.safeTransferFrom(from, to, tokenId, amount, data); }
Invalid Validation
#0 - c4-pre-sort
2024-02-22T18:02:04Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-22T18:02:27Z
raymondfam marked the issue as duplicate of #43
#2 - c4-judge
2024-03-07T04:16:40Z
HickupHH3 marked the issue as satisfactory