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: 3/173
Findings: 7
Award: $1,692.67
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 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
All funds might be stilled to protocolFeeRecipient address, and it might be impossible to return them back.
When contest finishes:
/// @notice Prevents reward withdrawal until the Quest has ended modifier onlyAdminWithdrawAfterEnd() { if (block.timestamp < endTime) revert NoWithdrawDuringClaim(); _; }
anyone can call withdrawFee()
.
This function withdrawFee()
sends protocolFee()
funds to protocolFeeRecipient
address.
But after transaction executed and funds transferred contract allows to call this function once again.
Possible attack, which can at least ruin the reputation.
withdrawFee()
as many times as possible.Manual audit
withdrawFee()
function twice.bool feeWithdrawn; function withdrawFee() public onlyAdminWithdrawAfterEnd reentrancyGuard { require(!feeWithdrawn, "already withdrawn"); IERC20(rewardToken).safeTransfer(protocolFeeRecipient, protocolFee()); feeWithdrawn = true; }
#0 - c4-judge
2023-02-05T07:43:51Z
kirk-baird marked the issue as duplicate of #23
#1 - c4-judge
2023-02-14T08:56:44Z
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
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Erc20Quest.sol#L96-L98 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Erc20Quest.sol#L89-L93 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Erc20Quest.sol#L100-L104
More than enough funds might be redeemed from ERC20Quest.
withdrawFee
has been executed.withdrawFee
has been executed. -- But here protocolFeeRecipient will receive fees for TWO users, Although it should has received only for one.Manual audit
As I mentioned early add possibility to call withdrawFee()
only once.
And in protocolFee()
instead of calculating fees only for redeemed users calculates fee over all users.
function protocolFee() public view returns (uint256) { return (maxTotalRewards() * rewardAmountInWeiOrTokenId * questFee) / 10_000; }
But your idea may be to send commissions only for those users who participated.
In this case, you need to allow withdrawFee()
to be called as many times as you want, but at the same time maintain the number of commissions already withdrawn and the number of participants who have collected their awards.
Like this:
uint256 receivedFeesFoUsers; function withdrawFee() public onlyAdminWithdrawAfterEnd { // TODO DON'T FORGET ADD REENTRANCY GUARD uint256 newUsers = receiptRedeemers() - receivedFeesFoUsers; receivedFeesFoUsers = receiptRedeemers(); uint256 fee = (newUsers * rewardAmountInWeiOrTokenId * questFee) / 10_000; IERC20(rewardToken).safeTransfer(protocolFeeRecipient, fee); }
#0 - c4-judge
2023-02-05T07:45:07Z
kirk-baird marked the issue as duplicate of #23
#1 - c4-judge
2023-02-14T08:56:42Z
kirk-baird marked the issue as satisfactory
🌟 Selected for report: adriro
Also found by: 0xRobocop, 0xbepresent, Breeje, CodingNameKiki, HollaDieWaldfee, Kenshin, M4TZ1P, Ruhum, Tricko, badman, bin2chen, carrotsmuggler, cccz, csanuragjain, glcanvas, joestakey, lukris02, m9800, mert_eren, peakbolt, peanuts, prestoncodes, rvierdiiev, sashik_eth
21.6061 USDC - $21.61
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Erc20Quest.sol#L78-L87 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Erc20Quest.sol#L89-L93 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Erc20Quest.sol#L95-L98 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/QuestFactory.sol#L215-L229 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Quest.sol#L94-L118
Users has possibility to claim after owner called withdrawRemainingTokens
. It leads to to the misunderstanding on the part of the participants to the organizers.
Let's imagine totalParticipants = 100
, mintedTokens = 10
, redeemed tokens = 4
.
Let's consider withdrawRemainingTokens(address to_)
function from ERC20Quest contract. This function must be called only by the owner and after quest finished.
Next, owner called this function, and transfer remaining to the another address.
On the quest's address remaining amount only for 6 participants.
Now another one users calls mintReceipt
function from QuestFactory contract => mintedTokens increased => now users may claim more than 6 tokens. but there is only money left for 6 people and no more.
Manual audit
Add flag isRemainingWithdrawn
to ERC20Quest. Set this flag when withdrawRemainingTokens(address to_)
called.
and during processing mintReceipt check this flag and revert if remaining withdrawn already happened.
#0 - c4-judge
2023-02-05T12:04:43Z
kirk-baird marked the issue as duplicate of #42
#1 - c4-judge
2023-02-06T08:27:07Z
kirk-baird marked the issue as not a duplicate
#2 - c4-judge
2023-02-06T08:27:14Z
kirk-baird marked the issue as duplicate of #22
#3 - c4-judge
2023-02-14T08:44:26Z
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#L94-L118 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/RabbitHoleReceipt.sol#L106-L135
User might buy huge amount of tokens and executing claim()
function will be cost a lot of gas and it will be too expensive and possible to get out of gas error.
Or even the transaction may not fit in the block.
Let's consider function claim
and Quest
contract.
rabbitHoleReceiptContract
uint[] memory tokens = rabbitHoleReceiptContract.getOwnedTokenIdsOfQuest(questId, msg.sender);
uint256 redeemableTokenCount = 0; for (uint i = 0; i < tokens.length; i++) { if (!isClaimed(tokens[i])) { redeemableTokenCount++; } }
In point 3 and point 4 we may iterates over unbounding amount of tokens. Which might lead to out of gas error.
Manual audit
add threshold
parameter to claim
function, and to getOwnedTokenIdsOfQuest
function.
If processed tokens reach threshold parameter interrupt iteration and process fetched tokens.
#0 - c4-judge
2023-02-06T08:28:38Z
kirk-baird marked the issue as duplicate of #135
#1 - c4-judge
2023-02-14T09:17:44Z
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/RabbitHoleReceipt.sol#L106-L135 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/RabbitHoleReceipt.sol#L95-L104
If the user often participates in quests, buys tokens he accumulates tokens that remain on his account. This leads to huge transactions cost after a while.
When user want to claim rewards Quest contract calls getOwnedTokenIdsOfQuest
which iterates over all tokens which user has. All iteration cost a gas, so after a while user will have a lot of outdated tokens, but the algorithm still will be iterates over them.
for (uint i = 0; i < msgSenderBalance; i++) { uint tokenId = tokenOfOwnerByIndex(claimingAddress_, i); if (keccak256(bytes(questIdForTokenId[tokenId])) == keccak256(bytes(questId_))) { tokenIdsForQuest[i] = tokenId; foundTokens++; } }
And this function calls from Quest's claim()
transaction, therefore getOwnedTokenIdsOfQuest
may use enormous gas amount.
Manual Audit
In Quest contract when calling claim function, each claimed token must be burned.
#0 - c4-judge
2023-02-06T08:51:12Z
kirk-baird marked the issue as duplicate of #135
#1 - c4-judge
2023-02-14T09:17:06Z
kirk-baird changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-02-14T09:17:42Z
kirk-baird marked the issue as satisfactory
🌟 Selected for report: glcanvas
Also found by: adriro, hansfriese, libratus
1604.382 USDC - $1,604.38
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Quest.sol#L13 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Quest.sol#L44 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Quest.sol#L96-L118 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/RabbitHoleReceipt.sol#L95-L104 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/QuestFactory.sol#L215-L229
Might be impossible to claim rewards by users. And admins must distribute tokens manually and pay fee for this. On a huge amount of participants this leads to huge amount of fees.
Let's consider QuestFactory
. It has:
RabbitHoleReceipt public rabbitholeReceiptContract;
Which responsible for mint tokens for users.
Then consider createQuest
function. Here we pass rabbitholeReceiptContract
into Quest
.
In Quest
this field is immutable.
Now lets consider next case:
rabbitholeReceiptContract
in QuestFactory
for another. To do this we call: setRabbitHoleReceiptContract
. And successfully changing address.QuestFactory
storages new address of rabbitholeReceiptContract
, but Quest
initialized with older one. So users successfully minted their tokens, but can't exchange them for tokens because the Quest's receipt contract know nothing about minted tokens.Possible solution here is change minterAddress
in the original RabbitHoleReceipt
contract and manually mint tokens by admin, but it will be too expensive and the company may lost a lot of money.
Manual audit
In QuestFactory
contract in the function mintReceipt
the rabbitholeReceiptContract must be fetched from the quest directly.
To Quest
Add:
function getRabbitholeReceiptContract() public view returns(RabbitHoleReceipt) { return rabbitHoleReceiptContract; }
Modify mintReceipt
function in QuestFactory
like:
function mintReceipt(string memory questId_, bytes32 hash_, bytes memory signature_) public { ... RabbitHoleReceipt rabbitholeReceiptContract = Quest(quests[questId_].questAddress).getRabbitholeReceiptContract(); rabbitholeReceiptContract.mint(msg.sender, questId_); ... }
#0 - c4-judge
2023-02-06T22:41:06Z
kirk-baird marked the issue as primary issue
#1 - c4-sponsor
2023-02-07T20:21:10Z
waynehoover marked the issue as disagree with severity
#2 - waynehoover
2023-02-07T20:21:56Z
Since our contract is upgradeable, they have to trust us that we aren’t going to do this during live quests. This was an emergency function, and likely won’t ever need to be used and only be accessible by only owner/us.
#3 - kirk-baird
2023-02-15T21:43:27Z
This is a valid issue as upgrading the receipt contract will break currently open quests to prevent minting of receipts. This does not result in a loss of funds as they can be recovered by the quest creator.
Additionally, it is only accessible by the admin and so I'm going to downgrade this to a medium.
#4 - c4-judge
2023-02-15T21:43:36Z
kirk-baird changed the severity to 2 (Med Risk)
#5 - c4-judge
2023-02-15T21:43:47Z
kirk-baird marked the issue as satisfactory
#6 - c4-judge
2023-02-15T21:45:35Z
kirk-baird marked the issue as selected for report
🌟 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
Current implementation of token vesting not resistant to:
The same thing can happen if you deploy factory to the same network but to a new address, because your signature does not depend on the factory address.
Manual audit
Use spec: https://eips.ethereum.org/EIPS/eip-712
See how it's implemented in Uniswap V2: https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2ERC20.sol https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2ERC20.sol#L29-L37 https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2ERC20.sol#L81-L93
Do the same.
#0 - c4-judge
2023-02-06T08:34:44Z
kirk-baird marked the issue as duplicate of #45
#1 - c4-judge
2023-02-06T08:34:49Z
kirk-baird marked the issue as partial-25
#2 - kirk-baird
2023-02-06T08:34:58Z
Partial credit as this is very poorly described.
#3 - c4-judge
2023-02-14T09:36:08Z
kirk-baird changed the severity to 2 (Med Risk)
#4 - c4-judge
2023-02-14T09:36:21Z
kirk-baird marked the issue as satisfactory
🌟 Selected for report: CodingNameKiki
Also found by: 0x1f8b, 0x4non, 0x5rings, 0xAgro, 0xMirce, 0xRobocop, 0xSmartContract, 0xackermann, AkshaySrivastav, ArmedGoose, Aymen0909, BClabs, Breeje, Dewaxindo, Diana, HollaDieWaldfee, IceBear, IllIllI, Iurii3, Josiah, Krayt, PaludoX0, Phenomana, PrasadLak, RaymondFam, Rolezn, SaeedAlipoor01988, SaharDevep, SleepingBugs, adriro, arialblack14, bin2chen, brevis, btk, carlitox477, carrotsmuggler, catellatech, chaduke, chrisdior4, cryptonue, cryptostellar5, csanuragjain, ddimitrov22, fellows, glcanvas, halden, hl_, horsefacts, jat, joestakey, kenta, libratus, lukris02, luxartvinsec, manikantanynala97, martin, matrix_0wl, nadin, nicobevi, oberon, peanuts, prestoncodes, rbserver, sakshamguruji, sashik_eth, sayan, seeu, thekmj, tnevler, trustindistrust, tsvetanovv, xAriextz, zadaru13, zaskoh
17.196 USDC - $17.20
Instead of 10_000 create constant anf use it.
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Quest.sol#L96 claims to msg.sender, but address might be compromised, and therefore you might be want to send funds for another one address.
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Erc1155Quest.sol#L11-L64 by EIP https://eips.ethereum.org/EIPS/eip-1155 ERC1155 supports ERC721 and ERC20, but current implementation support only ERC721. Think about rewriting code to single Quest with ERC1155.
Quest
abstract.mark _transferRewards
, _calculateRewards
as abstract.
_disableInitializers
in QuestFactory
Use _disableInitializers
instead of initializer
modifier in constructor.
questId
to questName
Current naming misleading, under Id implied uint256 or some integer field not string.
grantDefaultAdminAndCreateQuestRole
privatehttps://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/RabbitHoleReceipt.sol#L175-L177 Royalty from ERC2981 specification.
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/RabbitHoleReceipt.sol#L35 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/RabbitHoleReceipt.sol#L36 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/RabbitHoleTickets.sol#L25
toHexString
for address instead of for uint256generateAttribute('Reward Address', Strings.toHexString(rewardAddress_))
toHexString
convert a checksummed address into a non-checksummedYou may use EIP-55: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md#specification
remave
to remove
onlyAdminWithdrawAfterEnd
this modifier should be named as withdrawAfterEnd
QuestFactory
architecturePlease, create two different functions like: createErc20Quest
and createErc1155Quest
, it
improves readability, it removes comparing with contractType_
. Because further when you add
more types createQuest
will grow to a huge size with a huge copy paste and in which it will be
easy to make a mistake.
rewardAmountOrTokenId_
It's hard to understand, better split to two variables rewardsInWei
, rewardTokenId
. Then
create in Quest
mark getRewardAmount()
as abstract. Then for Erc1155Quest
create
variable rewardTokenId
. Then for Erc20Quest
create rewardsInWei
variable and use
them instead of misleading rewardAmountInWeiOrTokenId
.
Please add events for all functions which change state. For example: setRabbitHoleReceiptContract
, setProtocolFeeRecipient
and so on.
QuestFactory
#0 - c4-judge
2023-02-05T05:50:52Z
kirk-baird marked the issue as grade-b
#1 - c4-sponsor
2023-02-08T01:44:48Z
jonathandiep marked the issue as sponsor confirmed
🌟 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
While burning token here: https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/RabbitHoleReceipt.sol#L140 fields in mappings does not clearing https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/RabbitHoleReceipt.sol#L30 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/RabbitHoleReceipt.sol#L34 Recommendation: you may save up to 1/5 of transaction cost with gas refund if rewriting burn function with:
super._burn(tokenId_); delete questIdForTokenId[tokenId_]; delete questIdForTokenId[timestampForTokenId];
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Erc20Quest.sol#L81
withdrawRemainingTokens
from Quest.sol
already has onlyOwner
modifier.
Recommendation:
Remove onlyOwner
modifier from ERC20Quest.sol
's withdrawRemainingTokens
function.
mapping(uint256 => bool) private claimedList;
You're writing information about claim token in a single slot it cost 20_000 gas. You may Bitmap (like in UniswapV3 https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/TickBitmap.sol) and save up to 15_000 gas per token further. Recommendation: Rewrite to map like this
mapping(uint256 => uint256) private claimedList;
Add function position:
function position(uint256 tokenId) private pure returns (int16 wordPos, uint8 bitPos) { wordPos = int16(tokenId >> 8); bitPos = uint8(tokenId % 256); }
Next add function to check is token claimed and function to claim token. Use https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/TickBitmap.sol as a sample.
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/QuestFactory.sol#L43
With current implementation __AccessControl_init
has empty body and doesn't have any side
effects.
Recommendation:
Remove call:
__AccessControl_init();
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/QuestFactory.sol#L72 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/QuestFactory.sol#L105 All samples above has strings which are encodes and hashes, but result from call to call will be the same. Recommendation: Make it constant and use futher
bytes32 private constant ERC11555_HASH = keccak256(abi.encodePacked('erc1155')); bytes32 private constant ERC20_HASH = keccak256(abi.encodePacked('erc20'));
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/QuestFactory.sol#L219
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/QuestFactory.sol#L222
Field hash_
is redundant because it already contains in signature
by signing
algorithm definition.
Recommendation:
Instead of comparing:
if (keccak256(abi.encodePacked(msg.sender, questId_)) != hash_) revert InvalidHash();
You may just create hash, pass it to recoverSigner
function and compare
with claimSignerAddress
.
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/RabbitHoleReceipt.sol#L17
Contract ERC721Upgradeable
already contains in ERC721EnumerableUpgradeable
and ERC721URIStorageUpgradeable
.
Recommendation:
Remove definition of ERC721Upgradeable
it saves gas during deploy and improves readability.
mapping(uint => string) public questIdForTokenId;
Recommendation: Remove this mapping and this map filling.
if (keccak256(bytes(questIdForTokenId[tokenId])) == keccak256(bytes(questId_)))
You're comparing questIdForTokenId[tokenId])
with questId_
on each cycle and
use keccak256
for questId_
.
You can make hashing before the loop and just compare hash result in loop.
Recommendation:
Rewrite like:
bytes32 hash = keccak256(bytes(questId_)); for (uint i = 0; i < msgSenderBalance; i++) { uint tokenId = tokenOfOwnerByIndex(claimingAddress_, i); if (keccak256(bytes(questIdForTokenId[tokenId])) == hash) { tokenIdsForQuest[i] = tokenId; foundTokens++; } }
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/RabbitHoleReceipt.sol#L109-L135
In this function you iterate over whole sender's token twice. But you need two iterations: The
first one over all user's tokens, the second one over filtered tokens only.
Recommendation:
Write into last free position of tokenIdsForQuest
instead of actual position. Rewrite like
this:
for (uint i = 0; i < msgSenderBalance; i++) { uint tokenId = tokenOfOwnerByIndex(claimingAddress_, i); if (keccak256(bytes(questIdForTokenId[tokenId])) == keccak256(bytes(questId_))) { tokenIdsForQuest[foundTokens] = tokenId; foundTokens++; } }
Then rewrite second loop to:
for (uint i = 0; i < foundTokens; i++) { filteredTokens[i] = tokenIdsForQuest[i]; }
use next pattern:
Quest memory quest = quests[questID_];
Next read from this struct, but remember that you must write into storage
QuestFactory
side to save gasCheck that time is in range to save gas if checks failed
#0 - c4-judge
2023-02-05T05:44:19Z
kirk-baird marked the issue as grade-b