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: 37/173
Findings: 2
Award: $128.55
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
The pragma version used are:
pragma solidity ^0.8.15;
Note that mixing pragma is not recommended. Because different compiler versions have different meanings and behaviors, it also significantly raises maintenance costs. As a result, depending on the compiler version selected for any given file, deployed contracts may have security issues.
The minimum required version must be 0.8.17; otherwise, contracts will be affected by the following important bug fixes:
Apart from these, there are several minor bug fixes and improvements.
supportsInterface
The EIP-165
standard helps detect that a smart contract implements the expected logic, prevents human error when configuring smart contract bindings, so it is recommended to check that the received argument is a contract and supports the expected interface.
Reference:
Affected source code:
The code that contains "open todos" reflects that the development is not finished and that the code can change a posteriori, prior release, with or without audit.
Affected source code:
// TODO clean this whole thing up
Although it has not been possible to exploit the reentrancy attack, the logic of the methods named below, make the changes of the validation flags after a method that allows reentrancy, it is convenient to modify the flags before the external calls to contracts.
In the Quest.claim
method, the redeemedTokens
state variable is modified after an external contract is called, so if that contract allows a reentrancy attack, the attack vector is possible. It's better to apply the following changes:
... + redeemedTokens += redeemableTokenCount; _transferRewards(totalRedeemableRewards); - redeemedTokens += redeemableTokenCount; emit Claimed(msg.sender, totalRedeemableRewards); }
Affected source code:
#0 - c4-sponsor
2023-02-08T07:14:45Z
jonathandiep marked the issue as sponsor acknowledged
#1 - c4-judge
2023-02-16T06:59:04Z
kirk-baird marked the issue as grade-b
🌟 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
111.3544 USDC - $111.35
Bundling consecutive strings together to prevent encodePacked
from doing so can save gas
bytes memory svg = abi.encodePacked( + '<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350"><style>.base { fill: white; font-family: serif; font-size: 14px; }</style><rect width="100%" height="100%" fill="black" /><text x="50%" y="40%" class="base" dominant-baseline="middle" text-anchor="middle">RabbitHole Tickets #', - '<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350">', - '<style>.base { fill: white; font-family: serif; font-size: 14px; }</style>', - '<rect width="100%" height="100%" fill="black" />', - '<text x="50%" y="40%" class="base" dominant-baseline="middle" text-anchor="middle">RabbitHole Tickets #', tokenId_.toString(), + '</text></svg>' - '</text>', - '</svg>' );
Affected source code:
In red the previous version, y green the new one:
- | RabbitHoleReceipt · - · - · 2776002 · [90m9.3 %[39m · [32m[90m-[32m[39m │ + | RabbitHoleReceipt · - · - · 2775990 · [90m9.3 %[39m · [32m[90m-[32m[39m │ ················································|·············|·············|··············|···············|·············· - | ReceiptRenderer · - · - · 1186932 · [90m4 %[39m · [32m[90m-[32m[39m │ + | ReceiptRenderer · - · - · 1123645 · [90m3.7 %[39m · [32m[90m-[32m[39m │
createQuest
It is possible to optimize the gas cost of the createQuest
method by avoiding the keccak256
computation.
+ bytes32 private constant ERC20_HASH = keccak256(abi.encodePacked('erc20')); + bytes32 private constant ERC1155_HASH = keccak256(abi.encodePacked('erc1155')); function createQuest( address rewardTokenAddress_, uint256 endTime_, uint256 startTime_, uint256 totalParticipants_, uint256 rewardAmountOrTokenId_, string memory contractType_, string memory questId_ ) public onlyRole(CREATE_QUEST_ROLE) returns (address) { if (quests[questId_].questAddress != address(0)) revert QuestIdUsed(); - if (keccak256(abi.encodePacked(contractType_)) == keccak256(abi.encodePacked('erc20'))) { + if (keccak256(abi.encodePacked(contractType_)) == ERC20_HASH) { if (rewardAllowlist[rewardTokenAddress_] == false) revert RewardNotAllowed(); ... - if (keccak256(abi.encodePacked(contractType_)) == keccak256(abi.encodePacked('erc1155'))) { + if (keccak256(abi.encodePacked(contractType_)) == ERC1155_HASH) { if (msg.sender != owner()) revert OnlyOwnerCanCreate1155Quest();
Affected source code:
In red the previous version, y green the new one:
- | QuestFactory · - · - · 5313516 · [90m17.7 %[39m · [32m[90m-[32m[39m │ + | QuestFactory · - · - · 5313504 · [90m17.7 %[39m · [32m[90m-[32m[39m │
mintReceipt
It is possible to optimize the gas cost of the mintReceipt
method by avoiding multiple storage accesses.
function mintReceipt(string memory questId_, bytes32 hash_, bytes memory signature_) public { + Quest storage quest = quests[questId_]; - if (quests[questId_].numberMinted + 1 > quests[questId_].totalParticipants) revert OverMaxAllowedToMint(); - if (quests[questId_].addressMinted[msg.sender] == true) revert AddressAlreadyMinted(); + if (quest.numberMinted + 1 > quest.totalParticipants) revert OverMaxAllowedToMint(); + if (quest.addressMinted[msg.sender]) revert AddressAlreadyMinted(); if (keccak256(abi.encodePacked(msg.sender, questId_)) != hash_) revert InvalidHash(); if (recoverSigner(hash_, signature_) != claimSignerAddress) revert AddressNotSigned(); - quests[questId_].addressMinted[msg.sender] = true; - quests[questId_].numberMinted++; + quest.addressMinted[msg.sender] = true; + quest.numberMinted++; emit ReceiptMinted(msg.sender, questId_); rabbitholeReceiptContract.mint(msg.sender, questId_); }
Also hash_
argument can be removed, and be computed.
Affected source code:
In red the previous version, y green the new one:
mintReceipt - $43.22 (288,104) 1st mint + $43.02 (286,799) 1st mint - $41.49 (276,604) 2st mint + $41.29 (275,299) 2st mint - Range: $41.49 - $43.22 + Range: $41.29 - $43.02 - | QuestFactory · - · - · 5313516 · [90m17.7 %[39m · [32m[90m-[32m[39m │ + | QuestFactory · - · - · 5286756 · [90m17.6 %[39m · [32m[90m-[32m[39m │
getOwnedTokenIdsOfQuest
It is possible to optimize the gas cost of the getOwnedTokenIdsOfQuest
method by avoiding the keccak256
on each iteration.
function getOwnedTokenIdsOfQuest( string memory questId_, address claimingAddress_ ) public view returns (uint[] memory) { uint msgSenderBalance = balanceOf(claimingAddress_); + if (msgSenderBalance == 0) return new uint[](0); uint[] memory tokenIdsForQuest = new uint[](msgSenderBalance); uint foundTokens = 0; + bytes32 questHash = keccak256(bytes(questId_)); for (uint i = 0; i < msgSenderBalance; i++) { uint tokenId = tokenOfOwnerByIndex(claimingAddress_, i); - if (keccak256(bytes(questIdForTokenId[tokenId])) == keccak256(bytes(questId_))) { + if (keccak256(bytes(questIdForTokenId[tokenId])) == questHash) { tokenIdsForQuest[i] = tokenId; foundTokens++; } } uint[] memory filteredTokens = new uint[](foundTokens); uint filterTokensIndexTracker = 0; for (uint i = 0; i < msgSenderBalance; i++) { if (tokenIdsForQuest[i] > 0) { filteredTokens[filterTokensIndexTracker] = tokenIdsForQuest[i]; filterTokensIndexTracker++; } } return filteredTokens; }
Affected source code:
Compare a boolean value using == true
or == false
instead of using the boolean value is more expensive.
NOT
opcode it's cheaper than using EQUAL
or NOTEQUAL
when the value it's false, or just the value without == true
when it's true, because it will use less opcodes inside the VM.
Proof of concept (without optimizations):
pragma solidity 0.8.16; contract TesterA { function testEqual(bool a) public view returns (bool) { return a == true; } } contract TesterB { function testNot(bool a) public view returns (bool) { return a; } }
Gas saving executing: 18 per entry for == true
TesterA.testEqual: 21814 TesterB.testNot: 21796
pragma solidity 0.8.16; contract TesterA { function testEqual(bool a) public view returns (bool) { return a == false; } } contract TesterB { function testNot(bool a) public view returns (bool) { return !a; } }
Gas saving executing: 15 per entry for == false
TesterA.testEqual: 21814 TesterB.testNot: 21799
Affected source code:
Use the value instead of == true
:
In red the previous version, y green the new one:
claim - $19.00 (126,644) 1st claim + $18.99 (126,632) 1st claim - Range: $19.00 - $19.00 + Range: $18.99 - $18.99 mintReceipt - $43.22 (288,104) 1st mint + $43.21 (288,095) 1st mint - $41.49 (276,604) 2st mint + $41.49 (276,595) 2st mint - Range: $41.49 - $43.22 + Range: $41.49 - $43.21 ··························· createQuest - $204.89 (1,365,913) Create ERC1155 quest + $204.59 (1,363,910) Create ERC1155 quest - $204.89 (1,365,913) Create ERC1155 quest + $204.59 (1,363,910) Create ERC1155 quest - $226.26 (1,508,398) Create ERC20 quest + $225.96 (1,506,382) Create ERC20 quest - $226.26 (1,508,398) Create ERC20 quest + $225.96 (1,506,382) Create ERC20 quest - Range: $204.89 - $226.26 + Range: $204.59 - $225.96 - | QuestFactory · - · - · 5313516 · [90m17.7 %[39m · [32m[90m-[32m[39m │ + | QuestFactory · - · - · 5307411 · [90m17.7 %[39m · [32m[90m-[32m[39m │
Using compound assignment operators for state variables (like State += X
or State -= X
...) it's more expensive than using operator assignment (like State = State + X
or State = State - X
...).
Proof of concept (without optimizations):
pragma solidity 0.8.15; contract TesterA { uint256 private _a; function testShort() public { _a += 1; } } contract TesterB { Uint256 private _a; function testLong() public { _a = _a + 1; } }
Gas saving executing: 13 per entry
TesterA.testShort: 43507 TesterB.testLong: 43494
Affected source code:
In red the previous version, y green the new one:
claim - $19.00 (126,644) 1st claim + $18.99 (126,628) 1st claim - Range: $19.00 - $19.00 + Range: $18.99 - $18.99 createQuest - $204.89 (1,365,913) Create ERC1155 quest + $204.71 (1,364,713) Create ERC1155 quest - $204.89 (1,365,913) Create ERC1155 quest + $204.71 (1,364,713) Create ERC1155 quest - $226.26 (1,508,398) Create ERC20 quest + $226.08 (1,507,194) Create ERC20 quest - $226.26 (1,508,398) Create ERC20 quest + $226.08 (1,507,194) Create ERC20 quest - Range: $204.89 - $226.26 + Range: $204.71 - $226.08 - | QuestFactory · - · - · 5313516 · [90m17.7 %[39m · [32m[90m-[32m[39m │ + | QuestFactory · - · - · 5310936 · [90m17.7 %[39m · [32m[90m-[32m[39m │
#0 - c4-judge
2023-02-15T21:59:39Z
kirk-baird marked the issue as grade-b
#1 - c4-judge
2023-02-15T22:00:04Z
kirk-baird marked the issue as grade-a