Platform: Code4rena
Start Date: 20/09/2022
Pot Size: $100,000 USDC
Total HM: 4
Participants: 109
Period: 7 days
Judge: GalloDaSballo
Id: 163
League: ETH
Rank: 44/109
Findings: 2
Award: $123.86
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0x4non, 0x52, 0x5rings, 0xNazgul, 0xRobocop, 0xSmartContract, 0xdeadbeef, 0xsanson, 8olidity, Amithuddar, Aymen0909, B2, B353N, CertoraInc, Ch_301, Chom, CodingNameKiki, Deivitto, ElKu, Funen, JC, JohnnyTime, Kresh, Lambda, Noah3o6, RaymondFam, ReyAdmirado, RockingMiles, Rolezn, Sm4rty, SuldaanBeegsi, Tadashi, TomJ, Tomio, V_B, Waze, __141345__, a12jmx, ak1, arcoun, asutorufos, aviggiano, berndartmueller, bharg4v, bin2chen, brgltd, bulej93, c3phas, catchup, cccz, ch0bu, cryptonue, cryptphi, csanuragjain, delfin454000, devtooligan, djxploit, durianSausage, eighty, erictee, exd0tpy, fatherOfBlocks, giovannidisiena, hansfriese, ignacio, joestakey, ladboy233, lukris02, m9800, malinariy, martin, minhtrng, obront, oyc_109, pedr02b2, pedroais, pfapostol, philogy, prasantgupta52, rbserver, ronnyx2017, rotcivegaf, rvierdiiev, sach1r0, shung, simon135, throttle, tnevler, tonisives, wagmi, yixxas, zkhorse, zzykxx, zzzitron
55.1985 USDC - $55.20
requestRandomSeed
should be called only once in 24 hours. But it can be called in less than 24 hours if the previous reveal timestamp doesn't exactly match with the nextRevealTimestamp
.
Looking at the requestRandomSeed function:
// The first two lines say: uint256 nextRevealTimestamp = gobblerRevealsData.nextRevealTimestamp; if (block.timestamp < nextRevealTimestamp) revert RequestTooEarly(); // the second line makes sure that we dont reveal again too soon.
But the mistake lies in how the nextRevealTimestamp
is set in the function.
gobblerRevealsData.nextRevealTimestamp = uint64(nextRevealTimestamp + 1 days);
Note that we are adding 1 day
to the previous nextRevealTimestamp
. But if the current block.timestamp
doesn't coincide with the previous nextRevealTimestamp
, then logically it means:
(next nextRevealTimestamp
- current block.timestamp
) < 1 day
For example if the current nextRevealTimestamp
is 8:30 am today. And if someone calls the requestRandomSeed
function at 12:30 pm, then the future nextRevealTimestamp
will be set at 8:30 am tomorrow instead of 12:30 pm tomorrow. Which means the seed
could be requested only after a delay of 20 hours.
Manual analysis, VSCode
Change line 535 to as shown below. Basically we use block.timestamp
instead of previous nextRevealTimestamp
in the calculation.
gobblerRevealsData.nextRevealTimestamp = uint64(block.timestamp + 1 days);
#0 - Shungy
2022-09-28T13:27:45Z
Duplicate: https://github.com/code-423n4/2022-09-artgobblers-findings/issues/372 I think the finding is lower severity as I stated here (second paragraph): https://github.com/code-423n4/2022-09-artgobblers-findings/issues/372#issuecomment-1260684497
#1 - GalloDaSballo
2022-10-03T20:07:22Z
I think that the specific issue is:
gobblerRevealsData.nextRevealTimestamp
will be in the pastThis property of the system is shown via this test
/// @notice Can request earlier if we are behind scheduled function testCanRevealFasterIfLate() public { // Mint and reveal first gobbler mintGobblerToAddress(users[0], 1); vm.warp(block.timestamp + 48 days); setRandomnessAndReveal(1, "seed"); // Attempt reveal before 24 hours have passed mintGobblerToAddress(users[0], 1); // Works because we are 47 days late gobblers.requestRandomSeed(); }
Which in contrast to testRevealDelayRecurring
doesn't revert, because we are behind schedule.
The finding is Valid, I'm not convinced by the Medium Severity, Low is definitely correct as the expectation from code, comment and test is broken, however Medium seems like a stretch as arguably this allows revealing more gobblers when the system is late, not during the expected 24 hour windows.
In other words, the reveals are still every 24 hours, but they are 24 hours since launch, not 24 hours since the previous reveal.
In a way this is better behaviour than waiting 24 hours "if we're fallen behind". I also think the finding hasn't shown any meaningful economic loss nor availability of service, if anything we're getting reveals relatively faster in this situation.
Given the information that I have, I think the finding is valid but I'll downgrade to Low as no loss was shown
While expectations have been broken, they seem to gracefully degrade toward a better outcome (faster reveal when behind schedule), instead of forcing the end user to wait. This without any demonstrated leak of value nor exploit.
For those reasons I'll downgrade to Low
#2 - GalloDaSballo
2022-10-03T20:08:12Z
L
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0xNazgul, 0xSmartContract, Atarpara, CertoraInc, Deathstore, Deivitto, ElKu, MiloTruck, ReyAdmirado, SnowMan, Tadashi, V_B, __141345__, aviggiano, catchup, djxploit, gogo, pfapostol, philogy, shung
68.6605 USDC - $68.66
 | Issue |
