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: 84/173
Findings: 2
Award: $21.29
🌟 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/RabbitHoleTickets.sol#L48
The onlyMinter
modifier actually do nothing, leads to no check of mint process.
Attacker can steal all reward token (erc20 and erc1155) by minting RabbitHoleReceipt as he wish, to many accounts he controlled. And then steal the reward tokens by simply calling claim
of Quest.
Similarly RabbitHoleTickets's onlyMinter is also vulnerable.
modifier onlyMinter() { msg.sender == minterAddress; _; }
This equal does not check whether it's true or false, which actually means do nothing.
This can be demonstrated by this simple contract test:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.15; contract TEST{ address public minterAddress; modifier onlyMinter() { msg.sender == minterAddress; _; } constructor(){ minterAddress = msg.sender; } function test() onlyMinter external{ } }
The address (not minterAddress) can successfully call test
function.
Anyone can call mint
function, so the attacker can create many RabbitHoleReceipt NFTs, and call claim
of Quest to steal all reward tokens.
Human code reading
change to:
modifier onlyMinter() { require(msg.sender == minterAddress, "only minter"); _; }
git diff provided below:
diff --git a/contracts/RabbitHoleReceipt.sol b/contracts/RabbitHoleReceipt.sol index 085b617..e561378 100644 --- a/contracts/RabbitHoleReceipt.sol +++ b/contracts/RabbitHoleReceipt.sol @@ -56,7 +56,7 @@ contract RabbitHoleReceipt is } modifier onlyMinter() { - msg.sender == minterAddress; + require(msg.sender == minterAddress, "not minter"); _; } diff --git a/test/RabbitHoleReceipt.spec.ts b/test/RabbitHoleReceipt.spec.ts index 1ab2e31..bbac00c 100644 --- a/test/RabbitHoleReceipt.spec.ts +++ b/test/RabbitHoleReceipt.spec.ts @@ -51,7 +51,10 @@ describe('RabbitholeReceipt Contract', async () => { expect(await RHReceipt.balanceOf(firstAddress.address)).to.eq(1) expect(await RHReceipt.questIdForTokenId(1)).to.eq('def456') - }) + }); + it('mint should revert for non-minter', async () => { + await expect(RHReceipt.connect(firstAddress).mint(firstAddress.address, 'def456')).to.be.revertedWith("not minter"); + }); }) // todo, this needs a quest created @@ -77,9 +80,9 @@ describe('RabbitholeReceipt Contract', async () => { describe('getOwnedTokenIdsOfQuest', () => { it('returns the correct tokenIds', async () => { - await RHReceipt.mint(contractOwner.address, 'abc123') - await RHReceipt.mint(contractOwner.address, 'def456') - await RHReceipt.mint(contractOwner.address, 'eeeeee') + await RHReceipt.connect(minterAddress).mint(contractOwner.address, 'abc123') + await RHReceipt.connect(minterAddress).mint(contractOwner.address, 'def456') + await RHReceipt.connect(minterAddress).mint(contractOwner.address, 'eeeeee') let tokenIds = await RHReceipt.getOwnedTokenIdsOfQuest('abc123', contractOwner.address)
#0 - c4-judge
2023-02-03T02:23:47Z
kirk-baird marked the issue as duplicate of #9
#1 - c4-judge
2023-02-03T02:23:52Z
kirk-baird marked the issue as satisfactory
🌟 Selected for report: carlitox477
Also found by: 0xRobocop, 0xbepresent, ArmedGoose, Atarpara, IllIllI, Tointer, UdarTeam, adriro, betweenETHlines, cryptojedi88, evan, glcanvas, gzeon, horsefacts, ladboy233, libratus, lukris02, luxartvinsec, manikantanynala97, minhquanym, mookimgo, p4st13r4, simon135, thekmj, trustindistrust
18.6976 USDC - $18.70
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Quest.sol#L99 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/RabbitHoleReceipt.sol#L117
Attacker can make a specific user unable to claim (or pay a lot of gas fee) by sending him a lot of claimed RabbitHoleReceipt. This will make RabbitHoleReceipt
's getOwnedTokenIdsOfQuest
function iterate through a lot of useless tokens, causing a lot of gas, incurring huge fee to the victim user. Even worse, if the required gas is bigger than block gaslimit, the victim can not claim his reward.
/// @dev get the token ids for a quest owned by an address /// @param questId_ the quest id /// @param claimingAddress_ the address claiming to own the tokens function getOwnedTokenIdsOfQuest( string memory questId_, address claimingAddress_ ) public view returns (uint[] memory) { uint msgSenderBalance = balanceOf(claimingAddress_); uint[] memory tokenIdsForQuest = new uint[](msgSenderBalance); uint foundTokens = 0; for (uint i = 0; i < msgSenderBalance; i++) { uint tokenId = tokenOfOwnerByIndex(claimingAddress_, i); if (keccak256(bytes(questIdForTokenId[tokenId])) == keccak256(bytes(questId_))) { tokenIdsForQuest[i] = tokenId; foundTokens++; } ...
This code will iterate all tokens hold by the msg.sender, including useless receipts that have been claimed. As long as the msgSenderBalance is big enough, it will cause a huge amount of gas for user, which can be achieved by malicious attacker sending a lot of RabbitHoleReceipt to the victim.
Human code reading
Change the claim function to add a tokenIds list variable, to specify which token ids to claim.
Do not use loop to iterate user balance NFTs.
#0 - c4-judge
2023-02-05T06:05:35Z
kirk-baird marked the issue as duplicate of #135
#1 - c4-judge
2023-02-05T06:05:40Z
kirk-baird marked the issue as partial-25
#2 - c4-judge
2023-02-14T09:17:56Z
kirk-baird marked the issue as satisfactory