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: 30/98
Findings: 2
Award: $100.36
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Sathish9098
Also found by: 0xAgro, 0xSmartContract, 0xdaydream, 0xnev, Awesome, Aymen0909, BRONZEDISC, Bauchibred, Deathstore, Diana, IceBear, Jerry0x, Kresh, Matin, Rolezn, Stryder, T1MOH, Udsen, adriro, alejandrocovrr, atharvasama, codeslide, cryptonue, descharre, igingu, jack, joestakey, libratus, lukris02, luxartvinsec, nadin, nasri136, reassor, scokaf, shark, slvDev, tnevler
22.7749 USDC - $22.77
_safeMint
over _mint
Bio.sol, Namespace.sol, Tray.sol and ProfilePicture.sol use ERC721's _mint
method, which is missing the check if the account to mint the NFT to is a smart contract that can handle ERC721 tokens. The _safeMint
method does exactly this, so prefer using it over _mint
but always add a nonReentrant
modifier, since calls to _safeMint
can reenter.
File: canto-bio-protocol/src/Bio.sol 126: _mint(msg.sender, tokenId);
File: canto-namespace-protocol/src/Namespace.sol 177: _mint(msg.sender, namespaceIDToMint);
File: canto-namespace-protocol/src/Tray.sol 167: _mint(msg.sender, _amount); // We do not use _safeMint on purpose here to disallow callbacks and save gas
File: canto-pfp-protocol/src/ProfilePicture.sol 126: _mint(msg.sender, tokenId);
STATE
comment in ProfilePicture.sol
is duplicated and can be removed for consistency and clarity.
/*////////////////////////////////////////////////////////////// STATE //////////////////////////////////////////////////////////////*/ /// @notice Reference to the CID NFT ICidNFT private immutable cidNFT; - - /*////////////////////////////////////////////////////////////// - STATE - //////////////////////////////////////////////////////////////*/ /// @notice Data that is stored per PFP struct ProfilePictureData { /// @notice Reference to the NFT contract address nftContract; /// @notice Referenced nft ID uint256 nftID; }
NatSpec comments provide rich code documentation. The following functions are some examples that miss the @return
comment. Please consider completing the NatSpec comments for functions like these.
File: canto-pfp-protocol/src/ProfilePicture.sol 70: function tokenURI(uint256 _id) public view override returns (string memory) {
File: canto-bio-protocol/src/Bio.sol 43: function tokenURI(uint256 _id) public view override returns (string memory) {
File: canto-namespace-protocol/src/Namespace.sol 90: function tokenURI(uint256 _id) public view override returns (string memory) {
File: canto-namespace-protocol/src/Tray.sol 119: function tokenURI(uint256 _id) public view override returns (string memory) { 195: function getTile(uint256 _trayId, uint8 _tileOffset) external view returns (TileData memory tileData) { 203: function getTiles(uint256 _trayId) external view returns (TileData[TILES_PER_TRAY] memory tileData) { 276: function _startTokenId() internal pure override returns (uint256) {
File: canto-namespace-protocol/src/Utils.sol 73: function characterToUnicodeBytes( 222: function generateSVG(Tray.TileData[] memory _tiles, bool _isTray) internal pure returns (string memory) { 267: function _getUtfSequence(bytes memory _startingSequence, uint8 _characterIndex)
NatSpec comments provide rich code documentation. The following functions are some examples that miss NatSpec comments. Please consider adding NatSpec comments for functions like these.
File: canto-namespace-protocol/src/Tray.sol 231: function _beforeTokenTransfers( 245: function _drawing(uint256 _seed) private pure returns (TileData memory tileData) {
#0 - c4-judge
2023-04-11T05:49:19Z
0xleastwood marked the issue as grade-b
🌟 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
Number | Optimization Details | Issues |
---|---|---|
[G-01] | x += y (x -= y) costs more gas than x = x + y (x = x - y) for state variables | 1 |
[G-02] | Setting the constructor to payable | 4 |
[G-03] | Functions guaranteed to revert when called by normal users can be marked payable | 5 |
[G-04] | Optimize names to save gas | All contracts |
[G-05] | Use double if statements instead of && | 4 |
[G-06] | Using private rather than public for constants saves gas | 3 |
[G-07] | Use a more recent version of Solidity | All contracts |
x += y (x -= y)
costs more gas than x = x + y (x = x - y)
for state variablesUsing the addition operator instead of plus-equals saves 113 gas.
1 issue - 1 file:
- numBytes += numBytesChar; + numBytes = numBytes + numBytesChar;
payable
You can cut out 10 opcodes in the creation-time EVM bytecode if you declare a constructor payable. Making the constructor payable eliminates the need for an initial check of msg.value == 0
and saves 15 gas on deployment with no security risks.
4 issues - 4 files:
- constructor(address _cidNFT, string memory _subprotocolName) ERC721("Profile Picture", "PFP") { + constructor(address _cidNFT, string memory _subprotocolName) payable ERC721("Profile Picture", "PFP") {
constructor( bytes32 _initHash, uint256 _trayPrice, address _revenueAddress, address _note, address _namespaceNFT - ) ERC721A("Namespace Tray", "NSTRAY") Owned(msg.sender) { + ) payable ERC721A("Namespace Tray", "NSTRAY") Owned(msg.sender) {
constructor( address _tray, address _note, address _revenueAddress - ) ERC721("Namespace", "NS") Owned(msg.sender) { + ) payable ERC721("Namespace", "NS") Owned(msg.sender) {
- constructor() ERC721("Biography", "Bio") { + constructor() payable ERC721("Biography", "Bio") {
payable
If a function modifier such as onlyOwner is used, the function will revert if a normal user tries to pay the function. Marking the function as payable will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided. The extra opcodes avoided costs an average of about 21 gas per call to the function, in addition to the extra deployment cost.
5 issues - 2 files:
- function changeNoteAddress(address _newNoteAddress) external onlyOwner { + function changeNoteAddress(address _newNoteAddress) external payable onlyOwner {
- function changeRevenueAddress(address _newRevenueAddress) external onlyOwner { + function changeRevenueAddress(address _newRevenueAddress) external payable onlyOwner {
- function changeNoteAddress(address _newNoteAddress) external onlyOwner { + function changeNoteAddress(address _newNoteAddress) external payable onlyOwner {
- function changeRevenueAddress(address _newRevenueAddress) external onlyOwner { + function changeRevenueAddress(address _newRevenueAddress) external payable onlyOwner {
- function endPrelaunchPhase() external onlyOwner { + function endPrelaunchPhase() external payable onlyOwner {
Contracts most called functions could simply save gas by function ordering via Method ID. Calling a function at runtime will be cheaper if the function is positioned earlier in the order (has a relatively lower Method ID) because 22 gas are added to the cost of a function for every position that came before it. The caller can save on gas if you prioritize most called functions.
For example, Tray.sol function names can be named and sorted according to METHOD ID:
Sighash | Function Signature ======================== c87b56dd => tokenURI(uint256) d96a094a => buy(uint256) 42966c68 => burn(uint256) 35e37c72 => getTile(uint256,uint8) 399d0f4c => getTiles(uint256) 21496de6 => changeNoteAddress(address) 92f8b9a3 => changeRevenueAddress(address) ac4081d4 => endPrelaunchPhase() ef435773 => _beforeTokenTransfers(address,address,uint256,uint256) a66a9122 => _drawing(uint256) 98995f77 => _startTokenId()
Using nested if is cheaper than using && multiple check combinations. There are more advantages, such as easier to read code and better coverage reports.
4 issues - 2 files:
- if (tileData.fontClass != 0 && _characterList[i].skinToneModifier != 0) { - revert CannotFuseCharacterWithSkinTone(); - } + if (tileData.fontClass != 0) { + if (_characterList[i].skinToneModifier != 0) { + revert CannotFuseCharacterWithSkinTone(); + } + }
- 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(); + } + } + }
- if (numPrelaunchMinted != type(uint256).max && _id <= numPrelaunchMinted) - revert PrelaunchTrayCannotBeUsedAfterPrelaunch(_id); + if (numPrelaunchMinted != type(uint256).max) { + if (_id <= numPrelaunchMinted) { + revert PrelaunchTrayCannotBeUsedAfterPrelaunch(_id); + } + }
- if (startTokenId <= numPrelaunchMinted && to != address(0)) - revert PrelaunchTrayCannotBeUsedAfterPrelaunch(startTokenId); + if (startTokenId <= numPrelaunchMinted) { + if (to != address(0)) { + revert PrelaunchTrayCannotBeUsedAfterPrelaunch(startTokenId); + } + }
private
rather than public
for constants saves gasIf needed, the values can be read from the verified contract source code, or if there are multiple values there can be a single getter function that returns a tuple of the values of all currently-public constants. Saves 3406-3606 gas in deployment gas due to the compiler not having to create non-payable getter functions for deployment calldata, not having to store the bytes of the value outside of where it's used, and not adding another entry to the method ID table.
3 issues - 2 files:
- Tray public immutable tray; + Tray private immutable tray;
- uint256 public immutable trayPrice; + uint256 private immutable trayPrice;
- address public immutable namespaceNFT; + address private immutable namespaceNFT;
Solidity 0.8.10 has a useful change that reduced gas costs of external calls which expect a return value.
In 0.8.15 the conditions necessary for inlining are relaxed. Benchmarks show that the change significantly decreases the bytecode size (which impacts the deployment cost) while the effect on the runtime gas usage is smaller.
In 0.8.17 prevent the incorrect removal of storage writes before calls to Yul functions that conditionally terminate the external EVM call; Simplify the starting offset of zero-length operations to zero. More efficient overflow checks for multiplication.
I recommend that you upgrade the versions of all contracts in scope to the latest version of robustness, 0.8.19
.
#0 - c4-judge
2023-04-10T23:55:45Z
0xleastwood marked the issue as grade-a