---|---|
1 | Optimizing for loop's in mintLegendaryGobbler and revealGobblers method |
2 | Cache state variable in legendaryGobblerPrice() method |
3 | Optimizing formula in mintReservedGobblers method |
mintLegendaryGobbler
and revealGobblers
method</ins>The mintLegendaryGobbler function in ArtGobblers.sol
can save gas by modifying the for loop
in the following way:
The following diff
shows the mitigation:
diff --git "a/.\\orig.txt" "b/.\\modified.txt" index 1d26328..a09619f 100644 --- "a/.\\orig.txt" +++ "b/.\\modified.txt" @@ -429,7 +429,9 @@ contract ArtGobblers is GobblersERC721, LogisticVRGDA, Owned, ERC1155TokenReceiv uint256 id; // Storing outside the loop saves ~7 gas per iteration. - for (uint256 i = 0; i < cost; ++i) { + for (uint256 i = 0; ; ++i) { + if(i == cost) break; //gas saving here. + id = gobblerIds[i]; if (id >= FIRST_LEGENDARY_GOBBLER_ID) revert CannotBurnLegendary(id);
The forge gas report shows an average saving of 36 (72774 - 72738) and a maximum saving of 168 (456707 - 456539) from this single change.
The same kind of change could be made in the for loop
in the revealGobblers function as well. The diff information shows the exact change:
diff --git "a/.\\orig.txt" "b/.\\modified.txt" index 1d26328..f755227 100644 --- "a/.\\orig.txt" +++ "b/.\\modified.txt" @@ -589,7 +589,8 @@ contract ArtGobblers is GobblersERC721, LogisticVRGDA, Owned, ERC1155TokenReceiv // Implements a Knuth shuffle. If something in // here can overflow, we've got bigger problems. unchecked { - for (uint256 i = 0; i < numGobblers; ++i) { + for (uint256 i = 0; ; ++i) { + if(i == numGobblers) break; /*////////////////////////////////////////////////////////////// DETERMINE RANDOM SWAP //////////////////////////////////////////////////////////////*/
The forge gas report shows an average saving of 64 (525805 - 525741) and a maximum saving of 303 (2907828 - 2907525) gas for the revealGobblers
function from this single change.
legendaryGobblerPrice()
method</ins>The legendaryGobblerPrice function in ArtGobblers.sol
can save gas by using the cached state variable in one of the formulas. The diff below shows the change.
diff --git "a/.\\orig.txt" "b/.\\modified.txt" index 1d26328..39f7a95 100644 --- "a/.\\orig.txt" +++ "b/.\\modified.txt" @@ -490,7 +490,7 @@ contract ArtGobblers is GobblersERC721, LogisticVRGDA, Owned, ERC1155TokenReceiv if (numMintedAtStart > mintedFromGoo) revert LegendaryAuctionNotStarted(numMintedAtStart - mintedFromGoo); // Compute how many gobblers were minted since the auction began. - uint256 numMintedSinceStart = numMintedFromGoo - numMintedAtStart; + uint256 numMintedSinceStart = mintedFromGoo - numMintedAtStart; // prettier-ignore // If we've minted the full interval or beyond it, the price has decayed to 0.
The forge gas report shows an average saving of 109 (1646 - 1537) gas for legendaryGobblerPrice
function from this single change.
mintReservedGobblers
method</ins>The mintReservedGobblers function in ArtGobblers.sol
can save gas by modifying one of the formulas. The diff below shows the change.
diff --git "a/.\\orig.txt" "b/.\\modified.txt" index 1d26328..2e5a111 100644 --- "a/.\\orig.txt" +++ "b/.\\modified.txt" @@ -841,7 +841,7 @@ contract ArtGobblers is GobblersERC721, LogisticVRGDA, Owned, ERC1155TokenReceiv // Optimistically increment numMintedForReserves, may be reverted below. Overflow in this // calculation is possible but numGobblersEach would have to be so large that it would cause the // loop in _batchMint to run out of gas quickly. Shift left by 1 is equivalent to multiplying by 2. - uint256 newNumMintedForReserves = numMintedForReserves += (numGobblersEach << 1); + uint256 newNumMintedForReserves = numMintedForReserves += (numGobblersEach + numGobblersEach); // Ensure that after this mint gobblers minted to reserves won't comprise more than 20% of // the sum of the supply of goo minted gobblers and the supply of gobblers minted to reserves.
The forge gas report shows an average saving of 3 gas (65982 - 65979) from this single change.
 | Issue | Max Gas Saved |
---|---|---|
1 | Optimizing for loop in mintLegendaryGobbler method | 168 |
2 | Optimizing for loop in revealGobblers method | 303 |
3 | Cache state variable in legendaryGobblerPrice() method | 109 |
4 | Optimizing formula in mintReservedGobblers method | 3 |
#0 - GalloDaSballo
2022-10-05T22:37:10Z
Amazing report
Will give 100 gas for the loop optimization which is inline with the rest of judged reports Rest will accept at face value as testament to the great report which shows code and benchmarks
515