Forgotten Runes Warrior Guild contest - kenzo's results

16,000 Warrior NFTs sold in a phased Dutch Auction.

General Information

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

Forgotten Runes

Findings Distribution

Researcher Performance

Rank: 50/93

Findings: 2

Award: $48.66

🌟 Selected for report: 0

🚀 Solo Findings: 0

Lines of code

https://github.com/code-423n4/2022-05-runes/blob/main/contracts/ForgottenRunesWarriorsMinter.sol#L171:#L195

Vulnerability details

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.

Impact

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.

Proof of Concept

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.

Improving issueRefunds (AKA: almost paying for your C4 audit)

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%.

issueRefunds loop

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

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter