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: 41/93
Findings: 2
Award: $90.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
71.5346 USDC - $71.53
In ForgottenRunesWarriorsMinter.sol, the import '@openzeppelin/contracts/token/ERC721/ERC721.sol';
is unused
address vault
to address payable address
The variable address vault
in ForgottenRunesWarriorsMinter.sol can be payable
This variable is used to send ETH and this also avoid the cast to address payable
address public vault;
to address payable public vault;
setVaultAddress(msg.sender);
to setVaultAddress(payable(msg.sender));
function setVaultAddress(address _newVaultAddress) public onlyOwner {
to function setVaultAddress(address payable _newVaultAddress) public onlyOwner {
require(payable(vault).send(_amount));
to require(vault.send(_amount));
require(payable(vault).send(address(this).balance));
to require(vault.send(address(this).balance));
address public weth;
to IWETH public weth;
In ForgottenRunesWarriorsMinter.sol L54, the weth
variable can be IWETH
and IWETH
interface can inherit from IERC20
interface
This avoid cast from address
to IWETH
In ForgottenRunesWarriorsGuild.sol L58, * @param tokenId uint256 ID of the token to be minted
to * @param tokenId uint256 ID of the token minted
The following variables and functions are missing commenting as describe below: In ForgottenRunesWarriorsGuild.sol:
string public constant R
function _baseURI()
In ForgottenRunesWarriorsMinter.sol:
function _refundAddress(address)
function _mint(address)
function currentDaPrice()
should have more documentation of equations and the curveThe limit of 20 warriors(ERC721 token) at a time in L130 bidSummon(uint256)
and L201 publicSummon(uint256)
its easy to break, a contract could be used to mint more than 20 at a time
R
contract variableImprove the name of variable R
in ForgottenRunesWarriorsGuild.sol L32
In the contest video said "put a little bit of lore within all of all our contracts" and the minter ForgottenRunesWarriorsMinter.sol dont have this variable
Recommend remove this variable to clean the code and spend less gas, can put this information in a comment
METADATA_PROVENANCE_HASH
contract variableIn ForgottenRunesWarriorsGuild.sol L34, can storage this information in tokenURI
variable and baseTokenURI
function, also can remove the L145 setProvenanceHash
function
Recommend set daStartTime
, mintlistStartTime
, publicStartTime
, claimsStartTime
, selfRefundsStartTime
variables in the construtor with real values, and remove the initialize on the contract
Recommend a function to set start/lowest price with a startPrice > lowestPrice
require
function setStartLowestPrices(uint256 _startPrice, uint256 _lowestPrice) external onlyOwner { require(_startPrice > _lowestPrice, "The start price lower than lowest price"); startPrice = _startPrice; lowestPrice = _lowestPrice; }
Like in the setPhaseTimes
:
function setPhaseTimes( uint256 _newDaStartTime, uint256 _newSelfRefundsStartTime, uint256 _newMintlistStartTime, uint256 _newPublicStartTime, uint256 _newClaimsStartTime ) public onlyOwner {
This avoids confusion with contract variables
Group the contract variables in a struct, in example the times variables
struct Time { uint32 daStart; uint32 mintlistStart; uint32 publicStart; uint32 claimsStart; uint32 selfRefundsStart; uint32 daPriceCurveLength; uint32 daDropInterval; } Time public timeVars;
This save gas and clean the code
setWethAddress
function can be deleteIn ForgottenRunesWarriorsMinter.sol, The WETH address should never change and L54 weth
can be immutable
uploadImage
and L157 uploadAttributes
functions are emptyIts a good idea storage the encoded data on-chain but recommend emit and event with the id of the token and the data
Each function that changes the state of the contract should have an associated event
initialize
functionIn ForgottenRunesWarriorsGuild.sol, remove L52 initialize
function and use directly L137 setMinter
function
In ForgottenRunesWarriorsGuild.sol L173 and ForgottenRunesWarriorsMinter.sol L628, the require of the forwardERC20s
function, require(address(msg.sender) != address(0));
dont do anything, can be remove this require, also dont have revert message
Also in this function add address to
parameter for transfer the tokens directly to who sent it and use the @openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol
library to transfer the tokens
import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; contract ... { using SafeERC20 for IERC20; ... /** * @dev Again, 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 _to address who receives the tokens * @param _amount uint256 the amount to send */ function forwardERC20s(IERC20 _token, address _to, uint256 _amount) external onlyOwner { require(_to != address(0), "ForgottenRunesWarriorsGuild: transfer to the zero address"); _token.safeTransfer(_to, _amount); } ... }
withdrawAll
functionIn the ForgottenRunesWarriorsGuild.sol the function L163 withdrawAll
:
transfer
instead of send
withdrawAll
should be non payableHowever, the contract cant receibe ETH because dont have a receive/fallback payable function except the withdrawAll
function and this function return the balance of the contract to the sender(the owner), in other words the withdrawAll
function do nothing
Note: A contract can use selfdestruct to send ETH to the ForgottenRunesWarriorsGuild contract, but why would someone do this?
In ForgottenRunesWarriorsMinter.sol:
require(numSold < maxDaSupply, 'Auction sold out');
Redundant require, in the next line check L137 numSold + numWarriors <= maxDaSupply
and numWarriors > 0
=> numSold < maxDaSupply
require(numSold < maxForSale, 'Sold out');
Redundant require, in the next line check L208 numSold + numWarriors <= maxForSale
and numWarriors > 0
=> numSold < maxForSale
require(address(recipient) != address(0), 'address req');
In L258 the require is redundant, the ERC721 check the receive its not the address(0)
The functions setWarriorsAddress(IForgottenRunesWarriorsGuild)
and setWethAddress(address)
should have a not address(0)
check like require(<address> != address(0), "<address> is zero address");
In ForgottenRunesWarriorsGuild.sol, L94 function mint(address)
:
If move the _safeMint(recipient, tokenId);
to the end or use _mint
instead of _safemint
can remove the nonReentrant
modifier
Recommend:
function mint(address _recipient) public override returns (uint256) { require(numMinted < MAX_WARRIORS, 'All warriors have been summoned'); require(_msgSender() == minter, 'Not a minter'); uint256 tokenId = numMinted; numMinted += 1; _safeMint(_recipient, tokenId); return tokenId; }
note: The minter should have the responsibility to worry about the reentrancy attacks
In ForgottenRunesWarriorsMinter.sol:
In the functions L130 bidSummon(uint256)
, L171 mintlistSummon(bytes32[])
, L201 publicSummon(uint256)
, L229 claimSummon(bytes32[])
the mint is in the end of the function
In the functions L350 issueRefunds(uint256,uint256)
, L364 refundAddress(address)
, nonReentrant
, L371 selfRefund()
the .call
is in the end of the function
setPhaseTimes
functionIn ForgottenRunesWarriorsMinter.sol, L480 setPhaseTimes
:
claimsStartTime
require(newSelfRefundsStartTime > newMintlistStartTime, 'Set selfRefundsStart after mintlist');
setSelfRefundsStartTime(newSelfRefundsStartTime);
setPhaseTimes
functionIn ForgottenRunesWarriorsMinter.sol, L480 setPhaseTimes
:
require(newMintlistStartTime > newDaStartTime, 'Set mintlist after daStart');
In ForgottenRunesWarriorsMinter.sol:
daMinters
array, L76 daAmountPaid
mappings, L79 daAmountRefunded
mapping, L82 daNumMinted
mapping, L337 numDaMinters()
functionBidSummon
event to save the minters addresses, numWarriors and valuetoDAMinter
mapping save the minters infotoDAMinter
mapping using DAMinter
struct how saves the paid amount and the number of minted, in uint240 and uint16 to save gas(total 256 => a bytes32 instead of 2 * bytes32)event BidSummon(address indexed minter, uint256 numWarriors, uint256 value); struct DAMinter { /// @notice Tracks the total amount paid uint240 amountPaid; /// @notice Tracks the total count of NFTs minted uint16 numMinted; } /// @notice Tracks the information of DA minters mapping(address => DAMinter) public toDAMinter;
toDAMinter
mappingBidSummon
eventfunction 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' ); toDAMinter[msg.sender].amountPaid += uint240(msg.value); toDAMinter[msg.sender].numMinted += uint16(numWarriors); 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; } emit BidSummon(msg.sender, numWarriors, msg.value); for (uint256 i = 0; i < numWarriors; i++) { _mint(msg.sender); } }
/** * @notice issues refunds for the accounts in minters * @param minters array of addresses to refund */ function refundAddresses(address[] calldata minters) external onlyOwner { uint256 mintersLength = minters.length; for (uint256 i; i < mintersLength;) { _refundAddress(minters[i]); unchecked { ++i; } } } /** * @notice issues a refund for the address * @param minter address the address to refund */ function refundAddress(address minter) external onlyOwner { _refundAddress(minter); } /** * @notice refunds msg.sender what they're owed */ function selfRefund() public { require(selfRefundsStarted(), 'Self refund period not started'); _refundAddress(msg.sender); } /** * @notice refunds a minter what they're owed * @dev the toDAMinter od the minter is deleted * @param minter address the address of the account that wants a refund */ function _refundAddress(address minter) private { uint256 owed = refundOwed(minter); if (owed > 0) { delete toDAMinter[minter]; _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) { unchecked { return toDAMinter[minter].amountPaid - (finalPrice * toDAMinter[minter].numMinted); } }
Warning:
finalPrice
does not change when start the refundsNote: Use The Graph, Web3 or Ethers to get and process the events and get all minters addresses to use in refundAddresses(address[])
function
🌟 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
19.2009 USDC - $19.20
As different compiler versions have critical behavior specifics if the contract gets accidentally deployed using another compiler version compared to one they tested with, various types of undesired behavior can be introduced.
Consider locking compiler version, for example pragma solidity 0.8.6.
Ref link: The contracts use unlocked pragma #181
In ForgottenRunesWarriorsGuild.sol:
uint256 public numMinted
string public METADATA_PROVENANCE_HASH
In ForgottenRunesWarriorsMinter.sol:
uint256 i
Avoid cast to save gas and clean code
In ForgottenRunesWarriorsGuild.sol:
address(msg.sender
) to msg.sender
In ForgottenRunesWarriorsMinter.sol:
address(recipient)
to recipient
address(vault)
to vault
address(msg.sender
) to msg.sender
The following functions could be set external to save gas and improve code quality. External call cost is less expensive than of public functions, when dont have parameters.
In ForgottenRunesWarriorsGuild.sol:
initialize(address)
exists(uint256)
mint(address)
burn(uint256)
setProvenanceHash(string)
forwardERC20s(IERC20,uint256)
In ForgottenRunesWarriorsMinter.sol:
issueRefunds(uint256,uint256)
refundAddress(address)
setMintlist1MerkleRoot(bytes32)
setMintlist2MerkleRoot(bytes32)
setClaimlistMerkleRoot(bytes32)
setStartPrice(uint256)
setLowestPrice(uint256)
setDaPriceCurveLength(uint256)
setDaDropInterval(uint256)
setFinalPrice(uint256)
setMaxDaSupply(uint256)
setMaxForSale(uint256)
setMaxForClaim(uint256)
withdraw(uint256)
forwardERC20s(IERC20,amount)
The following functions could be set private to save gas and improve code quality, in ForgottenRunesWarriorsMinter.sol:
_safeTransferETHWithFallback(address,uint256)
_safeTransferETH(address,uint256)
_msgSender()
to msg.sender
Use msg.sender
to save gas in ForgottenRunesWarriorsGuild.sol:
require(_msgSender() == minter, 'Not a minter');
to require(msg.sender == minter, 'Not a minter');
_isApprovedOrOwner(_msgSender(), tokenId),
to _isApprovedOrOwner(msg.sender, tokenId),
When the value of the post-loop increment/decrement is not stored or used in any calculations, the prefix increment/decrement operators (++<var>
/--<var>
) cost less gas PER LOOP than the postfix increment/decrement operators (<var>++
/<var>--
)
Use ++<var>
rather than <var>++
to save gas
In ForgottenRunesWarriorsGuild.sol:
for (uint256 i = 0; i < numWarriors; i++) {
to for (uint256 i = 0; i < numWarriors; ++i) {
for (uint256 i = 0; i < count; i++) {
to for (uint256 i = 0; i < count; ++i) {
for (uint256 i = startIdx; i < endIdx + 1; i++) {
to for (uint256 i = startIdx; i < endIdx + 1; ++i) {
numSold += 1;
to ++numSold;
numClaimed += 1;
to ++numClaimed;
In ForgottenRunesWarriorsMinter.sol:
numMinted += 1;
to ++numMinted;
In ForgottenRunesWarriorsGuild.sol:
numMinted += 1;
to
unchecked { numMinted += 1; }
In ForgottenRunesWarriorsMinter.sol:
L152:
daAmountPaid[msg.sender] += msg.value; daNumMinted[msg.sender] += numWarriors; numSold += numWarriors;
to
unchecked { daAmountPaid[msg.sender] += msg.value; daNumMinted[msg.sender] += numWarriors; numSold += numWarriors; }
The sum of value cant overflow => daNumMinted, numSold and cant overflow, daAmountPaid > numSold
L162, L220, L259, L355:
for (...; i++) { ... }
to
unchecked { for (...;) { ... unchecked { i++; } }
L193, L248: <var> += 1;
to
unchecked { <var> += 1; }
L219: numSold += numWarriors;
to
unchecked { numSold += numWarriors; }
L275: All mathematics inside of currentDaPrice()
function can be unchecked
L379: daAmountRefunded[minter] += owed;
to
unchecked { daAmountRefunded[minter] += owed; }
L388: All mathematics inside of refundOwed()
function can be unchecked
!<bool>
instead of <bool> == false
In ForgottenRunesWarriorsMinter.sol, L182 require(mintlistMinted[msg.sender] == false, 'Already minted');
to require(!mintlistMinted[msg.sender], 'Already minted');
to save gas
In ForgottenRunesWarriorsGuild.sol:
function mint(address)
mload numMinted
in the begin of the function and use tokenId
in the MAX_WARRIORS
require
function mint(address recipient) public override nonReentrant returns (uint256) { uint256 tokenId = numMinted; require(tokenId < MAX_WARRIORS, 'All warriors have been summoned'); require(_msgSender() == minter, 'Not a minter'); _safeMint(recipient, tokenId); numMinted += 1; return tokenId; }
In ForgottenRunesWarriorsMinter.sol:
bidSummon(uint256)
variable maxDaSupply
currentDaPrice()
variable:
lowestPrice
sload 5 times
startPrice
sload 4 times
daStartTime
sload 2 times
daPriceCurveLength
sload 2 times
daDropInterval
sload 2 timesIn ForgottenRunesWarriorsMinter.sol L355: for (uint256 i = startIdx; i < endIdx + 1; i++) {
to for (uint256 i = startIdx; i <= endIdx; i++) {
to save gas