Platform: Code4rena
Start Date: 17/03/2023
Pot Size: $36,500 USDC
Total HM: 10
Participants: 98
Period: 3 days
Judge: leastwood
Total Solo HM: 5
Id: 223
League: ETH
Rank: 37/98
Findings: 1
Award: $77.59
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xSmartContract
Also found by: 0xdaydream, 0xnev, Aymen0909, Deekshith99, Diana, EvanW, Fanz, JCN, Jerry0x, K42, Kresh, Madalad, MiniGlome, Polaris_tow, Rageur, ReyAdmirado, Rolezn, SAAJ, SaeedAlipoor01988, Sathish9098, Shubham, Udsen, Viktor_Cortess, Walter, anodaram, arialblack14, atharvasama, caspersolangii, codeslide, descharre, fatherOfBlocks, felipe, ginlee, igingu, lukris02, nadin, slvDev, tnevler, turvy_fuzz, viking71
77.5893 USDC - $77.59
A majority of the optimizations were benchmarked via the protocol's tests. All benchmarked issues will include gas savings as seen from the output of forge snapshot --diff
.
Notes: Some code snippets may be truncated to save space.
Overall change to canto-bio-protocol
from forge snapshot:
testMint() (gas: -23 (-0.020%)) testRevertOver200() (gas: -6 (-0.059%)) testRevertLen0() (gas: -6 (-0.061%)) testShortString(string) (gas: -1098 (-0.145%)) testEmojiAtBoundaries2() (gas: -1525 (-0.176%)) testLongString(string) (gas: -2052 (-0.207%)) testEmojiAtBoundaries() (gas: -2060 (-0.224%)) Overall gas change: -6770 (-0.184%)
Overall change to canto-pfp-protocol
from forge snapshot:
testGetPFPWithNotMintedTokenID() (gas: 0 (0.000%)) testMintNFTOwnedByCaller() (gas: -31 (-0.011%)) testPFPNoLongerOwnedByOriginalOwner() (gas: -31 (-0.012%)) testNFTTransferredAfterwards() (gas: -31 (-0.012%)) testTokenURINFTOwnedByOwnerOfCIDNFT() (gas: -31 (-0.013%)) testPFPAssociatedWithNoCIDNFT() (gas: -31 (-0.016%)) testCannotMintNFTNotOwnedByCaller() (gas: -22151 (-24.664%)) Overall gas change: -22306 (-1.654%)
Overall change to canto-namespace-protocol
from forge snapshot:
testChangeNoteAddressByNonOwner() (gas: 0 (0.000%)) testChangeNoteAddressByOwner() (gas: 0 (0.000%)) testChangeRevenueAddressByNonOwner() (gas: 0 (0.000%)) testChangeRevenueAddressByOwner() (gas: 0 (0.000%)) testFusingWith0Characters() (gas: 0 (0.000%)) testFusingWithMoreThan13Characters() (gas: 0 (0.000%)) testTokenURIOfNonMintedToken() (gas: 0 (0.000%)) testChangeNoteAddress() (gas: 0 (0.000%)) testChangeNoteAddressNonOwner() (gas: 0 (0.000%)) testChangeRevenueAddressByOwner() (gas: 0 (0.000%)) testEndPrelaunchPhaseByNonOwner() (gas: 0 (0.000%)) testRevertNonOwnerChangeRevenueAddress() (gas: 0 (0.000%)) testRevertUnminted() (gas: 0 (0.000%)) testRevertUnmintedGetTiles() (gas: 0 (0.000%)) testOwnerBuyTooManyInPrelaunchPhase() (gas: -9 (-0.047%)) testEndPrelaunchPhaseByOwner() (gas: -28 (-0.172%)) testTokenURIForNonPrelaunchTray() (gas: -3263 (-0.404%)) testFusingAsApprovedForAllOfTrayOwner() (gas: -2397 (-0.466%)) testFusingAsApprovedOfTray() (gas: -2416 (-0.472%)) testBurnAsApprovedForAllOfOwner() (gas: -2541 (-0.503%)) testFusingWithOneTrayAndOneTile() (gas: -2417 (-0.509%)) testFusingAsOwnerOfTray() (gas: -2417 (-0.511%)) testBurnAsOwner() (gas: -2528 (-0.527%)) testFusingWithOneTrayAndMultiTiles() (gas: -2924 (-0.553%)) testTokenURIProperlyRendered() (gas: -2522 (-0.588%)) testTokenURIOfBurnedToken() (gas: -2453 (-0.618%)) testBurnNonPrelaunchTrayByApprovedForAll() (gas: -1955 (-0.619%)) testTokenURIForPrelaunchTrayDuringPrelaunch() (gas: -3215 (-0.621%)) testBurnNonPrelaunchTrayByApproved() (gas: -1960 (-0.624%)) testTransferNormalTray() (gas: -2198 (-0.630%)) testBurnNonPrelaunchTrayByOwner() (gas: -1953 (-0.666%)) testFusingWithPrelaunchTrayAfterPrelaunch() (gas: -2808 (-0.672%)) testBuyingOneAfterPrelaunchPhase() (gas: -2178 (-0.683%)) testBurnAsApproved() (gas: -4686 (-0.692%)) testTransferDuringPrelaunch() (gas: -2130 (-0.786%)) testFuseEmojiDoesNotSupportSkinTone() (gas: -2589 (-0.802%)) testFusingWithDuplicateTiles() (gas: -2521 (-0.808%)) testFuseCharacterWithSkinToneModifier() (gas: -2543 (-0.812%)) testGetValidTiles() (gas: -2130 (-0.838%)) testFusingWithMuTrayAndMultiTiles() (gas: -9153 (-0.850%)) testGetTileBurned() (gas: -1879 (-0.862%)) testRevertTooHighTileOffset() (gas: -2139 (-0.868%)) testTransferAfterPrelaunch() (gas: -2191 (-0.869%)) testBurnInPrelaunchPhase() (gas: -1879 (-0.869%)) testBuyInPrelaunchPhase() (gas: -2139 (-0.876%)) testGetValidTile() (gas: -2130 (-0.880%)) testBurnPrelaunchTray() (gas: -1950 (-0.894%)) testSelectTrayIdToFuseSupportSkinTone() (gas: -9998 (-0.915%)) testFusingDuringPrelaunch() (gas: -5601 (-0.965%)) testFuseWithEmojiSupportToneModifier() (gas: -11445 (-1.004%)) testBuyingMultipleOnesAfterPrelaunchPhase() (gas: -11558 (-1.164%)) testComplexScenario() (gas: -278460 (-1.319%)) testOwnerBuyMaxInPrelaunchPhase() (gas: -2344785 (-1.332%)) Overall gas change: -2744088 (-1.268%)
Number | Issue | Instances | Estimated Gas Savings |
---|---|---|---|
G-01 | Refactor code to avoid writing to and reading from storage in for loop | - | - |
G-02 | Refactor code to fail early and save 5k gas for users that input wrong parameters | - | 5000 |
G-03 | Cache pointer to struct in calldata/memory to avoid re-calculating offset | 6 | - |
G-04 | If statements that use && can be refactored into nested if statements | 11 | - |
G-05 | Expressions can be unchecked if guranteed to not over/underflow due to previous if /require statement or other constraints | 6 | - |
G-06 | Cache struct value from return data | 1 | - |
G-07 | Multiple uint/ID mappings can be combined into a single mapping of a uint/ID to a struct, where appropriate | - | - |
G-08 | Consider creating additional burn function in Tray.sol to avoid multiple external calls in for loop | - | - |
G-09 | Consider using bytes32 to set variable as immutable and save an SLOAD | 1 | 2100 |
for loop
Writing to storage and reading from storage should always try to be avoided in loops. In the following instance we are able to cache the value of lastHash
outside of the loop to avoid 7 SSTOREs and 14 SLOADs on every iteration. We can then update lastHash
by storing the cached value into storage once we are outisde of the loop.
Gas Savings as shown via forge snapshot --diff
:
testOwnerBuyTooManyInPrelaunchPhase() (gas: -9 (-0.047%)) testBurnAsApproved() (gas: -1704 (-0.252%)) testTokenURIForNonPrelaunchTray() (gas: -2130 (-0.264%)) testFusingWithOneTrayAndMultiTiles() (gas: -1704 (-0.322%)) testFusingAsApprovedForAllOfTrayOwner() (gas: -1704 (-0.331%)) testFusingAsApprovedOfTray() (gas: -1704 (-0.333%)) testBurnAsApprovedForAllOfOwner() (gas: -1704 (-0.337%)) testBurnAsOwner() (gas: -1704 (-0.355%)) testFusingWithOneTrayAndOneTile() (gas: -1704 (-0.359%)) testFusingAsOwnerOfTray() (gas: -1704 (-0.360%)) testFusingDuringPrelaunch() (gas: -2130 (-0.367%)) testTokenURIProperlyRendered() (gas: -1704 (-0.397%)) testTokenURIForPrelaunchTrayDuringPrelaunch() (gas: -2130 (-0.411%)) testTokenURIOfBurnedToken() (gas: -1704 (-0.430%)) testFusingWithPrelaunchTrayAfterPrelaunch() (gas: -2130 (-0.510%)) testBurnNonPrelaunchTrayByApprovedForAll() (gas: -1704 (-0.539%)) testBurnNonPrelaunchTrayByApproved() (gas: -1704 (-0.542%)) testBurnNonPrelaunchTrayByOwner() (gas: -1704 (-0.581%)) testTransferNormalTray() (gas: -2130 (-0.611%)) testFuseEmojiDoesNotSupportSkinTone() (gas: -2130 (-0.660%)) testFusingWithMuTrayAndMultiTiles() (gas: -7160 (-0.665%)) testBuyingOneAfterPrelaunchPhase() (gas: -2130 (-0.668%)) testFuseCharacterWithSkinToneModifier() (gas: -2130 (-0.680%)) testFusingWithDuplicateTiles() (gas: -2130 (-0.683%)) testSelectTrayIdToFuseSupportSkinTone() (gas: -8520 (-0.780%)) testBurnPrelaunchTray() (gas: -1704 (-0.781%)) testGetTileBurned() (gas: -1711 (-0.785%)) testTransferDuringPrelaunch() (gas: -2130 (-0.786%)) testBurnInPrelaunchPhase() (gas: -1711 (-0.792%)) testGetValidTiles() (gas: -2130 (-0.838%)) testTransferAfterPrelaunch() (gas: -2130 (-0.845%)) testRevertTooHighTileOffset() (gas: -2139 (-0.868%)) testBuyInPrelaunchPhase() (gas: -2139 (-0.876%)) testGetValidTile() (gas: -2130 (-0.880%)) testFuseWithEmojiSupportToneModifier() (gas: -10650 (-0.934%)) testBuyingMultipleOnesAfterPrelaunchPhase() (gas: -11510 (-1.159%)) testComplexScenario() (gas: -257520 (-1.219%)) testOwnerBuyMaxInPrelaunchPhase() (gas: -2344785 (-1.332%)) Overall gas change: -2699400 (-1.248%)
File: canto-namespace-protocol/src/Tray.sol 159: for (uint256 i; i < _amount; ++i) { 160: TileData[TILES_PER_TRAY] memory trayTiledata; 161: for (uint256 j; j < TILES_PER_TRAY; ++j) { 162: lastHash = keccak256(abi.encode(lastHash)); 163: trayTiledata[j] = _drawing(uint256(lastHash)); 164: } 165: tiles[startingTrayId + i] = trayTiledata; 166: }
diff --git a/src/Tray.sol b/src/Tray.sol index 4796cf6..1337443 100644 --- a/src/Tray.sol +++ b/src/Tray.sol @@ -156,14 +156,16 @@ contract Tray is ERC721A, Owned { } else { SafeTransferLib.safeTransferFrom(note, msg.sender, revenueAddress, _amount * trayPrice); } + bytes32 _lastHash = lastHash; for (uint256 i; i < _amount; ++i) { TileData[TILES_PER_TRAY] memory trayTiledata; for (uint256 j; j < TILES_PER_TRAY; ++j) { - lastHash = keccak256(abi.encode(lastHash)); - trayTiledata[j] = _drawing(uint256(lastHash)); + _lastHash = keccak256(abi.encode(_lastHash)); + trayTiledata[j] = _drawing(uint256(_lastHash)); } tiles[startingTrayId + i] = trayTiledata; } + lastHash = _lastHash; _mint(msg.sender, _amount); // We do not use _safeMint on purpose here to disallow callbacks and save gas }
5k gas
for users that input wrong parametersIn the instance below an SLOAD and SSTORE is occuring before the access control check in the if
statement. If the check causes a revert, the user who mistakenly called mint
with invalid parameters will incur an additional gas cost of ~5000 gas (Gcoldsload (2100 gas)
& Gsreset (2900 gas)
). To save 1 SLOAD and 1 SSTORE for users who accidentally call mint
with the wrong parameters, move the if
statement to the very top of the function.
Note that the ~22151 gas savings below are coming from the scenario where a user is the FIRST one to call mint AND uses invalid parameters (Gsset (20000 gas)
& Gcoldsload (2100 gas)
). After numMinted
has been incremented once, every user who calls mint with invalid parameters will only incur an extra cost of ~5000 gas.
Gas Savings as shown via forge snapshot --diff
:
testGetPFPWithNotMintedTokenID() (gas: 0 (0.000%)) testMintNFTOwnedByCaller() (gas: -6 (-0.002%)) testPFPNoLongerOwnedByOriginalOwner() (gas: -6 (-0.002%)) testNFTTransferredAfterwards() (gas: -6 (-0.002%)) testTokenURINFTOwnedByOwnerOfCIDNFT() (gas: -6 (-0.002%)) testPFPAssociatedWithNoCIDNFT() (gas: -6 (-0.003%)) testCannotMintNFTNotOwnedByCaller() (gas: -22151 (-24.664%)) Overall gas change: -22181 (-1.645%)
File: canto-pfp-protocol/src/ProfilePicture.sol 79: function mint(address _nftContract, uint256 _nftID) external { 80: uint256 tokenId = ++numMinted; 81: if (ERC721(_nftContract).ownerOf(_nftID) != msg.sender) 82: revert PFPNotOwnedByCaller(msg.sender, _nftContract, _nftID); 83: ProfilePictureData storage pictureData = pfp[tokenId]; 84: pictureData.nftContract = _nftContract; 85: pictureData.nftID = _nftID; 86: _mint(msg.sender, tokenId); 87: emit PfpAdded(msg.sender, tokenId, _nftContract, _nftID); 88: }
diff --git a/src/ProfilePicture.sol b/src/ProfilePicture.sol index 54d814a..e568879 100644 --- a/src/ProfilePicture.sol +++ b/src/ProfilePicture.sol @@ -77,9 +77,9 @@ contract ProfilePicture is ERC721 { /// @param _nftContract The nft contract address to reference /// @param _nftID The nft ID to reference function mint(address _nftContract, uint256 _nftID) external { - uint256 tokenId = ++numMinted; if (ERC721(_nftContract).ownerOf(_nftID) != msg.sender) revert PFPNotOwnedByCaller(msg.sender, _nftContract, _nftID); + uint256 tokenId = ++numMinted; ProfilePictureData storage pictureData = pfp[tokenId]; pictureData.nftContract = _nftContract; pictureData.nftID = _nftID;
Caching a mapping/array's value in a storage pointer when the value is accessed multiple times saves ~40 gas per access due to not having to perform the same offset calculation every time. In the instances below the offset calculation is happening multiple times within loops so the gas costs could vary depending on the number of iterations in the loop.
Gas Savings as shown via forge snapshot --diff
:
testFuseWithEmojiSupportToneModifier() (gas: -196 (-0.017%)) testFusingAsApprovedForAllOfTrayOwner() (gas: -159 (-0.031%)) testBurnAsApprovedForAllOfOwner() (gas: -157 (-0.031%)) testFusingAsApprovedOfTray() (gas: -160 (-0.031%)) testBurnAsOwner() (gas: -157 (-0.033%)) testFusingWithOneTrayAndOneTile() (gas: -157 (-0.033%)) testFusingAsOwnerOfTray() (gas: -157 (-0.033%)) testSelectTrayIdToFuseSupportSkinTone() (gas: -376 (-0.034%)) testTokenURIProperlyRendered() (gas: -157 (-0.037%)) testTokenURIOfBurnedToken() (gas: -157 (-0.040%)) testFusingWithPrelaunchTrayAfterPrelaunch() (gas: -195 (-0.047%)) testFusingWithDuplicateTiles() (gas: -164 (-0.053%)) testFuseEmojiDoesNotSupportSkinTone() (gas: -180 (-0.056%)) testFuseCharacterWithSkinToneModifier() (gas: -180 (-0.058%)) testFusingWithMuTrayAndMultiTiles() (gas: -827 (-0.077%)) testComplexScenario() (gas: -16512 (-0.078%)) testFusingWithOneTrayAndMultiTiles() (gas: -627 (-0.119%)) testBurnAsApproved() (gas: -2202 (-0.325%)) testFusingDuringPrelaunch() (gas: -2752 (-0.474%)) Overall gas change: -25472 (-0.012%)
File: canto-namespace-protocol/src/Namespace.sol 122: for (uint256 i; i < numCharacters; ++i) { 123: bool isLastTrayEntry = true; 124: uint256 trayID = _characterList[i].trayID; // @audit: 1st access 125: uint8 tileOffset = _characterList[i].tileOffset; // @audit: 2nd access 126: // Check for duplicate characters in the provided list. 1/2 * n^2 loop iterations, but n is bounded to 13 and we do not perform any storage operations 127: for (uint256 j = i + 1; j < numCharacters; ++j) { 128: if (_characterList[j].trayID == trayID) { // @audit: 1st access 129: isLastTrayEntry = false; 130: if (_characterList[j].tileOffset == tileOffset) revert FusingDuplicateCharactersNotAllowed(); // @audit: 2nd access 131: } 132: } 133: Tray.TileData memory tileData = tray.getTile(trayID, tileOffset); // Will revert if tileOffset is too high 134: uint8 characterModifier = tileData.characterModifier; 135: 136: if (tileData.fontClass != 0 && _characterList[i].skinToneModifier != 0) { // @audit: 3rd access 137: revert CannotFuseCharacterWithSkinTone(); 138: } 139: 140: if (tileData.fontClass == 0) { 141: // Emoji 142: characterModifier = _characterList[i].skinToneModifier; // @audit: 4th access 143: }
diff --git a/src/Namespace.sol b/src/Namespace.sol index 484d049..6a123c8 100644 --- a/src/Namespace.sol +++ b/src/Namespace.sol @@ -121,25 +121,27 @@ contract Namespace is ERC721, Owned { uint256[] memory uniqueTrays = new uint256[](_characterList.length); for (uint256 i; i < numCharacters; ++i) { bool isLastTrayEntry = true; - uint256 trayID = _characterList[i].trayID; - uint8 tileOffset = _characterList[i].tileOffset; + CharacterData calldata _characterData = _characterList[i]; + uint256 trayID = _characterData.trayID; + uint8 tileOffset = _characterData.tileOffset; // Check for duplicate characters in the provided list. 1/2 * n^2 loop iterations, but n is bounded to 13 and we do not perform any storage operations for (uint256 j = i + 1; j < numCharacters; ++j) { - if (_characterList[j].trayID == trayID) { + CharacterData calldata _characterDataInner = _characterList[j]; + if (_characterDataInner.trayID == trayID) { isLastTrayEntry = false; - if (_characterList[j].tileOffset == tileOffset) revert FusingDuplicateCharactersNotAllowed(); + if (_characterDataInner.tileOffset == tileOffset) revert FusingDuplicateCharactersNotAllowed(); } } Tray.TileData memory tileData = tray.getTile(trayID, tileOffset); // Will revert if tileOffset is too high uint8 characterModifier = tileData.characterModifier; - if (tileData.fontClass != 0 && _characterList[i].skinToneModifier != 0) { + if (tileData.fontClass != 0 && _characterData.skinToneModifier != 0) { revert CannotFuseCharacterWithSkinTone(); } if (tileData.fontClass == 0) { // Emoji - characterModifier = _characterList[i].skinToneModifier; + characterModifier = _characterData.skinToneModifier; } bytes memory charAsBytes = Utils.characterToUnicodeBytes(0, tileData.characterIndex, characterModifier); tileData.characterModifier = characterModifier;
Gas Savings as shown via forge snapshot --diff
:
testSelectTrayIdToFuseSupportSkinTone() (gas: -149 (-0.014%)) testTokenURIProperlyRendered() (gas: -119 (-0.028%)) testTokenURIForNonPrelaunchTray() (gas: -1085 (-0.134%)) testTokenURIForPrelaunchTrayDuringPrelaunch() (gas: -1085 (-0.209%)) Overall gas change: -2438 (-0.001%)
File: canto-namespace-protocol/src/Utils.sol 231: string( 232: characterToUnicodeBytes(_tiles[i].fontClass, _tiles[i].characterIndex, _tiles[i].characterModifier) // @audit: 3 accesses 233: ),
diff --git a/src/Utils.sol b/src/Utils.sol index 66b61c6..d491b77 100644 --- a/src/Utils.sol +++ b/src/Utils.sol @@ -223,13 +223,14 @@ library Utils { string memory innerData; uint256 dx = 400 / (_tiles.length + 1); for (uint256 i; i < _tiles.length; ++i) { + Tray.TileData memory _tileData = _tiles[i]; innerData = string.concat( innerData, '<text dominant-baseline="middle" text-anchor="middle" y="100" x="', LibString.toString(dx * (i + 1)), '">', string( - characterToUnicodeBytes(_tiles[i].fontClass, _tiles[i].characterIndex, _tiles[i].characterModifier) + characterToUnicodeBytes(_tileData.fontClass, _tileData.characterIndex, _tileData.characterModifier) ), "</text>" );
If
statements that use &&
can be refactored into nested if
statementsGas Savings as shown via forge snapshot --diff
:
testComplexScenario() (gas: -412 (-0.002%)) testFuseWithEmojiSupportToneModifier() (gas: -70 (-0.006%)) testSelectTrayIdToFuseSupportSkinTone() (gas: -70 (-0.006%)) testBurnAsApproved() (gas: -50 (-0.007%)) testFusingWithOneTrayAndMultiTiles() (gas: -49 (-0.009%)) testFusingAsApprovedForAllOfTrayOwner() (gas: -49 (-0.010%)) testBurnAsApprovedForAllOfOwner() (gas: -49 (-0.010%)) testFusingAsApprovedOfTray() (gas: -50 (-0.010%)) testBurnAsOwner() (gas: -50 (-0.010%)) testFusingWithOneTrayAndOneTile() (gas: -50 (-0.011%)) testFusingAsOwnerOfTray() (gas: -50 (-0.011%)) testBurnNonPrelaunchTrayByApprovedForAll() (gas: -36 (-0.011%)) testBurnNonPrelaunchTrayByOwner() (gas: -34 (-0.012%)) testFusingDuringPrelaunch() (gas: -70 (-0.012%)) testTokenURIProperlyRendered() (gas: -56 (-0.013%)) testBurnNonPrelaunchTrayByApproved() (gas: -41 (-0.013%)) testFusingWithMuTrayAndMultiTiles() (gas: -149 (-0.014%)) testTokenURIOfBurnedToken() (gas: -56 (-0.014%)) testBurnPrelaunchTray() (gas: -34 (-0.016%)) testGetTileBurned() (gas: -34 (-0.016%)) testBurnInPrelaunchPhase() (gas: -35 (-0.016%)) testFusingWithPrelaunchTrayAfterPrelaunch() (gas: -89 (-0.021%)) Overall gas change: -1583 (-0.001%)
File: canto-namespace-protocol/src/Tray.sol 175: if ( 176: namespaceNFT != msg.sender && 177: trayOwner != msg.sender && 178: getApproved(_id) != msg.sender && 179: !isApprovedForAll(trayOwner, msg.sender) 180: ) revert CallerNotAllowedToBurn(); 181: if (msg.sender == namespaceNFT) { 182: // Disallow fusing for prelaunch trays after phase has ended 183: uint256 numPrelaunchMinted = prelaunchMinted; 184: if (numPrelaunchMinted != type(uint256).max && _id <= numPrelaunchMinted) 185: revert PrelaunchTrayCannotBeUsedAfterPrelaunch(_id); 186: }
diff --git a/src/Tray.sol b/src/Tray.sol index 4796cf6..553c0ed 100644 --- a/src/Tray.sol +++ b/src/Tray.sol @@ -172,17 +172,23 @@ contract Tray is ERC721A, Owned { /// @param _id Tray ID function burn(uint256 _id) external { address trayOwner = ownerOf(_id); - if ( - namespaceNFT != msg.sender && - trayOwner != msg.sender && - getApproved(_id) != msg.sender && - !isApprovedForAll(trayOwner, msg.sender) - ) revert CallerNotAllowedToBurn(); + if (namespaceNFT != msg.sender) { + if (trayOwner != msg.sender) { + if (getApproved(_id) != msg.sender) { + if (!isApprovedForAll(trayOwner, msg.sender)) { + revert CallerNotAllowedToBurn(); + } + } + } + } if (msg.sender == namespaceNFT) { // Disallow fusing for prelaunch trays after phase has ended uint256 numPrelaunchMinted = prelaunchMinted; - if (numPrelaunchMinted != type(uint256).max && _id <= numPrelaunchMinted) - revert PrelaunchTrayCannotBeUsedAfterPrelaunch(_id); + if (numPrelaunchMinted != type(uint256).max) { + if (_id <= numPrelaunchMinted) { + revert PrelaunchTrayCannotBeUsedAfterPrelaunch(_id); + } + } } delete tiles[_id]; _burn(_id);
Gas Savings as shown via forge snapshot --diff
:
testFuseWithEmojiSupportToneModifier() (gas: -22 (-0.002%)) testFusingAsApprovedForAllOfTrayOwner() (gas: -20 (-0.004%)) testFusingAsApprovedOfTray() (gas: -20 (-0.004%)) testBurnAsApprovedForAllOfOwner() (gas: -20 (-0.004%)) testSelectTrayIdToFuseSupportSkinTone() (gas: -44 (-0.004%)) testBurnAsOwner() (gas: -20 (-0.004%)) testFusingWithOneTrayAndOneTile() (gas: -20 (-0.004%)) testFusingAsOwnerOfTray() (gas: -20 (-0.004%)) testTokenURIProperlyRendered() (gas: -20 (-0.005%)) testComplexScenario() (gas: -1035 (-0.005%)) testTokenURIOfBurnedToken() (gas: -20 (-0.005%)) testFusingWithPrelaunchTrayAfterPrelaunch() (gas: -25 (-0.006%)) testFuseEmojiDoesNotSupportSkinTone() (gas: -22 (-0.007%)) testFuseCharacterWithSkinToneModifier() (gas: -27 (-0.009%)) testFusingWithMuTrayAndMultiTiles() (gas: -95 (-0.009%)) testFusingWithOneTrayAndMultiTiles() (gas: -55 (-0.010%)) testBurnAsApproved() (gas: -136 (-0.020%)) testFusingDuringPrelaunch() (gas: -169 (-0.029%)) Overall gas change: -1790 (-0.001%)
File: canto-namespace-protocol/src/Namespace.sol 136: if (tileData.fontClass != 0 && _characterList[i].skinToneModifier != 0) { 137: revert CannotFuseCharacterWithSkinTone(); 138: }
diff --git a/src/Namespace.sol b/src/Namespace.sol index 484d049..1a66ca7 100644 --- a/src/Namespace.sol +++ b/src/Namespace.sol @@ -133,8 +133,10 @@ contract Namespace is ERC721, Owned { Tray.TileData memory tileData = tray.getTile(trayID, tileOffset); // Will revert if tileOffset is too high uint8 characterModifier = tileData.characterModifier; - if (tileData.fontClass != 0 && _characterList[i].skinToneModifier != 0) { - revert CannotFuseCharacterWithSkinTone(); + if (tileData.fontClass != 0) { + if (_characterList[i].skinToneModifier !=0) { + revert CannotFuseCharacterWithSkinTone(); + } }
Gas Savings as shown via forge snapshot --diff
:
testComplexScenario() (gas: -204 (-0.001%)) testFusingAsApprovedForAllOfTrayOwner() (gas: -5 (-0.001%)) testFuseWithEmojiSupportToneModifier() (gas: -34 (-0.003%)) testSelectTrayIdToFuseSupportSkinTone() (gas: -34 (-0.003%)) testBurnAsApproved() (gas: -28 (-0.004%)) testFusingAsApprovedOfTray() (gas: -24 (-0.005%)) testFusingWithOneTrayAndMultiTiles() (gas: -27 (-0.005%)) testBurnAsApprovedForAllOfOwner() (gas: -27 (-0.005%)) testBurnAsOwner() (gas: -27 (-0.006%)) testFusingWithOneTrayAndOneTile() (gas: -27 (-0.006%)) testFusingDuringPrelaunch() (gas: -34 (-0.006%)) testFusingAsOwnerOfTray() (gas: -28 (-0.006%)) testTokenURIProperlyRendered() (gas: -27 (-0.006%)) testTokenURIOfBurnedToken() (gas: -27 (-0.007%)) testFusingWithMuTrayAndMultiTiles() (gas: -82 (-0.008%)) testFusingWithPrelaunchTrayAfterPrelaunch() (gas: -34 (-0.008%)) Overall gas change: -669 (-0.000%)
File: canto-namespace-protocol/src/Namespace.sol 157: if ( 158: trayOwner != msg.sender && 159: tray.getApproved(trayID) != msg.sender && 160: !tray.isApprovedForAll(trayOwner, msg.sender) 161: ) revert CallerNotAllowedToFuse();
diff --git a/src/Namespace.sol b/src/Namespace.sol index 484d049..0ffe348 100644 --- a/src/Namespace.sol +++ b/src/Namespace.sol @@ -154,11 +154,13 @@ contract Namespace is ERC721, Owned { uniqueTrays[numUniqueTrays++] = trayID; // Verify address is allowed to fuse address trayOwner = tray.ownerOf(trayID); - if ( - trayOwner != msg.sender && - tray.getApproved(trayID) != msg.sender && - !tray.isApprovedForAll(trayOwner, msg.sender) - ) revert CallerNotAllowedToFuse(); + if (trayOwner != msg.sender) { + if (tray.getApproved(trayID) != msg.sender) { + if (!tray.isApprovedForAll(trayOwner, msg.sender)) { + revert CallerNotAllowedToFuse(); + } + } + } } }
Gas Savings as shown via forge snapshot --diff
:
testTransferNormalTray() (gas: 0 (0.000%)) testBurnAsApproved() (gas: -40 (-0.006%)) testBurnAsApprovedForAllOfOwner() (gas: -34 (-0.007%)) testBurnAsOwner() (gas: -33 (-0.007%)) testTokenURIOfBurnedToken() (gas: -33 (-0.008%)) Overall gas change: -140 (-0.000%)
File: canto-namespace-protocol/src/Namespace.sol 186: if (nftOwner != msg.sender && getApproved[_id] != msg.sender && !isApprovedForAll[nftOwner][msg.sender]) 187: revert CallerNotAllowedToBurn();
diff --git a/src/Namespace.sol b/src/Namespace.sol index 484d049..381e0a9 100644 --- a/src/Namespace.sol +++ b/src/Namespace.sol @@ -183,8 +183,15 @@ contract Namespace is ERC721, Owned { /// @param _id Namespace NFT ID function burn(uint256 _id) external { address nftOwner = ownerOf(_id); - if (nftOwner != msg.sender && getApproved[_id] != msg.sender && !isApprovedForAll[nftOwner][msg.sender]) - revert CallerNotAllowedToBurn(); + if (nftOwner != msg.sender) { + if (getApproved[_id] != msg.sender) { + if (!isApprovedForAll[nftOwner][msg.sender]) { + revert CallerNotAllowedToBurn(); + } + } + } string memory associatedName = tokenToName[_id];
Gas Savings as shown via forge snapshot --diff
:
testShortString(string) (gas: -785 (-0.103%)) testEmojiAtBoundaries2() (gas: -1092 (-0.126%)) testLongString(string) (gas: -1546 (-0.156%)) testEmojiAtBoundaries() (gas: -1449 (-0.157%)) Overall gas change: -4872 (-0.133%)
File: canto-bio-protocol/src/Bio.sol 60: if ((i > 0 && (i + 1) % 40 == 0) || prevByteWasContinuation || i == lengthInBytes - 1) { 61: bytes1 nextCharacter; 62: if (i != lengthInBytes - 1) { 63: nextCharacter = bioTextBytes[i + 1]; 64: } 65: if (nextCharacter & 0xC0 == 0x80) { 66: // Unicode continuation byte, top two bits are 10 67: prevByteWasContinuation = true; 68: } else { 69: // Do not split when the prev. or next character is a zero width joiner. Otherwise, 👨👧👦 could become 👨>👧👦 70: // Furthermore, do not split when next character is skin tone modifier to avoid 🤦♂️\n🏻 71: if ( 72: // Note that we do not need to check i < lengthInBytes - 4, because we assume that it's a valid UTF8 string and these prefixes imply that another byte follows 73: (nextCharacter == 0xE2 && bioTextBytes[i + 2] == 0x80 && bioTextBytes[i + 3] == 0x8D) || 74: (nextCharacter == 0xF0 && 75: bioTextBytes[i + 2] == 0x9F && 76: bioTextBytes[i + 3] == 0x8F && 77: uint8(bioTextBytes[i + 4]) >= 187 && 78: uint8(bioTextBytes[i + 4]) <= 191) || 79: (i >= 2 && 80: bioTextBytes[i - 2] == 0xE2 && 81: bioTextBytes[i - 1] == 0x80 && 82: bioTextBytes[i] == 0x8D) 83: ) { 84: prevByteWasContinuation = true; 85: continue; 86: } 87: assembly { 88: mstore(bytesLines, bytesOffset) 89: } 90: strLines[insertedLines++] = string(bytesLines); 91: bytesLines = new bytes(80); 92: prevByteWasContinuation = false; 93: bytesOffset = 0; 94: } 95: }
diff --git a/src/Bio.sol b/src/Bio.sol index 6d5757e..eb34f79 100644 --- a/src/Bio.sol +++ b/src/Bio.sol @@ -57,40 +57,42 @@ contract Bio is ERC721 { bytes1 character = bioTextBytes[i]; bytesLines[bytesOffset] = character; bytesOffset++; - if ((i > 0 && (i + 1) % 40 == 0) || prevByteWasContinuation || i == lengthInBytes - 1) { - bytes1 nextCharacter; - if (i != lengthInBytes - 1) { - nextCharacter = bioTextBytes[i + 1]; - } - if (nextCharacter & 0xC0 == 0x80) { - // Unicode continuation byte, top two bits are 10 - prevByteWasContinuation = true; - } else { - // Do not split when the prev. or next character is a zero width joiner. Otherwise, 👨👧👦 could become 👨>👧👦 - // Furthermore, do not split when next character is skin tone modifier to avoid 🤦♂️\n🏻 - if ( - // Note that we do not need to check i < lengthInBytes - 4, because we assume that it's a valid UTF8 string and these prefixes imply that another byte follows - (nextCharacter == 0xE2 && bioTextBytes[i + 2] == 0x80 && bioTextBytes[i + 3] == 0x8D) || - (nextCharacter == 0xF0 && - bioTextBytes[i + 2] == 0x9F && - bioTextBytes[i + 3] == 0x8F && - uint8(bioTextBytes[i + 4]) >= 187 && - uint8(bioTextBytes[i + 4]) <= 191) || - (i >= 2 && - bioTextBytes[i - 2] == 0xE2 && - bioTextBytes[i - 1] == 0x80 && - bioTextBytes[i] == 0x8D) - ) { - prevByteWasContinuation = true; - continue; + if (i > 0) { + if ((i + 1) % 40 == 0 || prevByteWasContinuation || i == lengthInBytes - 1) { + bytes1 nextCharacter; + if (i != lengthInBytes - 1) { + nextCharacter = bioTextBytes[i + 1]; } - assembly { - mstore(bytesLines, bytesOffset) + if (nextCharacter & 0xC0 == 0x80) { + // Unicode continuation byte, top two bits are 10 + prevByteWasContinuation = true; + } else { + // Do not split when the prev. or next character is a zero width joiner. Otherwise, 👨👧👦 could become 👨>👧👦 + // Furthermore, do not split when next character is skin tone modifier to avoid 🤦♂️\n🏻 + if ( + // Note that we do not need to check i < lengthInBytes - 4, because we assume that it's a valid UTF8 string and these prefixes imply that another byt e follows + (nextCharacter == 0xE2 && bioTextBytes[i + 2] == 0x80 && bioTextBytes[i + 3] == 0x8D) || + (nextCharacter == 0xF0 && + bioTextBytes[i + 2] == 0x9F && + bioTextBytes[i + 3] == 0x8F && + uint8(bioTextBytes[i + 4]) >= 187 && + uint8(bioTextBytes[i + 4]) <= 191) || + (i >= 2 && + bioTextBytes[i - 2] == 0xE2 && + bioTextBytes[i - 1] == 0x80 && + bioTextBytes[i] == 0x8D) + ) { + prevByteWasContinuation = true; + continue; + } + assembly { + mstore(bytesLines, bytesOffset) + } + strLines[insertedLines++] = string(bytesLines); + bytesLines = new bytes(80); + prevByteWasContinuation = false; + bytesOffset = 0; } - strLines[insertedLines++] = string(bytesLines); - bytesLines = new bytes(80); - prevByteWasContinuation = false; - bytesOffset = 0; } } }
Gas Savings as shown via forge snapshot --diff
:
testBuyingMultipleOnesAfterPrelaunchPhase() (gas: -20 (-0.002%)) testTokenURIForNonPrelaunchTray() (gas: -20 (-0.002%)) testComplexScenario() (gas: -1075 (-0.005%)) testBuyingOneAfterPrelaunchPhase() (gas: -20 (-0.006%)) testTransferNormalTray() (gas: -40 (-0.011%)) testTransferAfterPrelaunch() (gas: -33 (-0.013%)) testFuseWithEmojiSupportToneModifier() (gas: -167 (-0.015%)) testSelectTrayIdToFuseSupportSkinTone() (gas: -167 (-0.015%)) testBurnAsApproved() (gas: -192 (-0.028%)) testFusingDuringPrelaunch() (gas: -167 (-0.029%)) testTokenURIProperlyRendered() (gas: -134 (-0.031%)) testTokenURIOfBurnedToken() (gas: -133 (-0.034%)) testFusingWithOneTrayAndMultiTiles() (gas: -192 (-0.036%)) testFusingAsApprovedForAllOfTrayOwner() (gas: -192 (-0.037%)) testFusingAsApprovedOfTray() (gas: -192 (-0.037%)) testBurnAsApprovedForAllOfOwner() (gas: -192 (-0.038%)) testBurnAsOwner() (gas: -192 (-0.040%)) testFusingWithOneTrayAndOneTile() (gas: -192 (-0.040%)) testFusingAsOwnerOfTray() (gas: -192 (-0.041%)) testFusingWithMuTrayAndMultiTiles() (gas: -560 (-0.052%)) testBurnNonPrelaunchTrayByApprovedForAll() (gas: -192 (-0.061%)) testBurnNonPrelaunchTrayByApproved() (gas: -192 (-0.061%)) testGetTileBurned() (gas: -134 (-0.061%)) testBurnInPrelaunchPhase() (gas: -134 (-0.062%)) testBurnNonPrelaunchTrayByOwner() (gas: -192 (-0.065%)) testBurnPrelaunchTray() (gas: -189 (-0.087%)) Overall gas change: -5105 (-0.002%)
File: canto-namespace-protocol/src/Tray.sol 238: if (numPrelaunchMinted != type(uint256).max) { 239: // We do not allow any transfers of the prelaunch trays after the phase has ended 240: if (startTokenId <= numPrelaunchMinted && to != address(0)) 241: revert PrelaunchTrayCannotBeUsedAfterPrelaunch(startTokenId); 242: }
diff --git a/src/Tray.sol b/src/Tray.sol index 4796cf6..6c554df 100644 --- a/src/Tray.sol +++ b/src/Tray.sol @@ -237,8 +237,11 @@ contract Tray is ERC721A, Owned { uint256 numPrelaunchMinted = prelaunchMinted; if (numPrelaunchMinted != type(uint256).max) { // We do not allow any transfers of the prelaunch trays after the phase has ended - if (startTokenId <= numPrelaunchMinted && to != address(0)) - revert PrelaunchTrayCannotBeUsedAfterPrelaunch(startTokenId); + if (startTokenId <= numPrelaunchMinted) { + if (to != address(0)) { + revert PrelaunchTrayCannotBeUsedAfterPrelaunch(startTokenId); + } + } } }
if
/require
statement or other constraintsGas Savings as shown via forge snapshot --diff
:
testComplexScenario() (gas: -1032 (-0.005%)) testFusingWithMuTrayAndMultiTiles() (gas: -138 (-0.013%)) testFuseWithEmojiSupportToneModifier() (gas: -172 (-0.015%)) testBurnAsApproved() (gas: -138 (-0.020%)) testFusingWithOneTrayAndMultiTiles() (gas: -137 (-0.026%)) testFusingAsApprovedForAllOfTrayOwner() (gas: -137 (-0.027%)) testFusingAsApprovedOfTray() (gas: -138 (-0.027%)) testBurnAsApprovedForAllOfOwner() (gas: -137 (-0.027%)) testBurnAsOwner() (gas: -138 (-0.029%)) testFusingWithOneTrayAndOneTile() (gas: -138 (-0.029%)) testFusingAsOwnerOfTray() (gas: -138 (-0.029%)) testFusingDuringPrelaunch() (gas: -172 (-0.030%)) testSelectTrayIdToFuseSupportSkinTone() (gas: -344 (-0.031%)) testTokenURIProperlyRendered() (gas: -138 (-0.032%)) testTokenURIOfBurnedToken() (gas: -137 (-0.035%)) testFusingWithPrelaunchTrayAfterPrelaunch() (gas: -172 (-0.041%)) testFuseEmojiDoesNotSupportSkinTone() (gas: -172 (-0.053%)) testFuseCharacterWithSkinToneModifier() (gas: -172 (-0.055%)) testFusingWithDuplicateTiles() (gas: -172 (-0.055%)) Overall gas change: -3922 (-0.002%)
File: canto-namespace-protocol/src/Namespace.sol 112: if (numCharacters > 13 || numCharacters == 0) revert InvalidNumberOfCharacters(numCharacters); 113: uint256 fusingCosts = 2**(13 - numCharacters) * 1e18;
diff --git a/src/Namespace.sol b/src/Namespace.sol index 484d049..bec1d13 100644 --- a/src/Namespace.sol +++ b/src/Namespace.sol @@ -110,7 +110,10 @@ contract Namespace is ERC721, Owned { function fuse(CharacterData[] calldata _characterList) external { uint256 numCharacters = _characterList.length; if (numCharacters > 13 || numCharacters == 0) revert InvalidNumberOfCharacters(numCharacters); - uint256 fusingCosts = 2**(13 - numCharacters) * 1e18; + uint256 fusingCosts; + unchecked { + fusingCosts = 2**(13 - numCharacters) * 1e18; + } SafeTransferLib.safeTransferFrom(note, msg.sender, revenueAddress, fusingCosts); uint256 namespaceIDToMint = ++nextNamespaceIDToMint; Tray.TileData[] storage nftToMintCharacters = nftCharacters[namespaceIDToMint];
Gas Savings as shown via forge snapshot --diff
:
testComplexScenario() (gas: -312 (-0.001%)) testFusingWithMuTrayAndMultiTiles() (gas: -42 (-0.004%)) testFuseWithEmojiSupportToneModifier() (gas: -52 (-0.005%)) testBurnAsApproved() (gas: -42 (-0.006%)) testFusingWithOneTrayAndMultiTiles() (gas: -41 (-0.008%)) testFusingAsApprovedForAllOfTrayOwner() (gas: -41 (-0.008%)) testBurnAsApprovedForAllOfOwner() (gas: -41 (-0.008%)) testFusingAsApprovedOfTray() (gas: -42 (-0.008%)) testBurnAsOwner() (gas: -42 (-0.009%)) testFusingWithOneTrayAndOneTile() (gas: -42 (-0.009%)) testFusingAsOwnerOfTray() (gas: -42 (-0.009%)) testFusingDuringPrelaunch() (gas: -52 (-0.009%)) testSelectTrayIdToFuseSupportSkinTone() (gas: -104 (-0.010%)) testTokenURIProperlyRendered() (gas: -42 (-0.010%)) testTokenURIOfBurnedToken() (gas: -41 (-0.010%)) testFusingWithPrelaunchTrayAfterPrelaunch() (gas: -52 (-0.012%)) testFuseEmojiDoesNotSupportSkinTone() (gas: -52 (-0.016%)) testFuseCharacterWithSkinToneModifier() (gas: -52 (-0.017%)) testFusingWithDuplicateTiles() (gas: -52 (-0.017%)) Overall gas change: -1186 (-0.001%)
File: canto-namespace-protocol/src/Namespace.sol 115: uint256 namespaceIDToMint = ++nextNamespaceIDToMint;
diff --git a/src/Namespace.sol b/src/Namespace.sol index 484d049..02d5731 100644 --- a/src/Namespace.sol +++ b/src/Namespace.sol @@ -112,7 +112,10 @@ contract Namespace is ERC721, Owned { if (numCharacters > 13 || numCharacters == 0) revert InvalidNumberOfCharacters(numCharacters); uint256 fusingCosts = 2**(13 - numCharacters) * 1e18; SafeTransferLib.safeTransferFrom(note, msg.sender, revenueAddress, fusingCosts); - uint256 namespaceIDToMint = ++nextNamespaceIDToMint; + uint256 namespaceIDToMint; + unchecked { + namespaceIDToMint = ++nextNamespaceIDToMint; + } Tray.TileData[] storage nftToMintCharacters = nftCharacters[namespaceIDToMint];
Gas Savings as shown via forge snapshot --diff
:
testMintNFTOwnedByCaller() (gas: -25 (-0.009%)) testPFPNoLongerOwnedByOriginalOwner() (gas: -25 (-0.010%)) testNFTTransferredAfterwards() (gas: -25 (-0.010%)) testTokenURINFTOwnedByOwnerOfCIDNFT() (gas: -25 (-0.010%)) testPFPAssociatedWithNoCIDNFT() (gas: -25 (-0.013%)) testCannotMintNFTNotOwnedByCaller() (gas: -25 (-0.028%)) Overall gas change: -150 (-0.011%)
File: canto-pfp-protocol/src/ProfilePicture.sol 80: uint256 tokenId = ++numMinted;
diff --git a/src/ProfilePicture.sol b/src/ProfilePicture.sol index 54d814a..0df694d 100644 --- a/src/ProfilePicture.sol +++ b/src/ProfilePicture.sol @@ -77,7 +77,10 @@ contract ProfilePicture is ERC721 { /// @param _nftContract The nft contract address to reference /// @param _nftID The nft ID to reference function mint(address _nftContract, uint256 _nftID) external { - uint256 tokenId = ++numMinted; + uint256 tokenId; + unchecked { + tokenId = ++numMinted; + } if (ERC721(_nftContract).ownerOf(_nftID) != msg.sender) revert PFPNotOwnedByCaller(msg.sender, _nftContract, _nftID); ProfilePictureData storage pictureData = pfp[tokenId];
https://github.com/code-423n4/2023-03-canto-identity/blob/main/canto-bio-protocol/src/Bio.sol#L124
Gas Savings as shown via forge snapshot --diff
:
testLongString(string) (gas: -46 (-0.005%)) testEmojiAtBoundaries2() (gas: -43 (-0.005%)) testEmojiAtBoundaries() (gas: -46 (-0.005%)) testShortString(string) (gas: -43 (-0.006%)) testMint() (gas: -23 (-0.020%)) testRevertOver200() (gas: -6 (-0.059%)) testRevertLen0() (gas: -6 (-0.061%)) Overall gas change: -213 (-0.006%)
File: canto-bio-protocol/src/Bio.sol 80: uint256 tokenId = ++numMinted;
diff --git a/src/Bio.sol b/src/Bio.sol index 6d5757e..f1e7caa 100644 --- a/src/Bio.sol +++ b/src/Bio.sol @@ -121,9 +121,13 @@ contract Bio is ERC721 { function mint(string calldata _bio) external { // We check the length in bytes, so will be higher for UTF-8 characters. But sufficient for this check if (bytes(_bio).length == 0 || bytes(_bio).length > 200) revert InvalidBioLength(bytes(_bio).length); - uint256 tokenId = ++numMinted; + uint256 tokenId; + unchecked { + tokenId = ++numMinted; + } bio[tokenId] = _bio; _mint(msg.sender, tokenId); emit BioAdded(msg.sender, tokenId, _bio); } }
https://github.com/code-423n4/2023-03-canto-identity/blob/main/canto-bio-protocol/src/Bio.sol#L49
Gas Savings as shown via forge snapshot --diff
:
testShortString(string) (gas: -270 (-0.036%)) testEmojiAtBoundaries2() (gas: -390 (-0.045%)) testLongString(string) (gas: -460 (-0.046%)) testEmojiAtBoundaries() (gas: -565 (-0.061%)) Overall gas change: -1685 (-0.046%)
File: canto-bio-protocol/src/Bio.sol 49: uint lines = (lengthInBytes - 1) / 40 + 1;
diff --git a/src/Bio.sol b/src/Bio.sol index 6d5757e..23f56f1 100644 --- a/src/Bio.sol +++ b/src/Bio.sol @@ -46,7 +46,10 @@ contract Bio is ERC721 { bytes memory bioTextBytes = bytes(bioText); uint lengthInBytes = bioTextBytes.length; // Insert a new line after 40 characters, taking into account unicode character - uint lines = (lengthInBytes - 1) / 40 + 1; + uint lines; + unchecked { + lines = (lengthInBytes - 1) / 40 + 1; + } string[] memory strLines = new string[](lines); bool prevByteWasContinuation; uint256 insertedLines;
Note that this instance can not underflow since _nextTokenId()
is defined to start at 1 and is only incremented
Gas Savings as shown via forge snapshot --diff
:
testComplexScenario() (gas: -28 (-0.000%)) testFusingWithMuTrayAndMultiTiles() (gas: -23 (-0.002%)) testBuyingMultipleOnesAfterPrelaunchPhase() (gas: -28 (-0.003%)) testBurnAsApproved() (gas: -23 (-0.003%)) testTokenURIForNonPrelaunchTray() (gas: -28 (-0.003%)) testFusingWithOneTrayAndMultiTiles() (gas: -22 (-0.004%)) testFusingAsApprovedForAllOfTrayOwner() (gas: -22 (-0.004%)) testBurnAsApprovedForAllOfOwner() (gas: -22 (-0.004%)) testFusingAsApprovedOfTray() (gas: -23 (-0.004%)) testFusingWithOneTrayAndOneTile() (gas: -22 (-0.005%)) testBurnAsOwner() (gas: -23 (-0.005%)) testFusingAsOwnerOfTray() (gas: -23 (-0.005%)) testFusingWithPrelaunchTrayAfterPrelaunch() (gas: -28 (-0.007%)) testBurnNonPrelaunchTrayByApproved() (gas: -22 (-0.007%)) testBurnNonPrelaunchTrayByApprovedForAll() (gas: -23 (-0.007%)) testBurnNonPrelaunchTrayByOwner() (gas: -22 (-0.007%)) testTransferNormalTray() (gas: -28 (-0.008%)) testBuyingOneAfterPrelaunchPhase() (gas: -28 (-0.009%)) testBurnPrelaunchTray() (gas: -22 (-0.010%)) testTransferAfterPrelaunch() (gas: -28 (-0.011%)) testEndPrelaunchPhaseByOwner() (gas: -28 (-0.172%)) Overall gas change: -516 (-0.000%)
File: canto-namespace-protocol/src/Tray.sol 227: prelaunchMinted = _nextTokenId() - 1;
diff --git a/src/Tray.sol b/src/Tray.sol index 4796cf6..02db6da 100644 --- a/src/Tray.sol +++ b/src/Tray.sol @@ -224,7 +224,9 @@ contract Tray is ERC721A, Owned { /// @notice End the prelaunch phase and start the public mint function endPrelaunchPhase() external onlyOwner { if (prelaunchMinted != type(uint256).max) revert PrelaunchAlreadyEnded(); - prelaunchMinted = _nextTokenId() - 1; + unchecked { + prelaunchMinted = _nextTokenId() - 1; + } emit PrelaunchEnded(); }
In the instance below we can avoid re-reading struct values from the return value that was stored into memory by caching the values and using those instead. This will save us from doing offset calculations + MLOAD.
Gas Savings as shown via forge snapshot --diff
:
testFuseCharacterWithSkinToneModifier() (gas: 3 (0.001%)) testFuseWithEmojiSupportToneModifier() (gas: -48 (-0.004%)) testFusingAsApprovedForAllOfTrayOwner() (gas: -38 (-0.007%)) testBurnAsApprovedForAllOfOwner() (gas: -38 (-0.008%)) testFusingAsApprovedOfTray() (gas: -39 (-0.008%)) testFusingWithOneTrayAndOneTile() (gas: -38 (-0.008%)) testBurnAsOwner() (gas: -39 (-0.008%)) testFusingAsOwnerOfTray() (gas: -39 (-0.008%)) testSelectTrayIdToFuseSupportSkinTone() (gas: -96 (-0.009%)) testTokenURIProperlyRendered() (gas: -39 (-0.009%)) testComplexScenario() (gas: -2016 (-0.010%)) testTokenURIOfBurnedToken() (gas: -38 (-0.010%)) testFusingWithPrelaunchTrayAfterPrelaunch() (gas: -48 (-0.011%)) testFuseEmojiDoesNotSupportSkinTone() (gas: -48 (-0.015%)) testFusingWithMuTrayAndMultiTiles() (gas: -192 (-0.018%)) testFusingWithOneTrayAndMultiTiles() (gas: -115 (-0.022%)) testBurnAsApproved() (gas: -269 (-0.040%)) testFusingDuringPrelaunch() (gas: -336 (-0.058%)) Overall gas change: -3473 (-0.002%)
File: canto-namespace-protocol/src/Namespace.sol 133: Tray.TileData memory tileData = tray.getTile(trayID, tileOffset); // Will revert if tileOffset is too high 134: uint8 characterModifier = tileData.characterModifier; 135: 136: if (tileData.fontClass != 0 && _characterList[i].skinToneModifier != 0) { 137: revert CannotFuseCharacterWithSkinTone(); 138: } 139: 140: if (tileData.fontClass == 0) { 141: // Emoji 142: characterModifier = _characterList[i].skinToneModifier; 143: }
diff --git a/src/Namespace.sol b/src/Namespace.sol index 484d049..6c3e961 100644 --- a/src/Namespace.sol +++ b/src/Namespace.sol @@ -132,12 +132,13 @@ contract Namespace is ERC721, Owned { } Tray.TileData memory tileData = tray.getTile(trayID, tileOffset); // Will revert if tileOffset is too high uint8 characterModifier = tileData.characterModifier; + uint8 fontClass = tileData.fontClass; - if (tileData.fontClass != 0 && _characterList[i].skinToneModifier != 0) { + if (fontClass != 0 && _characterList[i].skinToneModifier != 0) { revert CannotFuseCharacterWithSkinTone(); } - if (tileData.fontClass == 0) { + if (fontClass == 0) { // Emoji characterModifier = _characterList[i].skinToneModifier; }
Saves a storage slot for the mapping. Depending on the circumstances and sizes of types, can avoid a Gsset (20000 gas) per mapping combined. Reads and subsequent writes can also be cheaper when a function requires both values and they both fit in the same storage slot. Finally, if both fields are accessed in the same function, can save ~42 gas per access due to not having to recalculate the key’s keccak256 hash (Gkeccak256 - 30 gas) and that calculation’s associated stack operations.
Gas Savings as shown via forge snapshot --diff
:
testFuseEmojiDoesNotSupportSkinTone() (gas: 13 (0.004%)) testFuseCharacterWithSkinToneModifier() (gas: 13 (0.004%)) testComplexScenario() (gas: -1146 (-0.005%)) testFuseWithEmojiSupportToneModifier() (gas: -83 (-0.007%)) testFusingWithDuplicateTiles() (gas: 25 (0.008%)) testSelectTrayIdToFuseSupportSkinTone() (gas: -123 (-0.011%)) testFusingWithMuTrayAndMultiTiles() (gas: -126 (-0.012%)) testFusingAsApprovedForAllOfTrayOwner() (gas: -66 (-0.013%)) testFusingAsApprovedOfTray() (gas: -67 (-0.013%)) testFusingWithOneTrayAndOneTile() (gas: -66 (-0.014%)) testFusingAsOwnerOfTray() (gas: -67 (-0.014%)) testFusingWithOneTrayAndMultiTiles() (gas: -95 (-0.018%)) testFusingWithPrelaunchTrayAfterPrelaunch() (gas: -82 (-0.020%)) testTokenURIProperlyRendered() (gas: -109 (-0.025%)) testBurnAsApprovedForAllOfOwner() (gas: -147 (-0.029%)) testBurnAsOwner() (gas: -147 (-0.031%)) testTokenURIOfBurnedToken() (gas: -129 (-0.033%)) testFusingDuringPrelaunch() (gas: -191 (-0.033%)) testBurnAsApproved() (gas: -234 (-0.035%)) testFusingWithMoreThan13Characters() (gas: 22 (0.141%)) testTokenURIOfNonMintedToken() (gas: 22 (0.175%)) testFusingWith0Characters() (gas: 22 (0.221%)) Overall gas change: -2761 (-0.001%)
File: canto-namespace-protocol/src/Namespace.sol 46: mapping(uint256 => string) public tokenToName; 47: 48: /// @notice Stores the character data of an NFT 49: mapping(uint256 => Tray.TileData[]) private nftCharacters;
diff --git a/src/Namespace.sol b/src/Namespace.sol index 484d049..4d8ff12 100644 --- a/src/Namespace.sol +++ b/src/Namespace.sol @@ -35,6 +35,10 @@ contract Namespace is ERC721, Owned { /// @notice Emoji modifier for the skin tone. Can have values of 0 (yellow) and 1 - 5 (light to dark). Only supported by some emojis uint8 skinToneModifier; } + struct TokenData { + string nftName; + Tray.TileData[] nftCharacters; + } /// @notice Next Namespace ID to mint. We start with minting at ID 1 uint256 public nextNamespaceIDToMint; @@ -42,11 +46,7 @@ contract Namespace is ERC721, Owned { /// @notice Maps names to NFT IDs mapping(string => uint256) public nameToToken; - /// @notice Maps NFT IDs to (ASCII) names - mapping(uint256 => string) public tokenToName; - - /// @notice Stores the character data of an NFT - mapping(uint256 => Tray.TileData[]) private nftCharacters; + mapping(uint256 => TokenData) public tokenData; /*////////////////////////////////////////////////////////////// EVENTS @@ -85,18 +85,23 @@ contract Namespace is ERC721, Owned { } } + function tokenToName(uint256 _id) public view returns (string memory) { + return tokenData[_id].nftName; + } + /// @notice Get the token URI for the specified _id /// @param _id ID to query for function tokenURI(uint256 _id) public view override returns (string memory) { if (_ownerOf[_id] == address(0)) revert TokenNotMinted(_id); + TokenData storage _tokenData = tokenData[_id]; string memory json = Base64.encode( bytes( string( abi.encodePacked( '{"name": "', - tokenToName[_id], + _tokenData.nftName, '", "image": "data:image/svg+xml;base64,', - Base64.encode(bytes(Utils.generateSVG(nftCharacters[_id], false))), + Base64.encode(bytes(Utils.generateSVG(_tokenData.nftCharacters, false))), '"}' ) ) @@ -113,7 +118,8 @@ contract Namespace is ERC721, Owned { uint256 fusingCosts = 2**(13 - numCharacters) * 1e18; SafeTransferLib.safeTransferFrom(note, msg.sender, revenueAddress, fusingCosts); uint256 namespaceIDToMint = ++nextNamespaceIDToMint; - Tray.TileData[] storage nftToMintCharacters = nftCharacters[namespaceIDToMint]; + TokenData storage _tokenData = tokenData[namespaceIDToMint]; + Tray.TileData[] storage nftToMintCharacters = _tokenData.nftCharacters; bytes memory bName = new bytes(numCharacters * 33); // Used to convert into a string. Can be 33 times longer than the string at most (longest zalgo characters is 33 byte s) uint256 numBytes; // Extract unique trays for burning them later on @@ -169,7 +175,7 @@ contract Namespace is ERC721, Owned { uint256 currentRegisteredID = nameToToken[nameToRegister]; if (currentRegisteredID != 0) revert NameAlreadyRegistered(currentRegisteredID); nameToToken[nameToRegister] = namespaceIDToMint; - tokenToName[namespaceIDToMint] = nameToRegister; + _tokenData.nftName = nameToRegister; for (uint256 i; i < numUniqueTrays; ++i) { tray.burn(uniqueTrays[i]); @@ -185,8 +191,9 @@ contract Namespace is ERC721, Owned { address nftOwner = ownerOf(_id); if (nftOwner != msg.sender && getApproved[_id] != msg.sender && !isApprovedForAll[nftOwner][msg.sender]) revert CallerNotAllowedToBurn(); - string memory associatedName = tokenToName[_id]; - delete tokenToName[_id]; + TokenData storage _tokenData = tokenData[_id]; + string memory associatedName = _tokenData.nftName; + _tokenData.nftName = ""; delete nameToToken[associatedName]; _burn(_id); } @@ -207,3 +214,4 @@ contract Namespace is ERC721, Owned { emit RevenueAddressUpdated(currentRevenueAddress, _newRevenueAddress); } }
burn
function in Tray.sol
to avoid multiple external calls in for loopInstead of looping through uniqueTrays
and performing an external call (~100 gas) on each iteration, an additional burn function can be added to Tray.sol
that will loop through uniqueTrays
and burn them. This would save 100 * _characterList.length
(at most 1300) gas for the fuse
function since only one external call is needed.
File: canto-namespace-protocol/src/Namespace.sol 174: for (uint256 i; i < numUniqueTrays; ++i) { 175: tray.burn(uniqueTrays[i]); 176: }
With the addition of a new burn function the above code can be refactored into something like this:
tray.burnTrays(uniqueTrays);
bytes32
to set variable as immutable and save an SLOADThe subprotocolName
variable is only set once in the constructor and therefore can be set to immutable to save ~2100 gas (Gcoldsload
). However, variables of type string
can not be set as immutable. If it is reasonable to enforce subprotocolName
to always be <= 32 bytes, then bytes32
can be used instead of string
which will allow the variable to be set to immutable
.
35: string public subprotocolName; 99: uint256 cidNFTID = cidNFT.getPrimaryCIDNFT(subprotocolName, _pfpID);
#0 - c4-judge
2023-04-11T04:18:18Z
0xleastwood marked the issue as grade-a