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: 33/93
Findings: 3
Award: $134.87
🌟 Selected for report: 0
🚀 Solo Findings: 0
48.5872 USDC - $48.59
Judge has assessed an item in Issue #270 as Medium risk. The relevant finding follows:
payable.send
may be too low for contract walletsETH withdrawals in both the minter and token contracts use payable(address).send
to transfer ether to the vault address. If the configured vault is a smart contract wallet like a Gnosis Safe multisig, the gas stipend from .send
may be too low to complete the transfer and these methods may revert with an out of gas error. There are workarounds to this issue, but it could be an unexpected interruption.
If your vault address is already known, consider simulating transfers out of these contracts using a fork test or transaction simulator to ensure they will succeed.
Alternatively, consider reusing the existing _safeTransferETH
function. Using .call
can introduce reentrancy, but these are permissioned calls to a trusted address that change no state variables.
ForgottenRunesWarriorsMinter.sol#L604-L619
/** * @notice Withdraw funds to the vault * @param _amount uint256 the amount to withdraw */ function withdraw(uint256 _amount) public onlyOwner { require(address(vault) != address(0), 'no vault'); require(payable(vault).send(_amount)); } /** * @notice Withdraw all funds to the vault */ function withdrawAll() public payable onlyOwner { require(address(vault) != address(0), 'no vault'); require(payable(vault).send(address(this).balance)); }
ForgottenRunesWarriorsGuild.sol#L159-L165
/** * @dev ETH should not be sent to this contract, but in the case that it is * sent by accident, this function allows the owner to withdraw it. */ function withdrawAll() public payable onlyOwner { require(payable(msg.sender).send(address(this).balance)); }
#0 - gzeoneth
2022-06-18T19:19:17Z
Duplicate of #254
🌟 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
52.771 USDC - $52.77
Thanks for the detailed explanations of your design decisions and reasoning and the walkthrough video you created for this contest!
payable.send
may be too low for contract walletsETH withdrawals in both the minter and token contracts use payable(address).send
to transfer ether to the vault address. If the configured vault is a smart contract wallet like a Gnosis Safe multisig, the gas stipend from .send
may be too low to complete the transfer and these methods may revert with an out of gas error. There are workarounds to this issue, but it could be an unexpected interruption.
If your vault address is already known, consider simulating transfers out of these contracts using a fork test or transaction simulator to ensure they will succeed.
Alternatively, consider reusing the existing _safeTransferETH
function. Using .call
can introduce reentrancy, but these are permissioned calls to a trusted address that change no state variables.
ForgottenRunesWarriorsMinter.sol#L604-L619
/** * @notice Withdraw funds to the vault * @param _amount uint256 the amount to withdraw */ function withdraw(uint256 _amount) public onlyOwner { require(address(vault) != address(0), 'no vault'); require(payable(vault).send(_amount)); } /** * @notice Withdraw all funds to the vault */ function withdrawAll() public payable onlyOwner { require(address(vault) != address(0), 'no vault'); require(payable(vault).send(address(this).balance)); }
ForgottenRunesWarriorsGuild.sol#L159-L165
/** * @dev ETH should not be sent to this contract, but in the case that it is * sent by accident, this function allows the owner to withdraw it. */ function withdrawAll() public payable onlyOwner { require(payable(msg.sender).send(address(this).balance)); }
setLowestPrice
The contract owner can set lowestPrice
to a value greater than startPrice
. This will cause price calculations to revert with a checked arithmetic error.
This would interrupt bidding, but does not prevent the owner from setting parameters back to valid values.
The owner can also set lowestPrice
to zero, which would allow users to mint for free if the auction price decreases low enough.
Consider validating these parameters.
ForgottenRunesWarriorsMinter.sol#L554-L559
/** * @notice set the dutch auction lowest price */ function setLowestPrice(uint256 _newPrice) public onlyOwner { require(_newPrice > 0 && _newPrice < startPrice, "Invalid lowest price"); lowestPrice = _newPrice; }
setDaDropInterval
The contract owner can set daDropInterval
to a value greater than daPriceCurveLength
, causing step calculations to round down to zero. This will cause auction price calculations to revert with a division by zero error.
This would interrupt bidding, but does not prevent the owner from setting parameters back to valid values.
Consider validating these parameters.
ForgottenRunesWarriorsMinter.sol#L568-L574
/** * @notice set how long it takes for the dutch auction to step down in price */ function setDaDropInterval(uint256 _newTime) public onlyOwner { require(daDropInterval > 0 && daDropInterval < daPriceCurveLength, "Invalid drop interval"); daDropInterval = _newTime; }
safeTransfer
for IERC20
The forwardERC20s
function calls IERC20.transfer
without checking the return value. The transfer may fail silently without reverting. Consider using OpenZeppelin SafeERC20 and safeTransfer
.
ForgottenRunesWarriorsMinter.sol#forwardERC20s
/** * @dev ERC20s should not be sent to this contract, but if someone * does, it's nice to be able to recover them * @param token IERC20 the token address * @param amount uint256 the amount to send */ function forwardERC20s(IERC20 token, uint256 amount) public onlyOwner { require(address(msg.sender) != address(0)); token.transfer(msg.sender, amount); }
Consider adding and emitting events when the contract owner changes sensitive parameters, like the merkle roots, phase times, prices and vault address in ForgottenRunesWarriorsMinter.sol
and the base URI, minter address, and provenance hash in ForgottenRunesWarriorsGuild.sol
. This enables you to monitor off chain for suspicious activity, and allows end users to observe and trust changes to these parameters.
It's possible to use type(uint256).max
rather than a numeric literal to access the maximum value of uint256
. This is more concise, and is a safeguard against accidentally deleting a stray f
:
ForgottenRunesWarriorsMinter.sol#L16-L18
/// @notice The start timestamp for the Dutch Auction (DA) sale and price uint256 public daStartTime = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
Suggested:
/// @notice The start timestamp for the Dutch Auction (DA) sale and price uint256 public daStartTime = type(uint256).max;
The forwardERC20s
function in both contracts checks that msg.sender
is not the zero address. The intent here is not clear, but this is very unlikely to be the case. Perhaps this was meant to zero check address(token)
instead:
ForgottenRunesWarriorsMinter.sol#forwardERC20s
/** * @dev ERC20s should not be sent to this contract, but if someone * does, it's nice to be able to recover them * @param token IERC20 the token address * @param amount uint256 the amount to send */ function forwardERC20s(IERC20 token, uint256 amount) public onlyOwner { require(address(msg.sender) != address(0)); token.transfer(msg.sender, amount); }
ForgottenRunesWarriorsMinter.sol
and ForgottenRunesWarriorsGuild.sol
have floating version pragmas. To prevent compiling with an unexpected Solidity version, set these explicitly.
ForgottenRunesWarriorsMinter.sol
and ForgottenRunesWarriorsGuild.sol
are missing license identifiers.
🌟 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
33.5137 USDC - $33.51
Two big ideas and a few minor ones. If you're willing to use smaller types for counting bids per user and prices, using a packed struct to store auction bid data can reduce bid and refund gas costs. Even if not, increasing the number of optimizer runs will reduce runtime gas costs for bids, mints, and refunds.
In the long and less impactful tail, you can optimize a few loops, change a few increments, and inline a couple variables.
Using a packed struct to store a user's auction bid data saves significant gas by reducing storage reads in bidSummon
, issueRefunds
, and selfRefund
.
We need to store three values related to a user's auction bid:
daNumMinted
, the total number of tokens minted in the auction phase by addressdaAmountPaid
, the total amount paid by addressdaAmountRefunded
, the total amount refunded by addressCurrently, we use three (address => uint256)
mappings to store these values, so each one uses a full storage slot:
ForgottenRunesWarriorsMinter.sol#L75
/// @notice Tracks the total amount paid by a given address in the DA mapping(address => uint256) public daAmountPaid; /// @notice Tracks the total amount refunded to a given address for the DA mapping(address => uint256) public daAmountRefunded; /// @notice Tracks the total count of NFTs minted by a given address in the DA mapping(address => uint256) public daNumMinted;
However, this data can comfortably fit into smaller types, so we can store all three values in a struct that will use a single storage slot. We want to find integer types for these values that can pack together into 256 bits.
There is a known upper bound on daNumMinted
: even if a single address claims all possible tokens, it won't exceed the maxDaSupply
of 8000. Even if the auction supply increases, it can't exceed the fixed total supply of 16000. This means we can safely fit each user's daNumMinted
into a uint16
(max value 65535). We can't go lower, as it's possible for one address to mint more than type(uint8).max
(255) tokens.
This leaves us to find a packable type for the two prices: daAmountPaid
and daAmountRefunded
. We have 240 bits remaining (256 - 16), and could use a uint120
for these two values. However, since we're going to be casting integer values, it's a good idea to use OpenZeppelin's SafeCast
library, which checks for overflow when we convert a type. The largest type this library supports that will pack with daNumMinted
in a single slot is uint96
.
Is this big enough for daAmountPaid
and daAmountRefunded
? The uint96
type stores a max value equivalent to around 79 billion ether:
» type(uint96).max 79228162514264337593543950335 » type(uint96).max / (1 ether) 79228162514
However, we need enough room to multiply number minted by final price and fit the result in a uint96
:
ForgottenRunesWarriorsMinter.sol#388
function refundOwed(address minter) public view returns (uint256) { uint256 totalCostOfMints = finalPrice * daNumMinted[minter]; uint256 refundsPaidAlready = daAmountRefunded[minter]; return daAmountPaid[minter] - totalCostOfMints - refundsPaidAlready; }
Let's take the more generous upper bound on daNumMinted
: even if the mint supply parameters change, nobody can possibly mint more than 16000. So what is the max finalPrice
value that fits? We want to solve for uint96 totalCostOfMints = finalPrice * 16000
, so we can take type(uint96).max / 16000
(and convert it to ether):
» type(uint96).max / (16000 ether) 4951760
As long as we keep final price constrained to a value less than 4,951,760 ETH, there should be room to calculate prices using our packed data. Since it's likely to actually be between 2.5 and 0.6 ETH, we're all good.
We can replace the three mappings above with a single "bids" mapping and packed structs:
struct Bid { uint16 daNumMinted; uint96 daAmountPaid; uint96 daAmountRefunded; } mapping(address => Bid) public daBids;
Since these were previously public state variables with default getters, we can introduce view functions to read these values if necessary:
function daNumMinted(address bidder) external view returns (uint256) { return daBids[bidder].daNumMinted; } function daAmountPaid(address bidder) external view returns (uint256) { return daBids[bidder].daAmountPaid; } function daAmountRefunded(address bidder) external view returns (uint256) { return daBids[bidder].daAmountRefunded; }
The current code in ForgottenRunesWarriorsMinter#bidSummon
will perform two SLOAD
s and two SSTORE
s to update the bid data by user: one read from storage, and one write to storage of the updated value per mapping.
daMinters.push(msg.sender); daAmountPaid[msg.sender] += msg.value; daNumMinted[msg.sender] += numWarriors; numSold += numWarriors;
We can save an SSTORE
by updating the bid in memory and writing it once:
daMinters.push(msg.sender); Bid memory bid = daBids[msg.sender]; bid.daAmountPaid += SafeCast.toUint96(msg.value); bid.daNumMinted += SafeCast.toUint16(numWarriors); daBids[msg.sender] = bid; numSold += numWarriors;
Remember to import the SafeCast
library:
import "@openzeppelin/contracts/utils/math/SafeCast.sol";
SafeCast will revert with an error in this function if msg.value
or numWarriors
cannot be safely downcast. We know numWarriors
is less than 20 since we check in a require
statement. msg.value
could theoretically be too big, but it would require sending 79 billion ETH. (Congratulations on your mint!)
We'll also need to update refunds to read from the new struct.
Here's the existing code:
ForgottenRunesWarriorsMinter.sol#L376
function _refundAddress(address minter) private { uint256 owed = refundOwed(minter); if (owed > 0) { daAmountRefunded[minter] += owed; _safeTransferETHWithFallback(minter, owed); } } /** * @notice returns the amount owed the address * @param minter address the address of the account that wants a refund */ function refundOwed(address minter) public view returns (uint256) { uint256 totalCostOfMints = finalPrice * daNumMinted[minter]; uint256 refundsPaidAlready = daAmountRefunded[minter]; return daAmountPaid[minter] - totalCostOfMints - refundsPaidAlready; }
The refundOwed
function uses all three bid values: daNumMinted
, daAmountPaid
, and daAmountRefunded
. Instead of four SLOAD
s, we can load the bid with just two SLOAD
s (one for the whole bid struct, and one for finalPrice
). This has a significant impact when iterating over many refunds:
/** * @notice returns the amount owed the address * @param minter address the address of the account that wants a refund */ function refundOwed(address minter) public view returns (uint256) { Bid memory bid = daBids[minter]; return bid.daAmountPaid - (finalPrice * bid.daNumMinted) - bid.daAmountRefunded; }
We won't save gas in _refundAddress
(this still requires one SLOAD
to read and one SSTORE
to set), but we do need to update the code to cast the calculated refund amount to uint96
:
function _refundAddress(address minter) private { uint256 owed = refundOwed(minter); if (owed > 0) { daBids[minter].daAmountRefunded += SafeCast.toUint96(owed); _safeTransferETHWithFallback(minter, owed); } }
This is a big change, but it has a significant impact, especially on the gas cost of refunds.
Gas report before:
·------------------------------------------------------------|---------------------------|-------------|-----------------------------· | Solc version: 0.8.6 · Optimizer enabled: true · Runs: 200 · Block limit: 30000000 gas │ ·····························································|···························|·············|······························ | Methods │ ·································|···························|·············|·············|·············|···············|·············· | Contract · Method · Min · Max · Avg · # calls · usd (avg) │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsGuild · burn · - · - · 31294 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsGuild · initialize · 46261 · 46273 · 46272 · 90 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsGuild · mint · 61624 · 95824 · 82999 · 4 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsGuild · setBaseURI · - · - · 31993 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsGuild · setMinter · - · - · 46157 · 3 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsGuild · setProvenanceHash · - · - · 46855 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsGuild · uploadAttributes · - · - · 23987 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsGuild · uploadImage · - · - · 23975 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsGuild · withdrawAll · - · - · 23593 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · bidSummon · 158591 · 755809 · 285939 · 18 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · claimSummon · - · - · 158179 · 6 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · issueRefunds · 69745 · 143756 · 115139 · 5 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · mintlistSummon · 126115 · 160315 · 151765 · 8 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · pause · - · - · 27732 · 2 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · publicSummon · 110840 · 654549 · 490754 · 20 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · selfRefund · - · - · 62257 · 2 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setClaimlistMerkleRoot · - · - · 46109 · 5 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setClaimsStartTime · - · - · 28703 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setDaDropInterval · - · - · 28637 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setDaPriceCurveLength · - · - · 28660 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setDaStartTime · - · - · 28657 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setFinalPrice · 28723 · 28783 · 28779 · 15 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setMaxDaSupply · - · - · 28701 · 3 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setMaxForClaim · - · - · 28679 · 2 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setMaxForSale · - · - · 28636 · 3 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setMintlist1MerkleRoot · - · - · 46195 · 9 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setMintlist2MerkleRoot · - · - · 46174 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setMintlistStartTime · - · - · 28680 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setPhaseTimes · 36598 · 44998 · 43987 · 50 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setPublicStartTime · - · - · 28679 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setSelfRefundsStartTime · 28702 · 28738 · 28720 · 2 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setStartPrice · - · - · 28637 · 2 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setVaultAddress · 26270 · 29070 · 28510 · 5 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setWarriorsAddress · - · - · 29007 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setWethAddress · - · - · 29004 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · teamSummon · 103853 · 623637 · 363745 · 4 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · unpause · - · - · 27748 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · withdraw · - · - · 35364 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · withdrawAll · - · - · 35074 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | Deployments · · % of limit · │ ·····························································|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsGuild · - · - · 2023631 · 6.7 % · - │ ·····························································|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · 2738151 · 2738175 · 2738173 · 9.1 % · - │ ·------------------------------------------------------------|-------------|-------------|-------------|---------------|-------------·
Gas report after:
·------------------------------------------------------------|---------------------------|-------------|-----------------------------· | Solc version: 0.8.6 · Optimizer enabled: true · Runs: 200 · Block limit: 30000000 gas │ ·····························································|···························|·············|······························ | Methods │ ·································|···························|·············|·············|·············|···············|·············· | Contract · Method · Min · Max · Avg · # calls · usd (avg) │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsGuild · burn · - · - · 31294 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsGuild · initialize · 46261 · 46273 · 46272 · 90 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsGuild · mint · 61624 · 95824 · 82999 · 4 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsGuild · setBaseURI · - · - · 31993 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsGuild · setMinter · - · - · 46157 · 3 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsGuild · setProvenanceHash · - · - · 46855 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsGuild · uploadAttributes · - · - · 23987 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsGuild · uploadImage · - · - · 23975 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsGuild · withdrawAll · - · - · 23593 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · bidSummon · 154292 · 734410 · 268340 · 18 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · claimSummon · - · - · 158201 · 6 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · issueRefunds · 48980 · 122991 · 86068 · 5 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · mintlistSummon · 126048 · 160248 · 151698 · 8 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · pause · - · - · 27754 · 2 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · publicSummon · 110862 · 654571 · 490776 · 20 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · selfRefund · - · - · 41492 · 2 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setClaimlistMerkleRoot · - · - · 46109 · 5 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setClaimsStartTime · - · - · 28703 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setDaDropInterval · - · - · 28659 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setDaPriceCurveLength · - · - · 28660 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setDaStartTime · - · - · 28679 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setFinalPrice · 28636 · 28696 · 28692 · 15 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setMaxDaSupply · - · - · 28723 · 3 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setMaxForClaim · - · - · 28701 · 2 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setMaxForSale · - · - · 28636 · 3 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setMintlist1MerkleRoot · - · - · 46109 · 9 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setMintlist2MerkleRoot · - · - · 46196 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setMintlistStartTime · - · - · 28702 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setPhaseTimes · 36598 · 44998 · 43987 · 50 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setPublicStartTime · - · - · 28701 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setSelfRefundsStartTime · 28635 · 28671 · 28653 · 2 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setStartPrice · - · - · 28637 · 2 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setVaultAddress · 26292 · 29092 · 28532 · 5 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setWarriorsAddress · - · - · 29007 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · setWethAddress · - · - · 29026 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · teamSummon · 103875 · 623659 · 363767 · 4 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · unpause · - · - · 27748 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · withdraw · - · - · 35364 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · withdrawAll · - · - · 35096 · 1 · - │ ·································|···························|·············|·············|·············|···············|·············· | Deployments · · % of limit · │ ·····························································|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsGuild · - · - · 2023631 · 6.7 % · - │ ·····························································|·············|·············|·············|···············|·············· | ForgottenRunesWarriorsMinter · 2924973 · 2924997 · 2924995 · 9.7 % · - │ ·------------------------------------------------------------|-------------|-------------|-------------|---------------|-------------·
Consider increasing the number of configured optimizer runs above 200. This value represents an optimize-for-contract-size/optimize-for-runtime-gas trade-off, and selecting a larger value will reduce gas usage of mint and refund functions while increasing the one time gas cost of contract deployment.
This is a minor optimization, but it can save a bit of gas in the hottest loops (like minting batches, and especially issuing refunds).
Loop counter increments can safely use unchecked addition in most cases. For example, ForgottenRunesWarriorsMinter.sol#bidSummon
for (uint256 i = 0; i < numWarriors; i++) { _mint(msg.sender); }
Can be rewritten as:
for (uint256 i = 0; i < numWarriors;) { _mint(msg.sender); unchecked { ++i; } }
This saves around 100 gas/loop.
Potential optimizations:
The node
local variable in claimSummon#L242
is unused and can be inlined:
require( MerkleProof.verify(_merkleProof, claimlistMerkleRoot, keccak256(abi.encodePacked(msg.sender))), 'Invalid proof' );
The elapsed
and steps
variables in currentDaPrice#L289
can similarly be inlined:
uint256 stepDeduction = ((block.timestamp - daStartTime) / daDropInterval) * dropPerStep;
These have a very minor impact and you may prefer readability.
The preincrement operator (++value
) is the cheapest way to increment a value. value += 1
or value++
can be rewritten as ++value
. This has a very minor impact (11 gas), but it's an easy optimization.
Potential optimizations:
Setting the empty storage variable numMinted
to 0
and METADATA_PROVENANCE_HASH
to ''
in ForgottenRunesWarriorsGuild.sol
can be omitted.
This does not save runtime gas: it only reduces the gas cost at deployment time.
Here is a revised version of ForgottenRunesWarriorsMinter.sol
with all the above optimizations.
pragma solidity ^0.8.0; import '@openzeppelin/contracts/token/ERC721/ERC721.sol'; import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import '@openzeppelin/contracts/security/ReentrancyGuard.sol'; import '@openzeppelin/contracts/security/Pausable.sol'; import '@openzeppelin/contracts/access/Ownable.sol'; import '@openzeppelin/contracts/utils/cryptography/MerkleProof.sol'; import './interfaces/IWETH.sol'; import './interfaces/IForgottenRunesWarriorsGuild.sol'; import "@openzeppelin/contracts/utils/math/SafeCast.sol"; /** * @dev This implements the minter of the Forgotten Runes Warriors Guild. They are {ERC721} tokens. */ contract ForgottenRunesWarriorsMinter is Ownable, Pausable, ReentrancyGuard { /// @notice The start timestamp for the Dutch Auction (DA) sale and price uint256 public daStartTime = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; /// @notice The start timestamp for mintlisters /// @dev This is the end of DA phase. No more DA bids when this is hit uint256 public mintlistStartTime = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; /// @notice The start timestamp for the public sale uint256 public publicStartTime = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; /// @notice The start timestamp for the claims uint256 public claimsStartTime = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; /// @notice The start timestamp for self refunds uint256 public selfRefundsStartTime = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; /// @notice The main Merkle root bytes32 public mintlist1MerkleRoot; /// @notice The secondary Merkle root /// @dev Having a backup merkle root lets us atomically update the merkletree without downtime on the frontend bytes32 public mintlist2MerkleRoot; /// @notice The claimslist Merkle root bytes32 public claimlistMerkleRoot; /// @notice The address of the Warriors contract IForgottenRunesWarriorsGuild public warriors; /// @notice The address of the vault address public vault; /// @notice The address of the WETH contract address public weth; /// @notice The start price of the DA uint256 public startPrice = 2.5 ether; /// @notice The lowest price of the DA uint256 public lowestPrice = 0.6 ether; /// @notice The length of time for the price curve in the DA uint256 public daPriceCurveLength = 380 minutes; /// @notice The interval of time in which the price steps down uint256 public daDropInterval = 10 minutes; /// @notice The final price of the DA. Will be updated when DA is over and then used for subsequent phases uint256 public finalPrice = 2.5 ether; /// @notice An array of the addresses of the DA minters /// @dev An entry is created for every da minting tx, so the same minter address is quite likely to appear more than once address[] public daMinters; struct Bid { uint16 daNumMinted; uint96 daAmountPaid; uint96 daAmountRefunded; } mapping(address => Bid) public daBids; /// @notice Tracks if a given address minted in the mintlist mapping(address => bool) public mintlistMinted; /// @notice Tracks the total count of NFTs claimed by a given address mapping(address => bool) public claimlistMinted; /// @notice The total number of tokens reserved for the DA phase uint256 public maxDaSupply = 8000; /// @notice Tracks the total count of NFTs sold (vs. freebies) uint256 public numSold; /// @notice Tracks the total count of NFTs for sale uint256 public maxForSale = 14190; /// @notice Tracks the total count of NFTs claimed for free uint256 public numClaimed; /// @notice Tracks the total count of NFTs that can be claimed /// @dev While we will have a merkle root set for this group, putting a hard cap helps limit the damage of any problems with an overly-generous merkle tree uint256 public maxForClaim = 1100; /** * @dev Create the contract and set the initial baseURI * @param _warriors address the initial warriors contract address */ constructor(IForgottenRunesWarriorsGuild _warriors, address _weth) { setWarriorsAddress(_warriors); setWethAddress(_weth); setVaultAddress(msg.sender); } /* * Timeline: * * bidSummon : |------------| * mintlistSummon : |------------|-------------------------------------| * publicSummon : |------------|------------------------| * claimSummon : |------------|-----------| * teamSummon : |---------------------------------------------------------------| */ /** * @notice Mint a Warrior in the Dutch Auction phase * @param numWarriors uint256 of the number of warriors you're trying to mint */ function bidSummon(uint256 numWarriors) external payable nonReentrant whenNotPaused { require(numSold < maxDaSupply, 'Auction sold out'); require(numSold + numWarriors <= maxDaSupply, 'Not enough remaining'); require(daStarted(), 'Auction not started'); require(!mintlistStarted(), 'Auction phase over'); require( numWarriors > 0 && numWarriors <= 20, 'You can summon no more than 20 Warriors at a time' ); uint256 currentPrice = currentDaPrice(); require( msg.value >= (currentPrice * numWarriors), 'Ether value sent is not sufficient' ); daMinters.push(msg.sender); Bid memory bid = daBids[msg.sender]; bid.daAmountPaid += SafeCast.toUint96(msg.value); bid.daNumMinted += SafeCast.toUint16(numWarriors); daBids[msg.sender] = bid; numSold += numWarriors; if (numSold == maxDaSupply) { // optimistic: save gas by not setting on every mint, but will // require manual `setFinalPrice` before refunds if da falls short finalPrice = currentPrice; } for (uint256 i = 0; i < numWarriors;) { _mint(msg.sender); unchecked { ++i; } } } /** * @notice Mint a Warrior in the mintlist phase (paid) * @param _merkleProof bytes32[] your proof of being able to mint */ function mintlistSummon(bytes32[] calldata _merkleProof) external payable nonReentrant whenNotPaused { require(numSold < maxForSale, 'Sold out'); require(mintlistStarted(), 'Mintlist phase not started'); require(msg.value == finalPrice, 'Ether value incorrect'); // verify didn't already mint require(mintlistMinted[msg.sender] == false, 'Already minted'); mintlistMinted[msg.sender] = true; // verify mintlist merkle bytes32 node = keccak256(abi.encodePacked(msg.sender)); require( MerkleProof.verify(_merkleProof, mintlist1MerkleRoot, node) || MerkleProof.verify(_merkleProof, mintlist2MerkleRoot, node), 'Invalid proof' ); numSold += 1; _mint(msg.sender); } /** * @notice Mint a Warrior in the Public phase (paid) * @param numWarriors uint256 of the number of warriors you're trying to mint */ function publicSummon(uint256 numWarriors) external payable nonReentrant whenNotPaused { require(numSold < maxForSale, 'Sold out'); require(numSold + numWarriors <= maxForSale, 'Not enough remaining'); require(publicStarted(), 'Public sale not started'); require( numWarriors > 0 && numWarriors <= 20, 'You can summon no more than 20 Warriors at a time' ); require( msg.value == (finalPrice * numWarriors), 'Ether value sent is incorrect' ); numSold += numWarriors; for (uint256 i = 0; i < numWarriors;) { _mint(msg.sender); unchecked { ++i; } } } /** * @dev claim a warrior for free if you're in the claimlist * @param _merkleProof bytes32[] the proof that you're eligible to mint here */ function claimSummon(bytes32[] calldata _merkleProof) external nonReentrant whenNotPaused { require(numClaimed < maxForClaim, 'No more claims'); require(claimsStarted(), 'Claim phase not started'); // verify didn't already claim require(claimlistMinted[msg.sender] == false, 'Already claimed'); claimlistMinted[msg.sender] = true; require( MerkleProof.verify(_merkleProof, claimlistMerkleRoot, keccak256(abi.encodePacked(msg.sender))), 'Invalid proof' ); ++numClaimed; _mint(msg.sender); } /** * @notice Mint a Warrior (owner only) * @param recipient address the address of the recipient * @param count uint256 of the number of warriors you're trying to mint */ function teamSummon(address recipient, uint256 count) external onlyOwner { require(address(recipient) != address(0), 'address req'); for (uint256 i = 0; i < count;) { _mint(recipient); unchecked { ++i; } } } function _mint(address recipient) private { warriors.mint(recipient); } /* * View utilities */ /** * @notice returns the current dutch auction price */ function currentDaPrice() public view returns (uint256) { if (!daStarted()) { return startPrice; } if (block.timestamp >= daStartTime + daPriceCurveLength) { // end of the curve return lowestPrice; } uint256 dropPerStep = (startPrice - lowestPrice) / (daPriceCurveLength / daDropInterval); uint256 stepDeduction = ((block.timestamp - daStartTime) / daDropInterval) * dropPerStep; // don't go negative in the next step if (stepDeduction > startPrice) { return lowestPrice; } uint256 currentPrice = startPrice - stepDeduction; return currentPrice > lowestPrice ? currentPrice : lowestPrice; } /** * @notice returns whether the dutch auction has started */ function daStarted() public view returns (bool) { return block.timestamp > daStartTime; } /** * @notice returns whether the mintlist has started */ function mintlistStarted() public view returns (bool) { return block.timestamp > mintlistStartTime; } /** * @notice returns whether the public mint has started */ function publicStarted() public view returns (bool) { return block.timestamp > publicStartTime; } /** * @notice returns whether the claims phase has started */ function claimsStarted() public view returns (bool) { return block.timestamp > claimsStartTime; } /** * @notice returns whether self refunds phase has started */ function selfRefundsStarted() public view returns (bool) { return block.timestamp > selfRefundsStartTime; } /** * @notice returns the number of minter addresses in the DA phase (includes duplicates) */ function numDaMinters() external view returns (uint256) { return daMinters.length; } function daNumMinted(address bidder) external view returns (uint256) { return daBids[bidder].daNumMinted; } function daAmountPaid(address bidder) external view returns (uint256) { return daBids[bidder].daAmountPaid; } function daAmountRefunded(address bidder) external view returns (uint256) { return daBids[bidder].daAmountRefunded; } /* * Refund logic */ /** * @notice issues refunds for the accounts in minters between startIdx and endIdx inclusive * @param startIdx uint256 the starting index of daMinters * @param endIdx uint256 the ending index of daMinters, inclusive */ function issueRefunds(uint256 startIdx, uint256 endIdx) public onlyOwner nonReentrant { for (uint256 i = startIdx; i < endIdx + 1;) { _refundAddress(daMinters[i]); unchecked { ++i; } } } /** * @notice issues a refund for the address * @param minter address the address to refund */ function refundAddress(address minter) public onlyOwner nonReentrant { _refundAddress(minter); } /** * @notice refunds msg.sender what they're owed */ function selfRefund() public nonReentrant { require(selfRefundsStarted(), 'Self refund period not started'); _refundAddress(msg.sender); } function _refundAddress(address minter) private { uint256 owed = refundOwed(minter); if (owed > 0) { daBids[minter].daAmountRefunded += SafeCast.toUint96(owed); _safeTransferETHWithFallback(minter, owed); } } /** * @notice returns the amount owed the address * @param minter address the address of the account that wants a refund */ function refundOwed(address minter) public view returns (uint256) { Bid memory bid = daBids[minter]; return bid.daAmountPaid - (finalPrice * bid.daNumMinted) - bid.daAmountRefunded; } /** * @notice Transfer ETH. If the ETH transfer fails, wrap the ETH and try send it as WETH. * @param to account who to send the ETH or WETH to * @param amount uint256 how much ETH or WETH to send */ function _safeTransferETHWithFallback(address to, uint256 amount) internal { if (!_safeTransferETH(to, amount)) { IWETH(weth).deposit{value: amount}(); IERC20(weth).transfer(to, amount); } } /** * @notice Transfer ETH and return the success status. * @dev This function only forwards 30,000 gas to the callee. * @param to account who to send the ETH to * @param value uint256 how much ETH to send */ function _safeTransferETH(address to, uint256 value) internal returns (bool) { (bool success, ) = to.call{value: value, gas: 30_000}(new bytes(0)); return success; } /* * Only the owner can do these things */ /** * @notice pause the contract */ function pause() public onlyOwner { _pause(); } /** * @notice unpause the contract */ function unpause() public onlyOwner { _unpause(); } /** * @notice set the dutch auction start timestamp */ function setDaStartTime(uint256 _newTime) public onlyOwner { daStartTime = _newTime; } /** * @notice set the mintlist start timestamp */ function setMintlistStartTime(uint256 _newTime) public onlyOwner { mintlistStartTime = _newTime; } /** * @notice set the public sale start timestamp */ function setPublicStartTime(uint256 _newTime) public onlyOwner { publicStartTime = _newTime; } /** * @notice set the claims phase start timestamp */ function setClaimsStartTime(uint256 _newTime) public onlyOwner { claimsStartTime = _newTime; } /** * @notice set the self refund phase start timestamp */ function setSelfRefundsStartTime(uint256 _newTime) public onlyOwner { selfRefundsStartTime = _newTime; } /** * @notice A convenient way to set all phase times at once * @param newDaStartTime uint256 the dutch auction start time * @param newMintlistStartTime uint256 the mintlst phase start time * @param newPublicStartTime uint256 the public phase start time * @param newClaimsStartTime uint256 the claims phase start time */ function setPhaseTimes( uint256 newDaStartTime, uint256 newMintlistStartTime, uint256 newPublicStartTime, uint256 newClaimsStartTime ) public onlyOwner { // we put these checks here instead of in the setters themselves // because they're just guardrails of the typical case require( newPublicStartTime >= newMintlistStartTime, 'Set public after mintlist' ); require( newClaimsStartTime >= newPublicStartTime, 'Set claims after public' ); setDaStartTime(newDaStartTime); setMintlistStartTime(newMintlistStartTime); setPublicStartTime(newPublicStartTime); setClaimsStartTime(newClaimsStartTime); } /** * @notice set the merkle root for the mintlist phase */ function setMintlist1MerkleRoot(bytes32 newMerkleRoot) public onlyOwner { mintlist1MerkleRoot = newMerkleRoot; } /** * @notice set the alternate merkle root for the mintlist phase * @dev we have two because it lets us idempotently update the website without downtime */ function setMintlist2MerkleRoot(bytes32 newMerkleRoot) public onlyOwner { mintlist2MerkleRoot = newMerkleRoot; } /** * @notice set the merkle root for the claimslist phase */ function setClaimlistMerkleRoot(bytes32 newMerkleRoot) public onlyOwner { claimlistMerkleRoot = newMerkleRoot; } /** * @notice set the vault address where the funds are withdrawn */ function setVaultAddress(address _newVaultAddress) public onlyOwner { vault = _newVaultAddress; } /** * @notice set the warriors token address */ function setWarriorsAddress( IForgottenRunesWarriorsGuild _newWarriorsAddress ) public onlyOwner { warriors = _newWarriorsAddress; } /** * @notice set the weth token address */ function setWethAddress(address _newWethAddress) public onlyOwner { weth = _newWethAddress; } /** * @notice set the dutch auction start price */ function setStartPrice(uint256 _newPrice) public onlyOwner { startPrice = _newPrice; } /** * @notice set the dutch auction lowest price */ function setLowestPrice(uint256 _newPrice) public onlyOwner { lowestPrice = _newPrice; } /** * @notice set the length of time the dutch auction price should change */ function setDaPriceCurveLength(uint256 _newTime) public onlyOwner { daPriceCurveLength = _newTime; } /** * @notice set how long it takes for the dutch auction to step down in price */ function setDaDropInterval(uint256 _newTime) public onlyOwner { daDropInterval = _newTime; } /** * @notice set "final" price of the dutch auction * @dev this is set automatically if the dutch-auction sells out, but needs to be set manually if the DA fails to sell out */ function setFinalPrice(uint256 _newPrice) public onlyOwner { finalPrice = _newPrice; } /** * @notice the max supply available in the dutch auction */ function setMaxDaSupply(uint256 _newSupply) public onlyOwner { maxDaSupply = _newSupply; } /** * @notice the total max supply available for sale in any phase */ function setMaxForSale(uint256 _newSupply) public onlyOwner { maxForSale = _newSupply; } /** * @notice the max supply available in the claimlist */ function setMaxForClaim(uint256 _newSupply) public onlyOwner { maxForClaim = _newSupply; } /** * @notice Withdraw funds to the vault * @param _amount uint256 the amount to withdraw */ function withdraw(uint256 _amount) public onlyOwner { require(address(vault) != address(0), 'no vault'); require(payable(vault).send(_amount)); } /** * @notice Withdraw all funds to the vault */ function withdrawAll() public payable onlyOwner { require(address(vault) != address(0), 'no vault'); require(payable(vault).send(address(this).balance)); } /** * @dev ERC20s should not be sent to this contract, but if someone * does, it's nice to be able to recover them * @param token IERC20 the token address * @param amount uint256 the amount to send */ function forwardERC20s(IERC20 token, uint256 amount) public onlyOwner { require(address(msg.sender) != address(0)); token.transfer(msg.sender, amount); } }
Gas report after. (Note that optimizer runs are increased to 200000):
·------------------------------------------------------------|---------------------------|----------------|-----------------------------· | Solc version: 0.8.6 · Optimizer enabled: true · Runs: 200000 · Block limit: 30000000 gas │ ·····························································|···························|················|······························ | Methods │ ·································|···························|·············|·············|················|···············|·············· | Contract · Method · Min · Max · Avg · # calls · usd (avg) │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsGuild · burn · - · - · 31174 · 1 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsGuild · initialize · 46198 · 46210 · 46209 · 90 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsGuild · mint · 61513 · 95713 · 82888 · 4 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsGuild · setBaseURI · - · - · 31978 · 1 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsGuild · setMinter · - · - · 46084 · 3 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsGuild · setProvenanceHash · - · - · 46840 · 1 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsGuild · uploadAttributes · - · - · 23975 · 1 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsGuild · uploadImage · - · - · 23963 · 1 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsGuild · withdrawAll · - · - · 23581 · 1 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · bidSummon · 153755 · 730219 · 267431 · 18 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · claimSummon · - · - · 158012 · 6 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · issueRefunds · 48595 · 122392 · 85448 · 5 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · mintlistSummon · 125927 · 160127 · 151577 · 8 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · pause · - · - · 27687 · 2 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · publicSummon · 110447 · 650502 · 487884 · 20 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · selfRefund · - · - · 41205 · 2 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · setClaimlistMerkleRoot · - · - · 46097 · 5 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · setClaimsStartTime · - · - · 28669 · 1 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · setDaDropInterval · - · - · 28647 · 1 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · setDaPriceCurveLength · - · - · 28648 · 1 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · setDaStartTime · - · - · 28645 · 1 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · setFinalPrice · 28624 · 28684 · 28680 · 15 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · setMaxDaSupply · - · - · 28689 · 3 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · setMaxForClaim · - · - · 28667 · 2 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · setMaxForSale · - · - · 28647 · 3 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · setMintlist1MerkleRoot · - · - · 46097 · 9 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · setMintlist2MerkleRoot · - · - · 46162 · 1 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · setMintlistStartTime · - · - · 28668 · 1 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · setPhaseTimes · 36538 · 44938 · 43927 · 50 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · setPublicStartTime · - · - · 28667 · 1 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · setSelfRefundsStartTime · 28646 · 28682 · 28664 · 2 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · setStartPrice · - · - · 28648 · 2 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · setVaultAddress · 26219 · 29019 · 28459 · 5 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · setWarriorsAddress · - · - · 28956 · 1 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · setWethAddress · - · - · 28998 · 1 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · teamSummon · 103636 · 619563 · 361600 · 4 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · unpause · - · - · 27687 · 1 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · withdraw · - · - · 35306 · 1 · - │ ·································|···························|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · withdrawAll · - · - · 35038 · 1 · - │ ·································|···························|·············|·············|················|···············|·············· | Deployments · · % of limit · │ ·····························································|·············|·············|················|···············|·············· | ForgottenRunesWarriorsGuild · - · - · 2690226 · 9 % · - │ ·····························································|·············|·············|················|···············|·············· | ForgottenRunesWarriorsMinter · 4119642 · 4119666 · 4119664 · 13.7 % · - │ ·------------------------------------------------------------|-------------|-------------|----------------|---------------|-------------·
#0 - liveactionllama
2022-05-06T03:59:07Z
Warden created this issue as a placeholder, because their submission was too large for the contest form. They then emailed their md file to our team at 21:21 (UTC) on 05/05/2022. I've updated this issue with their md file content.