Platform: Code4rena
Start Date: 25/01/2023
Pot Size: $36,500 USDC
Total HM: 11
Participants: 173
Period: 5 days
Judge: kirk-baird
Total Solo HM: 1
Id: 208
League: ETH
Rank: 14/173
Findings: 5
Award: $457.23
π Selected for report: 0
π Solo Findings: 0
π Selected for report: adriro
Also found by: 0xMAKEOUTHILL, 0xMirce, 7siech, AkshaySrivastav, AlexCzm, Awesome, Aymen0909, Cryptor, Deivitto, DimitarDimitrov, ElKu, Garrett, Jayus, Josiah, Kenshin, KrisApostolov, RaymondFam, SovaSlava, Timenov, UdarTeam, amaechieth, btk, c3phas, codeislight, fellows, frankudoags, gzeon, hansfriese, luxartvinsec, millersplanet, mookimgo, navinavu, oberon, paspe, pavankv, petersspetrov, pfapostol, prestoncodes, rbserver, sakshamguruji, shark, thekmj, trustindistrust, tsvetanovv, usmannk, vagrant, vanko1, xAriextz, yosuke
2.5852 USDC - $2.59
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/RabbitHoleReceipt.sol#L58-L61 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/RabbitHoleReceipt.sol#L98-L104
The onlyMinter() modifier is not correctly coded, allowing anyone to call important functions such as mint()
. This means a malicious user can mint himself many nft receipts and then claim the rewards, thus clearing the Quest contract balance.
This is the onlyMinter
modifier. As you can see, there is no if statement or require
statement on this. Which means anybody can circumvent this modifier and call the supposedly restricted functions.
modifier onlyMinter() { msg.sender == minterAddress; _; }
Once a Quest has started a malicious user will call the mint function in RabbitHoleReceipt
contract to mint NFT receipts for himself. Right after he can call the claim() function from Quest contract to claim the reward tokens. Doing this in a repeated way, he can clear the Quest contract balance.
Note that the claim()
function doesnt check whether the quests[questId_].addressMinted[msg.sender]
is true
or not. This makes sense, because it allows the receipts to be traded in secondary markets. But with this particular vulnerability, it allows for a malicious user to reap profit.
VS code, Manual analysis
Correct the modifier as follows:
modifier onlyMinter { require(msg.sender == minterAddress); _; }
#0 - c4-judge
2023-02-05T05:17:25Z
kirk-baird marked the issue as duplicate of #9
#1 - c4-judge
2023-02-14T08:39:00Z
kirk-baird marked the issue as satisfactory
π Selected for report: adriro
Also found by: 0xMAKEOUTHILL, 0xMirce, 7siech, AkshaySrivastav, AlexCzm, Awesome, Aymen0909, Cryptor, Deivitto, DimitarDimitrov, ElKu, Garrett, Jayus, Josiah, Kenshin, KrisApostolov, RaymondFam, SovaSlava, Timenov, UdarTeam, amaechieth, btk, c3phas, codeislight, fellows, frankudoags, gzeon, hansfriese, luxartvinsec, millersplanet, mookimgo, navinavu, oberon, paspe, pavankv, petersspetrov, pfapostol, prestoncodes, rbserver, sakshamguruji, shark, thekmj, trustindistrust, tsvetanovv, usmannk, vagrant, vanko1, xAriextz, yosuke
2.5852 USDC - $2.59
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/RabbitHoleTickets.sol#L47-L50 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/RabbitHoleTickets.sol#L83-L99
The onlyMinter() modifier is not correctly coded, allowing anyone to call important functions such as mint()
and mintBatch()
. This means a malicious user can mint himself many ERC1155 reward tokens directly.
This is the onlyMinter
modifier. As you can see, there is no if statement or require
statement on this. Which means anybody can circumvent this modifier and call the supposedly restricted functions.
modifier onlyMinter() { msg.sender == minterAddress; _; }
Once the RabbitHoleTickets
contract is deployed the malicious user can mint as many tokens as he wants for himself. Causing unimaginable loss to users and the protocol.
VS code, Manual analysis
Correct the modifier as follows:
modifier onlyMinter { require(msg.sender == minterAddress); _; }
#0 - c4-judge
2023-02-05T05:17:36Z
kirk-baird marked the issue as duplicate of #9
#1 - c4-judge
2023-02-14T08:38:58Z
kirk-baird marked the issue as satisfactory
π Selected for report: adriro
Also found by: 0xRobocop, 0xmrhoodie, 0xngndev, AkshaySrivastav, ArmedGoose, Atarpara, Bauer, CodingNameKiki, ElKu, Garrett, HollaDieWaldfee, IllIllI, Iurii3, KIntern_NA, KmanOfficial, Lotus, M4TZ1P, MiniGlome, Ruhum, SovaSlava, bin2chen, bytes032, carrotsmuggler, cccz, chaduke, codeislight, cryptonue, doublesharp, evan, fs0c, glcanvas, gzeon, hansfriese, hihen, hl_, holme, horsefacts, ladboy233, lukris02, mahdikarimi, manikantanynala97, martin, mert_eren, mrpathfindr, omis, peakbolt, peanuts, prestoncodes, rbserver, rvierdiiev, sashik_eth, timongty, tnevler, trustindistrust, usmannk, wait, yixxas, zadaru13, zaskoh
0.7512 USDC - $0.75
There is no access control on withdrawFee() function in Erc20Quest contract. Even though the protocol fees are sent to the designated protocolFeeRecipient
, this function can be called multiple times by anyone, draining the Quest contract. This is more like a DoS attack, causing loss of time and effort for everyone involved and affected.
The attack path works like this:
protocolFeeRecipient
set by the factory.unclaimable
funds as well.Hardhat, VS code, Manual analysis
I suggest, once the protocol fees are withdrawn, it should be marked as such with a state variable. That is, if you want to stop the evil attacker from having the last laugh.
#0 - c4-judge
2023-02-05T04:34:31Z
kirk-baird marked the issue as duplicate of #23
#1 - c4-judge
2023-02-14T08:54:38Z
kirk-baird changed the severity to 3 (High Risk)
#2 - c4-judge
2023-02-14T09:00:17Z
kirk-baird marked the issue as satisfactory
π Selected for report: RaymondFam
Also found by: 0xMirce, AkshaySrivastav, AlexCzm, Aymen0909, BClabs, CodingNameKiki, ElKu, HollaDieWaldfee, Josiah, KIntern_NA, MiniGlome, StErMi, adriro, bin2chen, cccz, chaduke, csanuragjain, gzeon, hihen, holme, libratus, minhquanym, omis, peakbolt, peanuts, rbserver, rvierdiiev, timongty, ubermensch, usmannk, wait, zaskoh
7.046 USDC - $7.05
withdrawRemainingTokens() function allows the withdrawal of the whole Erc1155Quest contract balance. This means any users who haven't yet claimed their rewards will be unable to do so resulting in a fund loss
.
withdrawRemainingTokens
function looks like this:
function withdrawRemainingTokens(address to_) public override onlyOwner { super.withdrawRemainingTokens(to_); IERC1155(rewardToken).safeTransferFrom( address(this), to_, rewardAmountInWeiOrTokenId, IERC1155(rewardToken).balanceOf(address(this), rewardAmountInWeiOrTokenId), '0x00' ); }
You can see that the amount withdrawn is given by IERC1155(rewardToken).balanceOf(address(this), rewardAmountInWeiOrTokenId)
which is the contract balance for that particular tokenId.
If some users haven't claimed their rewards yet, they will be unable to do so after the withdrawRemainingTokens
is called.
In the Erc20Quest
contract the withdrawRemainingTokens function calculates the nonClaimableTokens
which is the allowed amount of tokens to withdraw. A similar logic should be applied, but is missing in the 1155 contract.
VS code, Manual analysis
I suggest, the nonClaimableTokens
to be calculated and adjusted when calculating how much tokens to withdraw after Quest ends. The logic from Erc20Quest contract can be used here as well.
#0 - c4-judge
2023-02-05T05:17:57Z
kirk-baird marked the issue as duplicate of #42
#1 - c4-judge
2023-02-10T05:03:11Z
kirk-baird changed the severity to QA (Quality Assurance)
#2 - c4-judge
2023-02-10T05:12:15Z
This previously downgraded issue has been upgraded by kirk-baird
#3 - c4-judge
2023-02-16T07:05:27Z
kirk-baird marked the issue as satisfactory
#4 - c4-judge
2023-02-23T23:49:21Z
kirk-baird changed the severity to 2 (Med Risk)
π Selected for report: carrotsmuggler
Also found by: AkshaySrivastav, ElKu, HollaDieWaldfee, Iurii3, KmanOfficial, adriro, bin2chen, evan, hansfriese, hl_, mert_eren, omis, peanuts
122.948 USDC - $122.95
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Erc20Quest.sol#L81-L87 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Erc20Quest.sol#L102-L104
withdrawFee() is used for sending the protocol fees to protocolFeeRecipient. And withdrawRemainingTokens() is used for transferring nonClaimableTokens
back to the Quest admin. In the withdrawRemainingTokens
function, when calculating the amount of tokens to withdraw, the protocolFee()
is deducted from it. But if the withdrawFee
was already called there is no need to deduct it. Thus Quest admin loses tokens equal to protocolFee
.
The unfortunate event happens as such:
withdrawFee()
function and the protocol fees are sent to the address set by the factory contract. So the balance of the contract is less now.uint256 nonClaimableTokens = IERC20(rewardToken).balanceOf(address(this)) - protocolFee() - unclaimedTokens;
protocolFee()
once again, which is already deducted from the contract balance.protocolFee
.VS code, Manual analysis
I suggest, once the protocol fees are withdrawn, it should be marked as such with a state variable. And then use this state variable to decide whether the protocolFee
should be deducted from the contract balance.
#0 - c4-judge
2023-02-05T05:02:06Z
kirk-baird marked the issue as duplicate of #42
#1 - c4-judge
2023-02-06T08:22:27Z
kirk-baird marked the issue as not a duplicate
#2 - c4-judge
2023-02-06T08:22:34Z
kirk-baird marked the issue as duplicate of #61
#3 - c4-judge
2023-02-14T10:01:15Z
kirk-baird marked the issue as satisfactory
#4 - c4-judge
2023-02-23T23:48:12Z
kirk-baird changed the severity to 2 (Med Risk)
π Selected for report: ladboy233
Also found by: 0x4non, 0xmrhoodie, CodingNameKiki, ElKu, StErMi, Tricko, adriro, rbserver
323.8877 USDC - $323.89
Once the user has received an ERC-721 RabbitHole Receipt token, he has two options - either to call the claim() function and get the reward tokens, or sell it to another user who is interested. Here, a malicious user can cheat the system and do both. He does this by selling his receipt nft first and in the same block, front running the sell transaction with a call to claim()
function.
The attack path works like this:
Note that this works only when the buyer of the NFT manually
checks if the receipt was not claimed and then initiates the buy transaction. If he uses a contract which does the check and buy in a single transaction, this attack path wont work.
VS code, Manual analysis
The claim() function doesn't ask the user to send his receipt NFT to the contract before sending the reward tokens. It only checks if the user currently owns it and its not been claimed yet.
I suggest the claim() function should receive the NFT receipt before sending the reward tokens to avoid this kind of attack.
#0 - c4-judge
2023-02-05T04:43:00Z
kirk-baird marked the issue as primary issue
#1 - c4-sponsor
2023-02-07T20:43:04Z
waynehoover marked the issue as disagree with severity
#2 - waynehoover
2023-02-07T20:43:22Z
This is an issue with whatever FE is displaying code from our smart contract, not an issue in the SC itself.
#3 - c4-judge
2023-02-14T09:15:10Z
kirk-baird marked the issue as satisfactory
#4 - c4-judge
2023-02-14T09:15:13Z
kirk-baird marked issue #119 as primary and marked this issue as a duplicate of 119