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: 60/173
Findings: 5
Award: $40.42
🌟 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
The onlyMinter check in RabbitHoleReceipt is moot, allowing anyone to pass this check to mint.
modifier onlyMinter() { msg.sender == minterAddress; _; }
diff --git a/test/RabbitHoleReceipt.spec.ts b/test/RabbitHoleReceipt.spec.ts index 1ab2e31..19e9531 100644 --- a/test/RabbitHoleReceipt.spec.ts +++ b/test/RabbitHoleReceipt.spec.ts @@ -75,11 +75,12 @@ 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') + describe.only('getOwnedTokenIdsOfQuest', () => { + it.only('only minter can mint', async () => { + await RHReceipt.connect(firstAddress).mint(contractOwner.address, 'abc123') + await RHReceipt.connect(firstAddress).mint(contractOwner.address, 'def456') + await RHReceipt.connect(firstAddress).mint(contractOwner.address, 'eeeeee') + expect(await RHReceipt.minterAddress()).eq(firstAddress.address) let tokenIds = await RHReceipt.getOwnedTokenIdsOfQuest('abc123', contractOwner.address)
firstAddress
is not a minter, but is able to mint; the above test failed with
1) RabbitholeReceipt Contract getOwnedTokenIdsOfQuest returns the correct tokenIds: AssertionError: expected '0x3C44CdDdB6a900fa2b585dd299e03d12FA4…' to equal '0x90F79bf6EB2c4f870365E785982E1f101E9…' + expected - actual -0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC +0x90F79bf6EB2c4f870365E785982E1f101E93b906
modifier onlyMinter() { require(msg.sender == minterAddress, "NOT_MINTER"); _; }
#0 - c4-judge
2023-02-05T05:39:46Z
kirk-baird marked the issue as duplicate of #9
#1 - c4-judge
2023-02-14T08:38:02Z
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
The onlyMinter check in RabbitHoleTickets is moot, allowing anyone to pass this check to mint.
modifier onlyMinter() { msg.sender == minterAddress; _; }
Same as the other issue regarding RabbitHoleReceipt but its a different contract without inheritance.
diff --git a/test/RabbitHoleTickets.spec.ts b/test/RabbitHoleTickets.spec.ts index e64e510..f95ede1 100644 --- a/test/RabbitHoleTickets.spec.ts +++ b/test/RabbitHoleTickets.spec.ts @@ -33,10 +33,11 @@ describe('RabbitholeTickets Contract', async () => { }) }) - describe('mint', () => { - it('mints 5 tokens', async () => { - await RHTickets.connect(minterAddress).mint(firstAddress.address, 1, 5, "0x"); - + describe.only('mint', () => { + it.only('mints 5 tokens', async () => { + await RHTickets.connect(firstAddress).mint(firstAddress.address, 1, 5, "0x"); + expect(await RHTickets.minterAddress()).eq(firstAddress.address)
firstAddress
is not a minter, but is able to mint; the above test failed with
1) RabbitholeTickets Contract mint mints 5 tokens: AssertionError: expected '0x3C44CdDdB6a900fa2b585dd299e03d12FA4…' to equal '0x90F79bf6EB2c4f870365E785982E1f101E9…' + expected - actual -0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC +0x90F79bf6EB2c4f870365E785982E1f101E93b906
modifier onlyMinter() { require(msg.sender == minterAddress, "NOT_MINTER"); _; }
#0 - c4-judge
2023-02-05T05:40:16Z
kirk-baird marked the issue as duplicate of #9
#1 - c4-judge
2023-02-14T08:38:01Z
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
Admin can drain all token in ERC20Quest by calling withdrawFee multiple times and receive more token than it is entitled to.
function withdrawFee() public onlyAdminWithdrawAfterEnd { IERC20(rewardToken).safeTransfer(protocolFeeRecipient, protocolFee()); }
diff --git a/test/Erc20Quest.spec.ts b/test/Erc20Quest.spec.ts index d0626ee..729544a 100644 --- a/test/Erc20Quest.spec.ts +++ b/test/Erc20Quest.spec.ts @@ -363,6 +363,8 @@ describe('Erc20Quest', async () => { await ethers.provider.send('evm_increaseTime', [100001]) await deployedQuestContract.withdrawFee() + // Admin steal more fee by calling withdrawFee again + await deployedQuestContract.withdrawFee() expect(await deployedSampleErc20Contract.balanceOf(deployedQuestContract.address)).to.equal( totalRewardsPlusFee * 100 - 1 * 1000 - 200
The test will fail because the quest contract have less than expected balance (since fee is drawn twice)
Only allow the admin to call withdrawFee once.
#0 - c4-judge
2023-02-05T05:33:51Z
kirk-baird marked the issue as duplicate of #23
#1 - c4-judge
2023-02-14T08:59: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
The for loop in Quest.claim is unbounded which might run over block gas limit when there is a lot of reward to claim. The call to claim will revert and user will lose their NFT although they were rewarded.
for (uint i = 0; i < tokens.length; i++) { if (!isClaimed(tokens[i])) { redeemableTokenCount++; } }
quest-protocol/test/Erc20Quest.spec.ts
it.only('should only transfer the correct amount of rewards', async () => { const nRewards = 2000 for (let i = 0; i < nRewards; i++) { await deployedRabbitholeReceiptContract.mint(owner.address, questId) } await deployedQuestContract.start() await ethers.provider.send('evm_increaseTime', [86400]) expect(await deployedSampleErc20Contract.balanceOf(owner.address)).to.equal(0) const totalTokens = await deployedRabbitholeReceiptContract.getOwnedTokenIdsOfQuest(questId, owner.address) expect(totalTokens.length).to.equal(nRewards) expect(await deployedQuestContract.isClaimed(1)).to.equal(false) await deployedQuestContract.claim() expect(await deployedSampleErc20Contract.balanceOf(owner.address)).to.equal(rewardAmount * nRewards) await ethers.provider.send('evm_increaseTime', [-86400]) })
Allow user to claim specific reward token id or bound the maximum number of reward per user
#0 - c4-judge
2023-02-05T05:33:10Z
kirk-baird marked the issue as duplicate of #135
#1 - c4-judge
2023-02-14T09:17:58Z
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
Erc1155Quest allow owner to withdraw all balance from the Quest contract anytime, unlike in Erc20Quest where the owner's ability to withdraw token is significantly restricted (owner can only withdraw protocol fee, and remaining token less claimable portion). Although this is an privileged function, the way that its implementation deviated from the other Quest contract Erc20Quest made this Med RIsk since one may expect proper restriction in terms of admin withdrawing reward token when using ERC1155 token as ERC20 token.
function withdrawRemainingTokens(address to_) public override onlyOwner { super.withdrawRemainingTokens(to_); IERC1155(rewardToken).safeTransferFrom( address(this), to_, rewardAmountInWeiOrTokenId, IERC1155(rewardToken).balanceOf(address(this), rewardAmountInWeiOrTokenId), '0x00' ); }
Implement checks as appeared in Erc20Quest
#0 - c4-judge
2023-02-05T05:41:44Z
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:14Z
This previously downgraded issue has been upgraded by kirk-baird
#3 - c4-judge
2023-02-14T09:28:04Z
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: IllIllI
Also found by: 0x1f8b, 0x4non, 0xAgro, 0xSmartContract, 0xhacksmithh, 0xngndev, Aymen0909, Bnke0x0, Breeje, Deivitto, Diana, Dug, Iurii3, LethL, MiniGlome, NoamYakov, RaymondFam, ReyAdmirado, Rolezn, SAAJ, adriro, ali, arialblack14, atharvasama, c3phas, carlitox477, catellatech, chaduke, cryptonue, cryptostellar5, ddimitrov22, dharma09, doublesharp, favelanky, georgits, glcanvas, gzeon, halden, horsefacts, jasonxiale, joestakey, karanctf, lukris02, matrix_0wl, nadin, navinavu, saneryee, shark, thekmj
11.3269 USDC - $11.33
Some struct can be packed tighter to save storage slot, for example:
struct Quest { mapping(address => bool) addressMinted; address questAddress; uint totalParticipants; uint numberMinted; }
can be packed into
struct Quest { mapping(address => bool) addressMinted; address questAddress; uint48 totalParticipants; uint48 numberMinted; }
To save 2 storage slot, while still allow 2^48 participant and reward to be minted which should be reasonably enough.
Quest contracts are deployed directly from the factory, which can be expensive. Consider deploying proxy to a shared Quest implementation to reduce deployment gas cost. Although this will increase each call to the contract slightly.
hash_ is redundant since it can be calculated with other input
function mintReceipt(string memory questId_, bytes32 hash_, bytes memory signature_) public { if (quests[questId_].numberMinted + 1 > quests[questId_].totalParticipants) revert OverMaxAllowedToMint(); if (quests[questId_].addressMinted[msg.sender] == true) revert AddressAlreadyMinted(); if (keccak256(abi.encodePacked(msg.sender, questId_)) != hash_) revert InvalidHash();
Instead of abi.encodePacked twice, and wasting gas copying the data into memory twice,
function recoverSigner(bytes32 hash_, bytes memory signature_) public pure returns (address) { bytes32 messageDigest = keccak256(abi.encodePacked('\x19Ethereum Signed Message:\n32', hash_)); return ECDSAUpgradeable.recover(messageDigest, signature_); }
hash it here altogether, the actual message signed would need to be slightly changed (instead of signing a typed hash, sign the typed message)
if (keccak256(abi.encodePacked(msg.sender, questId_)) != hash_) revert InvalidHash();
#0 - c4-judge
2023-02-16T07:00:44Z
kirk-baird marked the issue as grade-b