Platform: Code4rena
Start Date: 06/12/2022
Pot Size: $36,500 USDC
Total HM: 16
Participants: 119
Period: 3 days
Judge: berndartmueller
Total Solo HM: 2
Id: 189
League: ETH
Rank: 64/119
Findings: 1
Award: $35.02
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: slvDev
Also found by: 0x4non, Bnke0x0, Diana, Dinesh11G, RaymondFam, ReyAdmirado, adriro, ahmedov, ajtra, c3phas, cryptostellar5, nicobevi, pfapostol, sakshamguruji, tnevler, zaskoh
35.0246 USDC - $35.02
NB: Some functions have been truncated where neccessary to just show affected parts of the code
File:/src/minters/FixedPrice.sol 46: sale = Sale(0, 0, address(0), type(uint96).max, payable(0), type(uint96).max);
diff --git a/src/minters/FixedPrice.sol b/src/minters/FixedPrice.sol index 4e2cb9e..bee73c5 100644 --- a/src/minters/FixedPrice.sol +++ b/src/minters/FixedPrice.sol @@ -26,6 +26,8 @@ contract FixedPrice is Initializable, OwnableUpgradeable, ISale { /// @notice sale info for this fixed price edition Sale public sale; + uint96 private constant UINT96_MAX = type(uint96).max; + /// @notice Event emitted when sale created /// @param _saleInfo the sale info for the edition event Start(Sale _saleInfo); @@ -43,7 +45,7 @@ contract FixedPrice is Initializable, OwnableUpgradeable, ISale { constructor() { factory = msg.sender; _disableInitializers(); - sale = Sale(0, 0, address(0), type(uint96).max, payable(0), type(uint96).max); + sale = Sale(0, 0, address(0), UINT96_MAX, payable(0), UINT96_MAX); }
File: /src/minters/OpenEdition.sol 42: constructor() { 48: address(0), 49: type(uint96).max, 50: payable(0), 51: type(uint96).max 52: ); 53: }
The code can be optimized by minimizing the number of SLOADs.
SLOADs are expensive (100 gas after the 1st one) compared to MLOADs/MSTOREs (3 gas each). Storage values read multiple times should instead be cached in memory the first time (costing 1 SLOAD) and then read from this cache to avoid multiple SLOADs.
NB: Some functions have been truncated where neccessary to just show affected parts of the code
File: /src/uris/Generative.sol 27: function tokenToSeed(uint256 _tokenId) internal view returns (bytes32) { 28: require(seedBase != bytes32(0), "SEED BASE NOT SET"); 29: return keccak256(abi.encodePacked(_tokenId, seedBase)); 30: }
diff --git a/src/uris/Generative.sol b/src/uris/Generative.sol index db09737..2d24c12 100644 --- a/src/uris/Generative.sol +++ b/src/uris/Generative.sol @@ -25,7 +25,8 @@ contract Generative is Unique { } function tokenToSeed(uint256 _tokenId) internal view returns (bytes32) { - require(seedBase != bytes32(0), "SEED BASE NOT SET"); - return keccak256(abi.encodePacked(_tokenId, seedBase)); + bytes32 _seedBase = seedBase; + require(_seedBase != bytes32(0), "SEED BASE NOT SET"); + return keccak256(abi.encodePacked(_tokenId, _seedBase)); }
File: /src/minters/FixedPrice.sol 50: function cancel() external onlyOwner { 51: require(block.timestamp < sale.startTime, "TOO LATE"); 52: _end(sale); 53: }
diff --git a/src/minters/FixedPrice.sol b/src/minters/FixedPrice.sol index 4e2cb9e..e8819b2 100644 --- a/src/minters/FixedPrice.sol +++ b/src/minters/FixedPrice.sol @@ -48,8 +48,9 @@ contract FixedPrice is Initializable, OwnableUpgradeable, ISale { /// @notice Owner can cancel current sale function cancel() external onlyOwner { - require(block.timestamp < sale.startTime, "TOO LATE"); - _end(sale); + Sale _sale = sale; + require(block.timestamp < _sale.startTime, "TOO LATE"); + _end(_sale); }
File:/src/minters/OpenEdition.sol 75: function cancel() external onlyOwner { 76: require(block.timestamp < sale.startTime, "TOO LATE"); 77: _end(sale); 78: }
diff --git a/src/minters/OpenEdition.sol b/src/minters/OpenEdition.sol index 004f7fd..bd90e0f 100644 --- a/src/minters/OpenEdition.sol +++ b/src/minters/OpenEdition.sol @@ -73,8 +73,9 @@ contract OpenEdition is Initializable, OwnableUpgradeable, ISale { /// @notice cancel a fixed price sale function cancel() external onlyOwner { - require(block.timestamp < sale.startTime, "TOO LATE"); - _end(sale); + Sale storage _sale = sale; + require(block.timestamp < _sale.startTime, "TOO LATE"); + _end(_sale); }
File: /src/minters/OpenEdition.sol 107: function timeLeft() public view returns (uint256) { 108: return sale.endTime > block.timestamp ? sale.endTime - block.timestamp : 0; 109: }
diff --git a/src/minters/OpenEdition.sol b/src/minters/OpenEdition.sol index 004f7fd..fe819c0 100644 --- a/src/minters/OpenEdition.sol +++ b/src/minters/OpenEdition.sol @@ -105,7 +105,8 @@ contract OpenEdition is Initializable, OwnableUpgradeable, ISale { /// @notice how much time is left in the sale function timeLeft() public view returns (uint256) { - return sale.endTime > block.timestamp ? sale.endTime - block.timestamp : 0; + uint96 saleEndTime = sale.endTime; + return saleEndTime > block.timestamp ? saleEndTime - block.timestamp : 0; }
File: /src/minters/LPDA.sol 58: function buy(uint256 _amount) external payable { 66: amountSold += amount; 81: if (newId == temp.finalId) { 82: sale.finalPrice = uint80(price); 83: uint256 totalSale = price * amountSold; 88: } 89: }
diff --git a/src/minters/LPDA.sol b/src/minters/LPDA.sol index f52161c..6cf6952 100644 --- a/src/minters/LPDA.sol +++ b/src/minters/LPDA.sol @@ -62,8 +62,8 @@ contract LPDA is Initializable, OwnableUpgradeable, ISale { require(block.timestamp >= temp.startTime, "TOO SOON"); uint256 price = getPrice(); require(msg.value >= amount * price, "WRONG PRICE"); - - amountSold += amount; + uint48 _amountSold = amountSold; + amountSold = _amountSold + amount; uint48 newId = amount + temp.currentId; require(newId <= temp.finalId, "TOO MANY"); @@ -80,7 +80,7 @@ contract LPDA is Initializable, OwnableUpgradeable, ISale { if (newId == temp.finalId) { sale.finalPrice = uint80(price); - uint256 totalSale = price * amountSold; + uint256 totalSale = price * _amountSold; uint256 fee = totalSale / 20; ISaleFactory(factory).feeReceiver().transfer(fee); temp.saleReceiver.transfer(totalSale - fee);
Caching a mapping's value in a local storage or calldata variable when the value is accessed multiple times saves ~42 gas per access due to not having to perform the same offset calculation every time. Help the Optimizer by saving a storage variable's reference instead of repeatedly fetching it
To help the optimizer,declare a storage type variable and use it instead of repeatedly fetching the reference in a map or an array.
As an example, instead of repeatedly calling someMap[someIndex]
, save its reference like this: SomeStruct storage someStruct = someMap[someIndex]
and use it.
File: /src/minters/LPDA.sol receipts[msg.sender].amount += amount; receipts[msg.sender].balance += uint80(msg.value);
When fetching data from a storage location, assigning the data to a memory variable causes all fields of the struct/array to be read from storage, which incurs a Gcoldsload (2100 gas) for each field of the struct/array. If the fields are read from the new memory variable, they incur an additional MLOAD rather than a cheap stack read. Instead of declearing the variable with the memory keyword, declaring the variable with the storage keyword and caching any fields that need to be re-read in stack variables, will be much cheaper, only incuring the Gcoldsload for the fields actually read. The only time it makes sense to read the whole struct/array into a memory variable, is if the full struct/array is being returned by the function, is being passed to a function that requires memory, or if the array/struct is being read from another memory array/struct
File: /src/minters/FixedPrice.sol 57: function buy(uint256 _amount) external payable { 58: Sale memory sale_ = sale;
diff --git a/src/minters/FixedPrice.sol b/src/minters/FixedPrice.sol index 4e2cb9e..363258b 100644 --- a/src/minters/FixedPrice.sol +++ b/src/minters/FixedPrice.sol @@ -55,7 +55,7 @@ contract FixedPrice is Initializable, OwnableUpgradeable, ISale { /// @notice buy from a fixed price sale after the sale starts /// @param _amount the amount of editions to buy function buy(uint256 _amount) external payable { - Sale memory sale_ = sale; + Sale storage sale_ = sale; IEscher721 nft = IEscher721(sale_.edition); require(block.timestamp >= sale_.startTime, "TOO SOON"); require(_amount * sale_.price == msg.value, "WRONG PRICE");
File:/src/minters/OpenEdition.sol 57: function buy(uint256 _amount) external payable { 58: uint24 amount = uint24(_amount); 59: Sale memory temp = sale;
diff --git a/src/minters/OpenEdition.sol b/src/minters/OpenEdition.sol index 004f7fd..7133557 100644 --- a/src/minters/OpenEdition.sol +++ b/src/minters/OpenEdition.sol @@ -56,7 +56,7 @@ contract OpenEdition is Initializable, OwnableUpgradeable, ISale { /// @param _amount the amount of editions to buy function buy(uint256 _amount) external payable { uint24 amount = uint24(_amount); - Sale memory temp = sale; + Sale storage temp = sale; IEscher721 nft = IEscher721(temp.edition); require(block.timestamp >= temp.startTime, "TOO SOON"); require(block.timestamp < temp.endTime, "TOO LATE");
Min | Average | Median | Max | |
---|---|---|---|---|
Before | 864 | 17848 | 6864 | 45816 |
After | 418 | 16261 | 2418 | 45949 |
File: /src/minters/OpenEdition.sol 89: function finalize() public { 90: Sale memory temp = sale; 91: require(block.timestamp >= temp.endTime, "TOO SOON"); 92: ISaleFactory(factory).feeReceiver().transfer(address(this).balance / 20); 93: _end(temp); 94: }
diff --git a/src/minters/OpenEdition.sol b/src/minters/OpenEdition.sol index 004f7fd..04d1464 100644 --- a/src/minters/OpenEdition.sol +++ b/src/minters/OpenEdition.sol @@ -87,7 +87,7 @@ contract OpenEdition is Initializable, OwnableUpgradeable, ISale { /// @notice finish an open edition and payout the artist function finalize() public { - Sale memory temp = sale; + Sale storage temp = sale; require(block.timestamp >= temp.endTime, "TOO SOON"); ISaleFactory(factory).feeReceiver().transfer(address(this).balance / 20); _end(temp);
File: /src/minters/LPDA.sol 58: function buy(uint256 _amount) external payable { 59: uint48 amount = uint48(_amount); 60: Sale memory temp = sale;
diff --git a/src/minters/LPDA.sol b/src/minters/LPDA.sol index f52161c..bcf2b94 100644 --- a/src/minters/LPDA.sol +++ b/src/minters/LPDA.sol @@ -57,7 +57,7 @@ contract LPDA is Initializable, OwnableUpgradeable, ISale { /// @param _amount the amount of editions to buy function buy(uint256 _amount) external payable { uint48 amount = uint48(_amount); - Sale memory temp = sale; + Sale storage temp = sale; IEscher721 nft = IEscher721(temp.edition); require(block.timestamp >= temp.startTime, "TOO SOON"); uint256 price = getPrice();
Min | Average | Median | Max | |
---|---|---|---|---|
Before | 2148 | 7221 | 9174 | 9342 |
After | 2176 | 7195 | 9121 | 9289 |
File: /src/minters/LPDA.sol 99: function refund() public { 100: Receipt memory r = receipts[msg.sender];
diff --git a/src/minters/LPDA.sol b/src/minters/LPDA.sol index f52161c..5cc7bde 100644 --- a/src/minters/LPDA.sol +++ b/src/minters/LPDA.sol @@ -97,11 +97,11 @@ contract LPDA is Initializable, OwnableUpgradeable, ISale { /// @notice allow a buyer to get a refund on the current price difference function refund() public { - Receipt memory r = receipts[msg.sender]; + Receipt storage r = receipts[msg.sender]; uint80 price = uint80(getPrice()) * r.amount; uint80 owed = r.balance - price; require(owed > 0, "NOTHING TO REFUND"); - receipts[msg.sender].balance = price; + r.balance = price; payable(msg.sender).transfer(owed); }
Min | Average | Median | Max | |
---|---|---|---|---|
Before | 1501 | 1501 | 1501 | 1501 |
After | 1268 | 1268 | 1268 | 1268 |
File: /src/minters/LPDA.sol 117: function getPrice() public view returns (uint256) { 118: Sale memory temp = sale;
diff --git a/src/minters/LPDA.sol b/src/minters/LPDA.sol index f52161c..65a1918 100644 --- a/src/minters/LPDA.sol +++ b/src/minters/LPDA.sol @@ -115,7 +115,7 @@ contract LPDA is Initializable, OwnableUpgradeable, ISale { /// @notice the price of the sale function getPrice() public view returns (uint256) { - Sale memory temp = sale; + Sale storage temp = sale;
File: /src/minters/LPDA.sol 66: amountSold += amount;
diff --git a/src/minters/LPDA.sol b/src/minters/LPDA.sol index f52161c..5de2d13 100644 --- a/src/minters/LPDA.sol +++ b/src/minters/LPDA.sol @@ -63,7 +63,7 @@ contract LPDA is Initializable, OwnableUpgradeable, ISale { uint256 price = getPrice(); require(msg.value >= amount * price, "WRONG PRICE"); - amountSold += amount; + amountSold = amountSold + amount; uint48 newId = amount + temp.currentId; require(newId <= temp.finalId, "TOO MANY");
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.
https://docs.soliditylang.org/en/v0.8.11/internals/layout_in_storage.html Use a larger size then downcast where needed
File: /src/Escher721.sol //@audit: uint96 feeNumerator 64: function setDefaultRoyalty( 65: address receiver, 66: uint96 feeNumerator 67: ) public onlyRole(DEFAULT_ADMIN_ROLE) { 68: _setDefaultRoyalty(receiver, feeNumerator); 69: }
Solidity version 0.8+ comes with implicit overflow and underflow checks on unsigned integers. When an overflow or an underflow isn’t possible (as an example, when a comparison is made before the arithmetic operation), some gas can be saved by using an unchecked block
File: /src/minters/LPDA.sol 86: temp.saleReceiver.transfer(totalSale - fee);
diff --git a/src/minters/LPDA.sol b/src/minters/LPDA.sol index f52161c..f1e4277 100644 --- a/src/minters/LPDA.sol +++ b/src/minters/LPDA.sol @@ -83,7 +83,10 @@ contract LPDA is Initializable, OwnableUpgradeable, ISale { uint256 totalSale = price * amountSold; uint256 fee = totalSale / 20; ISaleFactory(factory).feeReceiver().transfer(fee); - temp.saleReceiver.transfer(totalSale - fee); + unchecked { + temp.saleReceiver.transfer(totalSale - fee); + } _end(); } }
totalSale - fee
would never underflow since fee
is equivalent to totalSale / 20
see Line 84
The majority of Solidity for loops increment a uint256 variable that starts at 0. These increment operations never need to be checked for over/underflow because the variable will never reach the max number of uint256 (will run out of gas long before that happens). The default over/underflow check wastes gas in every iteration of virtually every for loop . eg.
e.g Let's work with a sample loop below.
for(uint256 i; i < 10; i++){ //doSomething }
can be written as shown below.
for(uint256 i; i < 10;) { // loop logic unchecked { i++; } }
We can also write it as an inlined function like below.
function inc(i) internal pure returns (uint256) { unchecked { return i + 1; } } for(uint256 i; i < 10; i = inc(i)) { // doSomething }
Affected code https://github.com/code-423n4/2022-12-escher/blob/5d8be6aa0e8634fdb2f328b99076b0d05fefab73/src/minters/FixedPrice.sol#L65-L67
File: /src/minters/FixedPrice.sol 65: for (uint48 x = sale_.currentId + 1; x <= newId; x++) { 66: nft.mint(msg.sender, x); 67: }
diff --git a/src/minters/FixedPrice.sol b/src/minters/FixedPrice.sol index 4e2cb9e..be2a3d7 100644 --- a/src/minters/FixedPrice.sol +++ b/src/minters/FixedPrice.sol @@ -62,8 +62,11 @@ contract FixedPrice is Initializable, OwnableUpgradeable, ISale { uint48 newId = uint48(_amount) + sale_.currentId; require(newId <= sale_.finalId, "TOO MANY"); - for (uint48 x = sale_.currentId + 1; x <= newId; x++) { + for (uint48 x = sale_.currentId + 1; x <= newId;) { nft.mint(msg.sender, x); + unchecked { + ++x; + } }
File:/src/minters/OpenEdition.sol 66: for (uint24 x = temp.currentId + 1; x <= newId; x++) { 67: nft.mint(msg.sender, x); 68: }
diff --git a/src/minters/OpenEdition.sol b/src/minters/OpenEdition.sol index 004f7fd..252fd65 100644 --- a/src/minters/OpenEdition.sol +++ b/src/minters/OpenEdition.sol @@ -63,8 +63,11 @@ contract OpenEdition is Initializable, OwnableUpgradeable, ISale { require(amount * sale.price == msg.value, "WRONG PRICE"); uint24 newId = amount + temp.currentId; - for (uint24 x = temp.currentId + 1; x <= newId; x++) { + for (uint24 x = temp.currentId + 1; x <= newId;) { nft.mint(msg.sender, x); + unchecked { + ++x; + } }
File: /src/minters/LPDA.sol 73: for (uint256 x = temp.currentId + 1; x <= newId; x++) { 74: nft.mint(msg.sender, x); 75: }
diff --git a/src/minters/LPDA.sol b/src/minters/LPDA.sol index f52161c..8bc1680 100644 --- a/src/minters/LPDA.sol +++ b/src/minters/LPDA.sol @@ -70,8 +70,11 @@ contract LPDA is Initializable, OwnableUpgradeable, ISale { receipts[msg.sender].amount += amount; receipts[msg.sender].balance += uint80(msg.value); - for (uint256 x = temp.currentId + 1; x <= newId; x++) { + for (uint256 x = temp.currentId + 1; x <= newId;) { nft.mint(msg.sender, x); + unchecked { + ++x; + }
#0 - c4-judge
2022-12-14T13:23:57Z
berndartmueller marked the issue as grade-b