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: 96/173
Findings: 1
Award: $18.70
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: AkshaySrivastav
Also found by: KIntern_NA, SovaSlava, Tointer, Tricko, V_B, __141345__, betweenETHlines, bin2chen, cccz, critical-or-high, glcanvas, halden, hihen, jesusrod15, ladboy233, libratus, m9800, minhquanym, omis, peakbolt, rbserver, romand, rvierdiiev, wait, zaskoh
18.6976 USDC - $18.70
If the some quest is created for multiple chain, the signature could be replayed. When the task requires certain amount of resources such as staking, the signature replay could trick the system, getting the rewards without doing the work and spending the cost.
Imagine, some quest is created for multiple chain, Ethereum, Polygon and BSC, and the task is similar on each chain, staking 1000 USDC for 3 month. As soon as the user finishes the task on Ethereum by taking 1000 USDC, the signature could be generated by the claimSigner/indexer. Then even if no USDC is staked on Polygon and BSC, the same signature could still be applied to mint the receipt and claim the rewards.
In mintReceipt()
, the chainid is not included, only the msg.sender
and questId
. Which means the same signature could be used for different chains multiple times, without the quest being performed.
File: quest-protocol/contracts/QuestFactory.sol 219: function mintReceipt(string memory questId_, bytes32 hash_, bytes memory signature_) public { 220: if (quests[questId_].numberMinted + 1 > quests[questId_].totalParticipants) revert OverMaxAllowedToMint(); 221: if (quests[questId_].addressMinted[msg.sender] == true) revert AddressAlreadyMinted(); 222: if (keccak256(abi.encodePacked(msg.sender, questId_)) != hash_) revert InvalidHash(); 223: if (recoverSigner(hash_, signature_) != claimSignerAddress) revert AddressNotSigned(); 224: 225: quests[questId_].addressMinted[msg.sender] = true; 226: quests[questId_].numberMinted++; 227: emit ReceiptMinted(msg.sender, questId_); 228: rabbitholeReceiptContract.mint(msg.sender, questId_); 229: }
Manual analysis.
Include the chain id in the signed hash in mintReceipt()
. And the signature from the claimSigner/indexer need to add this info also.
File: quest-protocol/contracts/QuestFactory.sol 219: function mintReceipt(string memory questId_, bytes32 hash_, bytes memory signature_) public { -222: if (keccak256(abi.encodePacked(msg.sender, questId_)) != hash_) revert InvalidHash(); +222: if (keccak256(abi.encodePacked(msg.sender, chainid(), questId_)) != hash_) revert InvalidHash();
#0 - c4-judge
2023-02-05T06:12:45Z
kirk-baird marked the issue as duplicate of #45
#1 - c4-judge
2023-02-14T09:36:08Z
kirk-baird changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-02-14T09:36:29Z
kirk-baird marked the issue as satisfactory