Platform: Code4rena
Start Date: 20/09/2022
Pot Size: $100,000 USDC
Total HM: 4
Participants: 109
Period: 7 days
Judge: GalloDaSballo
Id: 163
League: ETH
Rank: 27/109
Findings: 2
Award: $930.57
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0x4non, 0x52, 0x5rings, 0xNazgul, 0xRobocop, 0xSmartContract, 0xdeadbeef, 0xsanson, 8olidity, Amithuddar, Aymen0909, B2, B353N, CertoraInc, Ch_301, Chom, CodingNameKiki, Deivitto, ElKu, Funen, JC, JohnnyTime, Kresh, Lambda, Noah3o6, RaymondFam, ReyAdmirado, RockingMiles, Rolezn, Sm4rty, SuldaanBeegsi, Tadashi, TomJ, Tomio, V_B, Waze, __141345__, a12jmx, ak1, arcoun, asutorufos, aviggiano, berndartmueller, bharg4v, bin2chen, brgltd, bulej93, c3phas, catchup, cccz, ch0bu, cryptonue, cryptphi, csanuragjain, delfin454000, devtooligan, djxploit, durianSausage, eighty, erictee, exd0tpy, fatherOfBlocks, giovannidisiena, hansfriese, ignacio, joestakey, ladboy233, lukris02, m9800, malinariy, martin, minhtrng, obront, oyc_109, pedr02b2, pedroais, pfapostol, philogy, prasantgupta52, rbserver, ronnyx2017, rotcivegaf, rvierdiiev, sach1r0, shung, simon135, throttle, tnevler, tonisives, wagmi, yixxas, zkhorse, zzykxx, zzzitron
55.1985 USDC - $55.20
Index event fields make the field more quickly accessible to off-chain tools that parse events. However, note that each index field costs extra gas during emission, so it's not necessarily best to index the maximum allowed per event (three fields). Each event should use three indexed fields if there are three or more fields, and gas usage is not particularly of concern for the events in question. If there are fewer than three fields, all of the fields should be indexed.
Instances number of this issue: 9
// src/ArtGobblers.sol 232: event GobblerPurchased(address indexed user, uint256 indexed gobblerId, uint256 price); 233: event LegendaryGobblerMinted(address indexed user, uint256 indexed gobblerId, uint256[] burnedGobblerIds); 234: event ReservedGobblersMinted(address indexed user, uint256 lastMintedGobblerId, uint256 numGobblersEach); 240: event GobblersRevealed(address indexed user, uint256 numGobblers, uint256 lastRevealedId); // src/Pages.sol 134: event PagePurchased(address indexed user, uint256 indexed pageId, uint256 price); 136: event CommunityPagesMinted(address indexed user, uint256 lastMintedPageId, uint256 numPages); // src/utils/token/GobblersERC721.sol 17: event ApprovalForAll(address indexed owner, address indexed operator, bool approved); // src/utils/token/GobblersERC1155B.sol 29: event ApprovalForAll(address indexed owner, address indexed operator, bool approved); // src/utils/token/PagesERC721.sol 18: event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
The pragma declared across the contracts is >=0.8.0. Locking the pragma (for e.g. by not using ^ in pragma solidity 0.8.0) ensures that contracts do not accidentally get deployed using an older compiler version with unfixed bugs. (see here)
Description: Contracts should be deployed with the same compiler version and flags that they have been tested with thoroughly. Locking the pragma helps to ensure that contracts do not accidentally get deployed using, for example, an outdated compiler version that might introduce bugs that affect the contract system negatively.
Recommendation: Consider locking the pragma version.
Use a solidity version of at least 0.8.4 to get custom errors, which are cheaper at deployment than revert()/require()
strings.
Use a solidity version of at least 0.8.10 to have external calls skip contract existence checks if the external call has a return value.
Description: Using very old versions of Solidity prevents benefits of bug fixes and newer security checks. Using the latest versions might make contracts susceptible to undiscovered compiler bugs.
Recommendation: Consider using the most recent version.
#0 - GalloDaSballo
2022-10-06T00:35:07Z
1NC for pragma
Events report needs an explanation please
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0xNazgul, 0xSmartContract, Atarpara, CertoraInc, Deathstore, Deivitto, ElKu, MiloTruck, ReyAdmirado, SnowMan, Tadashi, V_B, __141345__, aviggiano, catchup, djxploit, gogo, pfapostol, philogy, shung
875.3748 USDC - $875.37
If a variable is not set/initialized, it is assumed to have the default value (0 for uint, false for bool, address(0) for address…). Explicitly initializing it with its default value is an anti-pattern and wastes gas.
Instances number of this issue: 7
// src/ArtGobblers.sol 432: for (uint256 i = 0; i < cost; ++i) { 592: for (uint256 i = 0; i < numGobblers; ++i) { // src/Pages.sol 251: for (uint256 i = 0; i < numPages; i++) _mint(community, ++lastMintedPageId); // src/utils/GobblerReserve.sol 37: for (uint256 i = 0; i < ids.length; i++) { // src/utils/token/GobblersERC721.sol 186: for (uint256 i = 0; i < amount; ++i) { // rc/utils/token/GobblersERC1155B.sol 114: for (uint256 i = 0; i < owners.length; ++i) { 173: for (uint256 i = 0; i < amount; ++i) {
The demo of the loop gas comparison can be seen here.
Saves 5 gas per loop
Instances number of this issue: 1
// src/utils/GobblerReserve.sol 37: for (uint256 i = 0; i < ids.length; i++) {
The for loop length can be cached to memory before the loop, even for memory variables. The demo of the loop gas comparison can be seen here. Instances number of this issue: 1
// src/utils/GobblerReserve.sol 37: for (uint256 i = 0; i < ids.length; i++) {
Use bytes32 instead of string to save gas whenever possible. String is a dynamic data structure and therefore is more gas consuming then bytes32.
Instances number of this issue: 3 Bytes32 can be used instead of string in the following:
// src/ArtGobblers.sol 136: string public UNREVEALED_URI; 139: string public BASE_URI; // src/Pages.sol 96: string public BASE_URI;
revert()/require()
stringsCustom errors are available from solidity version 0.8.4. Custom errors save ~50 gas each time they're hit by avoiding having to allocate and store the revert string. Not defining the strings also save deployment gas
Instances number of this issue: 37
// lib/solmate/src/auth/Owned.sol 20: require(msg.sender == owner, "UNAUTHORIZED"); // lib/solmate/src/tokens/ERC721.sol 36: require((owner = _ownerOf[id]) != address(0), "NOT_MINTED"); 40: require(owner != address(0), "ZERO_ADDRESS"); 69: require(msg.sender == owner || isApprovedForAll[owner][msg.sender], "NOT_AUTHORIZED"); 87: require(from == _ownerOf[id], "WRONG_FROM"); 89: require(to != address(0), "INVALID_RECIPIENT"); 91-94: require( msg.sender == from || isApprovedForAll[from][msg.sender] || msg.sender == getApproved[id], "NOT_AUTHORIZED" ); 118-123: require( to.code.length == 0 || ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, "") == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); 134-139: require( to.code.length == 0 || ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, data) == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); 158: require(to != address(0), "INVALID_RECIPIENT"); 160: require(_ownerOf[id] == address(0), "ALREADY_MINTED"); 175: require(owner != address(0), "NOT_MINTED"); 196-201: require( to.code.length == 0 || ERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, "") == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); 211-216: require( to.code.length == 0 || ERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, data) == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); // lib/solmate/src/utils/SignedWadMath.sol 142: require(x > 0, "UNDEFINED"); // lib/VRGDAs/src/VRGDA.sol 32: require(decayConstant < 0, "NON_NEGATIVE_DECAY_CONSTANT"); // src/ArtGobblers.sol 437: require(getGobblerData[id].owner == msg.sender, "WRONG_FROM"); 885: require(from == getGobblerData[id].owner, "WRONG_FROM"); 887: require(to != address(0), "INVALID_RECIPIENT"); 889-892: require( msg.sender == from || isApprovedForAll[from][msg.sender] || msg.sender == getApproved[id], "NOT_AUTHORIZED" ); // src/utils/token/GobblersERC721.sol 62: require((owner = getGobblerData[id].owner) != address(0), "NOT_MINTED"); 66: require(owner != address(0), "ZERO_ADDRESS"); 95: require(msg.sender == owner || isApprovedForAll[owner][msg.sender], "NOT_AUTHORIZED"); 121-126: require( to.code.length == 0 || ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, "") == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); 137-142: require( to.code.length == 0 || ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, data) == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); // src/utils/token/GobblersERC1155B.sol 107: require(owners.length == ids.length, "LENGTH_MISMATCH"); 149-153: require( ERC1155TokenReceiver(to).onERC1155Received(msg.sender, address(0), id, 1, data) == ERC1155TokenReceiver.onERC1155Received.selector, "UNSAFE_RECIPIENT" ); 185-189: require( ERC1155TokenReceiver(to).onERC1155BatchReceived(msg.sender, address(0), ids, amounts, data) == ERC1155TokenReceiver.onERC1155BatchReceived.selector, "UNSAFE_RECIPIENT" ); src/utils/token/PagesERC721.sol 55: require((owner = _ownerOf[id]) != address(0), "NOT_MINTED"); 59: require(owner != address(0), "ZERO_ADDRESS"); 85: require(msg.sender == owner || isApprovedForAll(owner, msg.sender), "NOT_AUTHORIZED"); 103: require(from == _ownerOf[id], "WRONG_FROM"); 105: require(to != address(0), "INVALID_RECIPIENT"); 107-110: require( msg.sender == from || isApprovedForAll(from, msg.sender) || msg.sender == getApproved[id], "NOT_AUTHORIZED" ); 135-139: require( ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, "") == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); 151-155: require( ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, data) == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); // src/Pages.sol 266: if (pageId == 0 || pageId > currentId) revert("NOT_MINTED");
Instances number of this issue: 16
address(0)
check: 8
// lib/solmate/src/tokens/ERC721.sol 40: require(owner != address(0), "ZERO_ADDRESS"); 89: require(to != address(0), "INVALID_RECIPIENT"); 158: require(to != address(0), "INVALID_RECIPIENT"); 175: require(owner != address(0), "NOT_MINTED"); // src/ArtGobblers.sol 887: require(to != address(0), "INVALID_RECIPIENT"); // src/utils/token/GobblersERC721.sol 66: require(owner != address(0), "ZERO_ADDRESS"); src/utils/token/PagesERC721.sol 59: require(owner != address(0), "ZERO_ADDRESS"); 105: require(to != address(0), "INVALID_RECIPIENT");
ERC721TokenReceiver(to).onERC721Received
check: 8
// lib/solmate/src/tokens/ERC721.sol 118-123: require( to.code.length == 0 || ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, "") == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); 134-139: require( to.code.length == 0 || ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, data) == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); 196-201: require( to.code.length == 0 || ERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, "") == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); 211-216: require( to.code.length == 0 || ERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, data) == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); // src/utils/token/GobblersERC721.sol 121-126: require( to.code.length == 0 || ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, "") == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); 137-142: require( to.code.length == 0 || ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, data) == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); 135-139: require( ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, "") == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); 151-155: require( ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, data) == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" );
bool
for storage incurs overhead// Booleans are more expensive than uint256 or any type that takes up a full
// word because each write operation emits an extra SLOAD to first read the // slot's contents, replace the bits taken up by the boolean, and then write // back. This is the compiler's defense against contract upgrades and // pointer aliasing, and it cannot be disabled.
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/58f635312aa21f947cae5f8578638a85aa2519f5/contracts/security/ReentrancyGuard.sol#L23-L27 Use uint256(1) and uint256(2) for true/false to avoid a Gwarmaccess (100 gas) for the extra SLOAD, and to avoid Gsset (20000 gas) when changing from false to true, after having been true in the past
Instances number of this issue: 1
// src/ArtGobblers.sol 212: bool waitingForSeed;
When using elements that are smaller than 32 bytes, your contract’s gas usage may be higher. This is because the EVM operates on 32 bytes at a time. Therefore, if the element is smaller than that, the EVM must use more operations in order to reduce the size of the element from 32 bytes to the desired size.
Each operation involving a uint8 costs an extra 22-28 gas (depending on whether the other operand is also a variable of type uint8) as compared to ones involving uint256, due to the compiler having to clear the higher bits of the memory word before operating on the uint8, as well as the associated stack operations of doing so. Use a larger size then downcast where needed
Reference: Layout of State Variables in Storage
Instances number of this issue: 20
// src/ArtGobblers.sol 159: uint128 public numMintedFromGoo; 167: uint128 public currentNonLegendaryId; 189: uint128 startPrice; 191: uint128 numSold; 204: uint64 randomSeed; 206: uint64 nextRevealTimestamp; 208: uint56 lastRevealedId; 210: uint56 toBeRevealed; 899: uint32 emissionMultiple = getGobblerData[id].emissionMultiple; // Caching saves gas. // src/Pages.sol 107: uint128 public currentId; 114: uint128 public numMintedForCommunity; // src/utils/token/GobblersERC721.sol 38: uint64 idx; 40: uint32 emissionMultiple; 49: uint32 gobblersOwned; 51: uint32 emissionMultiple; 53: uint128 lastBalance; 55: uint64 lastTimestamp; // src/utils/token/GobblersERC1155B.sol 48: uint64 idx; 50: uint32 emissionMultiple;
Avoids a Gsset (20000 gas) in the constructor, and replaces each Gwarmacces (100 gas) with a PUSH32 (3 gas). If getters are still desired, '_' can be added to the variable name and the getter can be added manually.
Instances number of this issue: 3 The followings can be changed from public to internal or private:
// src/ArtGobblers.sol 136: string public UNREVEALED_URI; 139: string public BASE_URI; // src/Pages.sol 96: string public BASE_URI;
Changing the visibility from public to private or internal can save gas when a constant isn’t used outside of its contract.
Savings are due to the compiler not having to create non-payable getter functions for deployment calldata, and not adding another entry to the method ID table.
If needed, the value can be read from the verified contract source code.
A cheap way to store constants in a single location is to create an internal constant in a library. If the variable is a local cache of another contract’s value, consider making the cache variable internal or private, which will require external users to query the contract with the source of truth, so that callers don’t get out of sync.
Instances number of this issue: 13 The followings can be changed from public to internal or private:
// src/ArtGobblers.sol 112: uint256 public constant MAX_SUPPLY = 10000; 115: uint256 public constant MINTLIST_SUPPLY = 2000; 118: uint256 public constant LEGENDARY_SUPPLY = 10; 122: uint256 public constant RESERVED_SUPPLY = (MAX_SUPPLY - MINTLIST_SUPPLY - LEGENDARY_SUPPLY) / 5; 126-129: uint256 public constant MAX_MINTABLE = MAX_SUPPLY - MINTLIST_SUPPLY - LEGENDARY_SUPPLY - RESERVED_SUPPLY; 146: bytes32 public immutable merkleRoot; 156: uint256 public immutable mintStart; 159: uint128 public numMintedFromGoo; 177: uint256 public constant LEGENDARY_GOBBLER_INITIAL_START_PRICE = 69; 180: uint256 public constant FIRST_LEGENDARY_GOBBLER_ID = MAX_SUPPLY - LEGENDARY_SUPPLY + 1; 184: uint256 public constant LEGENDARY_AUCTION_INTERVAL = MAX_MINTABLE / (LEGENDARY_SUPPLY + 1); // lib/VRGDAs/src/VRGDA.sol 17: int256 public immutable targetPrice; 21: int256 internal immutable decayConstant;
Instances number of this issue: 6
ERC721TokenReceiver.onERC721Received.selector
and ERC1155TokenReceiver.onERC1155BatchReceived.selector
can be saved as constants.
// src/utils/token/GobblersERC721.sol 121-126: require( to.code.length == 0 || ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, "") == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); 137-142: require( to.code.length == 0 || ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, data) == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); // src/utils/token/PagesERC721.sol 135-139: require( ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, "") == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); 151-155: require( ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, data) == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); // src/utils/token/GobblersERC1155B.sol 149-153: require( ERC1155TokenReceiver(to).onERC1155Received(msg.sender, address(0), id, 1, data) == ERC1155TokenReceiver.onERC1155Received.selector, "UNSAFE_RECIPIENT" ); 185-189: require( ERC1155TokenReceiver(to).onERC1155BatchReceived(msg.sender, address(0), ids, amounts, data) == ERC1155TokenReceiver.onERC1155BatchReceived.selector, "UNSAFE_RECIPIENT" );
X = X + Y / X = X - Y
IS CHEAPER THAN X += Y / X -= Y
The demo of the gas comparison can be seen here.
Consider use X = X + Y / X = X - Y
to save gas.
Instances number of this issue: 10
// src/ArtGobblers.sol 439: burnedMultipleTotal += getGobblerData[id].emissionMultiple; 456: getUserData[msg.sender].emissionMultiple += uint32(burnedMultipleTotal); // Update multiple. 464: legendaryGobblerAuctionData.numSold += 1; // Increment the # of legendaries sold. 662: getUserData[currentIdOwner].emissionMultiple += uint32(newCurrentIdMultiple); 844: uint256 newNumMintedForReserves = numMintedForReserves += (numGobblersEach << 1); 912: getUserData[to].emissionMultiple += emissionMultiple; 905: getUserData[to].gobblersOwned += 1; getUserData[from].emissionMultiple -= emissionMultiple; 906: getUserData[from].gobblersOwned -= 1; // src/Pages.sol 244: uint256 newNumMintedForCommunity = numMintedForCommunity += uint128(numPages); // src/utils/token/GobblersERC721.sol 184: getUserData[to].gobblersOwned += uint32(amount);
#0 - GalloDaSballo
2022-10-06T01:38:07Z
6.3k from immutables
Rest 100 gas
Will not save gas because it's packed in an already dirty slot
6.4k