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: 71/93
Findings: 2
Award: $45.73
🌟 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
Low Risk Issues
Impact You are going to update the finalPrice manually before you refund when the DA fails to sell out, but the second phase will be started automatically right after the first phase is ended. So if you don't update the finalPrice on that timestamp, the finalPrice will be still 2.5 ETH(initial value) and it will revert when minters try to buy with the correct price. And if some minters(I think there will be no such a minter though) buy with 2.5 ETH then there is no method to refund because it doesn't save funds for each address from the second phase.
Proof of Concept The DA fails to sell out and the finalPrice isn't updated automatically. You don't update the finalPrice manually at the next timestamp. Then minters can't buy warriors with correct price until you set the finalPrice.
Recommended Mitigation Steps I see the duration between the first phase and the second one is 1 day and the price will be the lowestPrice before the first phase ends. (Actually within 380 mins with default settings) So you can modify to set the finalPrice when currentPrice = lowestPrice. You can add this code after #L160. if (currentPrice == lowestPrice && finalPrice != lowestPrice) { finalPrice = lowestPrice; } Then when you use default setting finalPrice will be set automatically in any cases. If you don't ensure the price will meet the lowestPrice during the first phase with different settings, I think it would be good to save the last price even though it cost more gas or you need to implement a logic to set the finalPrice automatically.
And I think it's related to gas optimizations but I want to explain here because I need to explain continuously. ForgottenRunesWarriorsMinter.sol#L145 With default settings, the price will be the lowestPrice within 380 mins and you don't need to calcualte currentPrice again and again once finalPrice = lowestPrice. So you can write like this instead of #L145. uint256 currentPrice = finalPrice == lowestPrice ? finalPrice : currentDaPrice();
Non-critical Issues
I know you will set correct start times for phases but I think it would be good to add one more require() as you added two already. ForgottenRunesWarriorsMinter.sol#487 require( newMintlistStartTime > newDaStartTime, 'Set mintlist after Da' );
NatSpec is incomplete @param is missing (You added @param for same calldata here: ForgottenRunesWarriorsMinter.sol#L169) ForgottenRunesWarriorsGuild.sol#L151 ForgottenRunesWarriorsGuild.sol#L156 ForgottenRunesWarriorsMinter.sol#L504 ForgottenRunesWarriorsMinter.sol#L512 ForgottenRunesWarriorsMinter.sol#L519 ForgottenRunesWarriorsMinter.sol#L526 ForgottenRunesWarriorsMinter.sol#L533 ForgottenRunesWarriorsMinter.sol#L542 ForgottenRunesWarriorsMinter.sol#L549 ForgottenRunesWarriorsMinter.sol#L556 ForgottenRunesWarriorsMinter.sol#L563 ForgottenRunesWarriorsMinter.sol#L570 ForgottenRunesWarriorsMinter.sol#L578 ForgottenRunesWarriorsMinter.sol#L585 ForgottenRunesWarriorsMinter.sol#L592 ForgottenRunesWarriorsMinter.sol#L599
It would be good to use (uint256).max instead of 64 fs. ForgottenRunesWarriorsMinter.sol#L18 ForgottenRunesWarriorsMinter.sol#L23 ForgottenRunesWarriorsMinter.sol#L27 ForgottenRunesWarriorsMinter.sol#L31 ForgottenRunesWarriorsMinter.sol#L35
#0 - kartoonjoy
2022-05-05T18:41:02Z
Warden Hanfriese has requested that section 3 be updated from 3. It would be good to use uint256(-1) instead of 64 fs. to 3. It would be good to use (uint256).max nstead of 64 fs.
🌟 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
15.4498 USDC - $15.45
You can modify daMinters not to save duplicate addresses in order to save gas. ForgottenRunesWarriorsMinter.sol#L151 There is a mapping that counts the number of minted warriors on that address already so you can add a new address only by checking this mapping. You can write instead of #L151. if (daNumMinted[msg.sender] == 0) { daMinters.push(msg.sender); } Then this array won't save duplicated addresses and it will be better when you refund later also.
0 is less efficient than != 0 for unsigned integers 1). ForgottenRunesWarriorsMinter.sol#L141 numWarriors > 0 && numWarriors <= 20, 2). ForgottenRunesWarriorsMinter.sol#L211 numWarriors > 0 && numWarriors <= 20, 3). ForgottenRunesWarriorsMinter.sol#L378 if (owed > 0) {
++i costs less gas compared to i++ or i += 1 1). ForgottenRunesWarriorsGuild.sol#104 numMinted += 1; 2). ForgottenRunesWarriorsMinter.sol#193 numSold += 1; 3). ForgottenRunesWarriorsMinter.sol#248 numClaimed += 1; 4). ForgottenRunesWarriorsMinter.sol#162 for (uint256 i = 0; i < numWarriors; i++) { 5). ForgottenRunesWarriorsMinter.sol#220 for (uint256 i = 0; i < numWarriors; i++) { 6). ForgottenRunesWarriorsMinter.sol#259 for (uint256 i = 0; i < count; i++) { 7). ForgottenRunesWarriorsMinter.sol#355 for (uint256 i = startIdx; i < endIdx + 1; i++) {