RabbitHole Quest Protocol contest - p4st13r4's results

A protocol to distribute token rewards for completing on-chain tasks.

General Information

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

RabbitHole

Findings Distribution

Researcher Performance

Rank: 95/173

Findings: 1

Award: $18.70

๐ŸŒŸ Selected for report: 0

๐Ÿš€ Solo Findings: 0

Awards

18.6976 USDC - $18.70

Labels

bug
2 (Med Risk)
satisfactory
duplicate-552

External Links

Lines of code

https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Quest.sol#L96 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Quest.sol#L99

Vulnerability details

Impact

Affected code:

The claim() function of a Quest calls into the RabbitHoleReceipt contract to check which receipts belong to the msg.sender for the given questId, and then proceeds to pay the unclaimed rewards to the user.

An issue may arise for users that participated in many quests and thus have a lot of past receipts, because the getOwnedTokenIdsOfQuest() function used in RabbitHoleReceipt iterates through all the userโ€™ past receipts, and then filters for the given questId. If the list of receipts gets too big, then an user wonโ€™t be able to be rewarded for all the present and future quests, since the claim() function will run out of gas

Proof of Concept

This is a test added in test/Erc20Quest.spec.ts, within the claim() group of tests:

it.only('should run out of gas if you partecipated in too many quests', async () => {
  // Mint 6k "past" receipts
  const receiptsPromises = []
  for (let i = 0; i < 6000; i++) {
    let randomQuestId = (Math.random() + 1).toString(36).substring(2)
    receiptsPromises.push(deployedRabbitholeReceiptContract.mint(owner.address, randomQuestId))
  }

  await Promise.all(receiptsPromises)

  // Mint one additional receipt for current quest
  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)
  expect(await deployedQuestContract.isClaimed(1)).to.equal(false)

  // @audit: Will run out of gas
  await deployedQuestContract.claim()
  expect(await deployedSampleErc20Contract.balanceOf(owner.address)).to.equal(rewardAmount)
  await ethers.provider.send('evm_increaseTime', [-86400])
})

The test fails because claim() exceeds 30M gas usage

Tools Used

Editor

Build a separate data structure in RabbitHoleReceipt to help deal with already redeemed receipts

#0 - c4-judge

2023-02-05T05:15:33Z

kirk-baird marked the issue as duplicate of #135

#1 - c4-judge

2023-02-14T09:18:01Z

kirk-baird marked the issue as satisfactory

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax ยฉ 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter