Forgotten Runes Warrior Guild contest - hickuphh3'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: 34/93

Findings: 3

Award: $108.74

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

48.5872 USDC - $48.59

Labels

bug
duplicate
2 (Med Risk)
upgraded by judge

External Links

Judge has assessed an item in Issue #119 as Medium risk. The relevant finding follows:

L01: Use .call() instead of .send()

Line References

https://github.com/code-423n4/2022-05-runes/blob/060b4f82b79c8308fe65674a39a07c44fa586cd3/contracts/ForgottenRunesWarriorsMinter.sol#L611

https://github.com/code-423n4/2022-05-runes/blob/060b4f82b79c8308fe65674a39a07c44fa586cd3/contracts/ForgottenRunesWarriorsMinter.sol#L619

https://github.com/code-423n4/2022-05-runes/blob/060b4f82b79c8308fe65674a39a07c44fa586cd3/contracts/ForgottenRunesWarriorsGuild.sol#L164

Description

It is recommended to use call() instead of send() because the former fowards all remaining gas with the call, while the latter has a gas limit of 2300. This would cause withdrawals to fail if the recipient (vault) is a smart contract that:

  • lacks receive() / payable fallback() functions
  • implements a payable fallback function which uses more than 2300 gas
  • implements a payable fallback function which uses less than 2300 gas, but is called through a proxy that raise the call's gas usage to above 2300
(bool success, ) = payable(vault).call{ value: _amount }("");
require(success, "ETH transfer failed");

#0 - gzeoneth

2022-06-18T19:19:22Z

Duplicate of #254

Codebase Impressions & Summary

Codebase quality in general is high. The accompanying video walkthrough and explanation greatly helps us wardens to be quickly acquainted with the various functionalities. Contracts are also well-tested and covered.

The greatest risk I see is centralisation. The dutch auction (DA) parameters, phase timestamps, final DA price and WETH and warrior NFT contracts are mutable, and lack any restrictions on when they can be called. All funds can also be withdrawn all funds at any time. The team is also able to mint as many NFTs as they would like (up to the hardcapped supply of 16k).

From an operational perspective, I understand the need for things to be kept fluid so that the team will be able to respond and update to situations. However, the downside of this is that it is impossible to introduce safeguards into the system. For instance, it would be great to have a sanity limit on setFinalPrice where it can’t exceed startPrice, but this sanity limit is rendered ineffective because startPrice is mutable. Consider relinquishing control over some less important variables to reduce the trust people need to have on the team.

Low Severity Findings

L01: Use .call() instead of .send()

Line References

https://github.com/code-423n4/2022-05-runes/blob/060b4f82b79c8308fe65674a39a07c44fa586cd3/contracts/ForgottenRunesWarriorsMinter.sol#L611

https://github.com/code-423n4/2022-05-runes/blob/060b4f82b79c8308fe65674a39a07c44fa586cd3/contracts/ForgottenRunesWarriorsMinter.sol#L619

https://github.com/code-423n4/2022-05-runes/blob/060b4f82b79c8308fe65674a39a07c44fa586cd3/contracts/ForgottenRunesWarriorsGuild.sol#L164

Description

It is recommended to use call() instead of send() because the former fowards all remaining gas with the call, while the latter has a gas limit of 2300. This would cause withdrawals to fail if the recipient (vault) is a smart contract that:

  • lacks receive() / payable fallback() functions
  • implements a payable fallback function which uses more than 2300 gas
  • implements a payable fallback function which uses less than 2300 gas, but is called through a proxy that raise the call's gas usage to above 2300
(bool success, ) = payable(vault).call{ value: _amount }("");
require(success, "ETH transfer failed");

L02: Redundant setWarriorsAddress and setWethAddress setters

Line References

https://github.com/code-423n4/2022-05-runes/blob/060b4f82b79c8308fe65674a39a07c44fa586cd3/contracts/ForgottenRunesWarriorsMinter.sol#L534-L545

Description

Having setters for the warrior NFT and WETH addresses doesn’t exactly inspire confidence over the code quality. It would give readers the impression that they can be changed to another address at any time.

Remove these setters and make the variables immutable since they will only be set in the constrcutor.

L03: Limit team summon NFT amount

Line References

https://github.com/code-423n4/2022-05-runes/blob/060b4f82b79c8308fe65674a39a07c44fa586cd3/contracts/ForgottenRunesWarriorsMinter.sol#L257-L262

Description

The team is able to mint any amount of warriors to any desired recipient at once, up to the hardcoded supply of 16k. This could potentially cause griefing for users participating in the different phases.

Consider limiting the number of NFTs that the team can mint (16k - maxForSale - maxForClaim). Admittedly, having this limit is rendered less effective by the fact that maxForSale and maxForClaim are mutable.

G01: Update config settings

Description

Some gas optimizations can be achieved by simply updating the settings in hardhat.config.ts.

  1. The number of solc runs used for contract compilation is 200. This number can be bumped significantly to produce more gas efficient code (max value of 2**32-1). More information can be found in the solidity docs.
  2. Use latest solidity version + IR optimizer. The new Yul-based optimizer that is shipped with 0.8.13 ”can do much higher-level optimization across functions”.
  • Increase solc runs significantly in hardhat.config.ts. Contract sizes and thus deployment costs will increase, but functions will cost less gas to execute.
  • Turn on the IR-based Yul optimizer. This will require a bump to the latest solidity version.
solidity: {
  compilers: [
    {
      version: '0.8.13',
      settings: {
        viaIR: true, // use IR-based Yul optimizer
        optimizer: { enabled: true, runs: 100000 }, // increase solc runs
      },
    },
	...

G02: Redundant zero address check

Line References

https://github.com/code-423n4/2022-05-runes/blob/060b4f82b79c8308fe65674a39a07c44fa586cd3/contracts/ForgottenRunesWarriorsGuild.sol#L174

https://github.com/code-423n4/2022-05-runes/blob/060b4f82b79c8308fe65674a39a07c44fa586cd3/contracts/ForgottenRunesWarriorsMinter.sol#L628

Description

msg.sender will be the owner’s address, which is non-zero. The non-zero address check in forwardERC20s is therefore redundant and can be removed.

require(address(msg.sender) != address(0));

G03: Push to daMinters array only if amountPaid is zero to avoid duplicates

Line References

https://github.com/code-423n4/2022-05-runes/blob/060b4f82b79c8308fe65674a39a07c44fa586cd3/contracts/ForgottenRunesWarriorsMinter.sol#L151

Description

To avoid storing duplicate addresses in the daMinters array (thus avoiding a potential SSTORE and saves gas when issuing refunds), consider pushing the minter address only for the first mint. This can be checked in multiple ways, such as if either daAmountPaid or daNumMinted is non-zero.

if (daAmountPaid[msg.sender] == 0) daMinters.push(msg.sender);

G04: Redundant literal boolean comparisons

Line References

https://github.com/code-423n4/2022-05-runes/blob/060b4f82b79c8308fe65674a39a07c44fa586cd3/contracts/ForgottenRunesWarriorsMinter.sol#L183

https://github.com/code-423n4/2022-05-runes/blob/060b4f82b79c8308fe65674a39a07c44fa586cd3/contracts/ForgottenRunesWarriorsMinter.sol#L239

Description

Since claimlistMinted[msg.sender] is a boolean mapping, there isn’t a need to check against the boolean literal false.

require(!claimlistMinted[msg.sender], 'Already claimed');

G05: Change WETH to IWETH type to avoid repeated casting

Line References

https://github.com/code-423n4/2022-05-runes/blob/060b4f82b79c8308fe65674a39a07c44fa586cd3/contracts/ForgottenRunesWarriorsMinter.sol#L401-L402

Description

By declaring WETH to be of IWETH, repeated castings to IWETH and IERC20 can be avoided. For instance, the gas cost for the should refund a griefer test reduces by 124 gas from 144654 to 144530.

IWETH public weth;

function setWethAddress(address _newWethAddress) public onlyOwner {
  weth = IWETH(_newWethAddress);
}

function _safeTransferETHWithFallback(address to, uint256 amount) internal {
  if (!_safeTransferETH(to, amount)) {
    weth.deposit{value: amount}();
    weth.transfer(to, amount);
  }
}

G06: Start minting from id 1 instead of 0

Line References

https://github.com/code-423n4/2022-05-runes/blob/060b4f82b79c8308fe65674a39a07c44fa586cd3/contracts/ForgottenRunesWarriorsGuild.sol#L102-L104

Description

As outlined in the 4th tip of this NFT gas optimization article, “if one of your users is going to make the first mint, make it cheaper for them by initializing the tokenId to 1.”

function mint(address recipient)
  public
  override
  nonReentrant
  returns (uint256)
{
  // note that < has been changed to <=
  // to account for shift in starting token ID
  require(numMinted <= MAX_WARRIORS, 'All warriors have been summoned');
  require(_msgSender() == minter, 'Not a minter');
  uint256 tokenId = ++numMinted;
  _safeMint(recipient, tokenId);
  return tokenId;
}

G07: Replace new bytes(0) with ""

Line References

https://github.com/code-423n4/2022-05-runes/blob/060b4f82b79c8308fe65674a39a07c44fa586cd3/contracts/ForgottenRunesWarriorsMinter.sol#L416

Description

It is more gas efficient to use "" as an argument to provide empty calldata instead of new bytes(0). The gas reporter reported a ~5k gas saving for the “should have reasonable gas to refund” test.

(bool success, ) = to.call{value: value, gas: 30_000}("");
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