Platform: Code4rena
Start Date: 03/05/2022
Pot Size: $30,000 USDC
Total HM: 6
Participants: 93
Period: 3 days
Judge: gzeon
Id: 118
League: ETH
Rank: 50/93
Findings: 2
Award: $48.66
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: defsec
Also found by: 0v3rf10w, 0x1f8b, 0x4non, 0x52, 0xDjango, 0xf15ers, 0xkatana, 0xliumin, AuditsAreUS, BowTiedWardens, CertoraInc, Cr4ckM3, Funen, GimelSec, Hawkeye, IllIllI, Kulk0, M0ndoHEHE, MaratCerby, Picodes, Ruhum, TerrierLover, TrungOre, VAD37, WatchPug, berndartmueller, broccolirob, catchup, cccz, cryptphi, csanuragjain, delfin454000, dirk_y, eccentricexit, ellahi, fatherOfBlocks, gzeon, hake, hansfriese, hickuphh3, horsefacts, hubble, hyh, ilan, joestakey, kebabsec, kenta, kenzo, leastwood, m9800, marximimus, minhquanym, oyc_109, p4st13r4, pauliax, pedroais, peritoflores, plotchy, rajatbeladiya, reassor, rfa, robee, rotcivegaf, samruna, shenwilly, shung, simon135, sorrynotsorry, sseefried, teddav, throttle, tintin, unforgiven, z3s
30.2807 USDC - $30.28
The mintonomics blog post and the contest readme both state that in the mintlist phase, minters will have 24 hours to mint 1 warrior each. But there is no such time limit in the code. Once the mintlist is open, it is open indefinitely.
Mintlist phase is not open for just 24 hours. Mintlisted addresses can mint from the mintlist even while the public sale is going on. So this will be unfair for the public participants. As the audit readme states "The goal of this code review and bounty is to ensure that the contract can perform safely according to the communities expectations and what is communicated in that blog post", I think this missing time limit can be considered incorrect implementation - function incorrect as to specs.
By looking at the mintlistSummon
function, one can see that there is no 24h time limit, unlike what the mintonomics blog post and the contest readme state that should be.
Add the time limit.
#0 - cryppadotta
2022-05-05T02:07:12Z
Ehhh - I see what you're saying but maybe it's better phrased as like, "minters will have an exclusive 24 hour period to mint 1 warrior each". It's not that the mintlist ends after 24 hours and, in fact, we denote this fact in the timeline in the more technical readme.
#1 - gzeoneth
2022-06-18T18:31:08Z
Downgrading to Low / QA.
#2 - gzeoneth
2022-06-18T19:34:34Z
Consider as warden's QA report.
🌟 Selected for report: BowTiedWardens
Also found by: 0v3rf10w, 0x1f8b, 0x4non, 0xDjango, 0xNazgul, 0xProf, 0xc0ffEE, 0xf15ers, 0xkatana, 0xliumin, ACai, AlleyCat, CertoraInc, Cityscape, Cr4ckM3, DavidGialdi, Dinddle, FSchmoede, Funen, GimelSec, Hawkeye, IllIllI, Kulk0, M0ndoHEHE, MaratCerby, MiloTruck, Picodes, RoiEvenHaim, Tadashi, TerrierLover, TrungOre, VAD37, WatchPug, antonttc, catchup, defsec, delfin454000, dirk_y, eccentricexit, ellahi, fatherOfBlocks, gzeon, hake, hansfriese, hickuphh3, horsefacts, ilan, joestakey, kebabsec, kenta, kenzo, marximimus, minhquanym, noobie, oyc_109, p4st13r4, pauliax, rajatbeladiya, reassor, rfa, robee, rotcivegaf, saian, samruna, shenwilly, shung, simon135, slywaters, sorrynotsorry, throttle, unforgiven, z3s
18.3762 USDC - $18.38
This simple change will save 44-60% of your gas, meaning 5-10 ETH.
This is the basic logic for refunding addresses (comment lines removed):
function _refundAddress(address minter) private { uint256 owed = refundOwed(minter); if (owed > 0) { daAmountRefunded[minter] += owed; _safeTransferETHWithFallback(minter, owed); } } function refundOwed(address minter) public view returns (uint256) { uint256 totalCostOfMints = finalPrice * daNumMinted[minter]; uint256 refundsPaidAlready = daAmountRefunded[minter]; return daAmountPaid[minter] - totalCostOfMints - refundsPaidAlready; }
To know that the minter has been refunded, instead of storing daAmountRefunded[minter]
- an expensive operation - we can update daAmountPaid[minter]
back to 0, to receive a gas refund instead.
This does mean that the amount the user originally paid is not saved any more after the refund, but there is no use for it.
daAmountPaid
is only used for issuing refunds, and will not be updated further after DA phase has ended, and the refunds won't start before DA phase has ended. So I believe it is fine to edit.
So I changed the functions to the following (note - now daAmountPaid
should probably be renamed to something like daPaymentToBoConsideredForRefund
):
function _refundAddress(address minter) private { uint256 owed = refundOwed(minter); if (owed > 0) { daAmountPaid[minter] = 0; _safeTransferETHWithFallback(minter, owed); } } function refundOwed(address minter) public view returns (uint256) { if (daAmountPaid[minter] == 0) { return 0; } uint256 totalCostOfMints = finalPrice * daNumMinted[minter]; return daAmountPaid[minter] - totalCostOfMints; }
According to the single refund gasPrint function, this change saves 44% of gas. From 109219 to 60236. When issuing 8000 refunds this translates to 5-10 ETH. Using the bulk 100-address gas test, the function shows even more savings - 59%.
For an additional small saving (roughly $240 for 8000 addresses), change the issueRefunds loop from this:
for (uint256 i = startIdx; i < endIdx + 1; i++) { _refundAddress(daMinters[i]); } }
to this:
for (uint256 i = startIdx; i <= endIdx;) { _refundAddress(daMinters[i]); unchecked { ++i; } }
#0 - gzeoneth
2022-06-22T06:16:55Z
Great saving but also break a few feature of the contract; e.g. if the owner call setFinalPrice to reduce the price after refund, refunded user won't be able to receive additional refund; the repurposing of daAmountPaid and removal of daAmountRefunded may also lead to some tracking issue given there are no events emitted