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: 88/98
Findings: 1
Award: $12.03
๐ 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
12.034 USDC - $12.03
Number | Issue details | Instances |
---|---|---|
G-1 | Usage of uint s/int s smaller than 32 bytes (256 bits) incurs overhead. | 29 |
G-2 | abi.encode() is less efficient than abi.encodePacked() | 1 |
G-3 | Internal functions only called once can be inlined to save gas. | 1 |
G-4 | Multiple accesses of a mapping/array should use a local variable cache. | 7 |
G-5 | >= costs less gas than > . | 5 |
G-6 | <x> += <y> costs more gas than <x> = <x> + <y> for state variables | 1 |
G-7 | Using fixed bytes is cheaper than using string . | 14 |
Total: 7 issues.
uint
s/int
s smaller than 32 bytes (256 bits) incurs overhead.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.
Use uint256
instead.
File: 2023-03-canto-identity/canto-bio-protocol/src/Bio.sol
77: uint8(bioTextBytes[i + 4]) >= 187 && 78: uint8(bioTextBytes[i + 4]) <= 191) ||
File: 2023-03-canto-identity/canto-namespace-protocol/src/Namespace.sol
34: uint8 tileOffset; 36: uint8 skinToneModifier; 125: uint8 tileOffset = _characterList[i].tileOffset; 134: uint8 characterModifier = tileData.characterModifier;
File: 2023-03-canto-identity/canto-namespace-protocol/src/Tray.sol
59: uint8 fontClass; 61: uint16 characterIndex; 63: uint8 characterModifier; 195: function getTile(uint256 _trayId, uint8 _tileOffset) external view returns (TileData memory tileData) { 250: tileData.characterIndex = uint16(charRandValue % NUM_CHARS_EMOJIS); 252: tileData.characterIndex = uint16(charRandValue % NUM_CHARS_LETTERS); 255: tileData.characterIndex = uint16(charRandValue % NUM_CHARS_LETTERS_NUMBERS); 259: tileData.fontClass = 3 + uint8((res - 80) / 8); 261: tileData.fontClass = 5 + uint8((res - 96) / 4); 263: tileData.fontClass = 7 + uint8((res - 104) / 2); 267: tileData.characterModifier = uint8(zalgoSeed % 256);
File: 2023-03-canto-identity/canto-namespace-protocol/src/Utils.sol
12: error EmojiDoesNotSupportSkinToneModifier(uint16 emojiIndex); 74: uint8 _fontClass, 75: uint16 _characterIndex, 131: uint8 asciiStartingIndex = 97; // Starting index for (lowercase) characters 135: return abi.encodePacked(bytes1(asciiStartingIndex + uint8(_characterIndex))); 138: uint8 asciiStartingIndex = 97; 145: bytes memory character = abi.encodePacked(bytes1(asciiStartingIndex + uint8(_characterIndex))); 215: return _getUtfSequence(startingSequence, uint8(_characterIndex)); 267: function _getUtfSequence(bytes memory _startingSequence, uint8 _characterIndex) 272: uint8 lastByteVal = uint8(_startingSequence[3]); 273: uint8 lastByteSum = lastByteVal + _characterIndex; 278: _startingSequence[2] = bytes1(uint8(_startingSequence[2]) + 1);
abi.encode()
is less efficient than abi.encodePacked()
abi.encode
will apply ABI encoding rules. Therefore all elementary types are padded to 32 bytes and dynamic arrays include their length. Therefore it is possible to also decode this data again (with abi.decode
) when the type are known.
abi.encodePacked
will only use the only use the minimal required memory to encode the data. E.g. an address will only use 20 bytes and for dynamic arrays only the elements will be stored without length. For more info see the Solidity docs for packed mode.
For the input of the keccak
method it is important that you can ensure that the resulting bytes of the encoding are unique. So if you always encode the same types and arrays always have the same length then there is no problem. But if you switch the parameters that you encode or encode multiple dynamic arrays you might have conflicts.
For example:
abi.encodePacked(address(0x0000000000000000000000000000000000000001), uint(0))
encodes to the same as abi.encodePacked(uint(0x0000000000000000000000000000000000000001000000000000000000000000), address(0))
and abi.encodePacked(uint[](1,2), uint[](3))
encodes to the same as abi.encodePacked(uint[](1), uint[](2,3))
Therefore these examples will also generate the same hashes even so they are different inputs.
On the other hand you require less memory and therefore in most cases abi.encodePacked uses less gas than abi.encode.
Use abi.encodePacked()
where possible to save gas
File: 2023-03-canto-identity/canto-namespace-protocol/src/Tray.sol
162: lastHash = keccak256(abi.encode(lastHash));
Not inlining costs 20 to 40 gas because of two extra JUMP
instructions and additional stack operations needed for function calls.
Check this out: link
Note: To sum up, when there is an internal function that is only called once, there is no point for that function to exist.
Remove the function and make it inline if it is a simple function.
File: 2023-03-canto-identity/canto-namespace-protocol/src/Tray.sol
276: function _startTokenId() internal pure override returns (uint256) {
The instances below point to the second+ access of a value inside a mapping/array, within a function. 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 recalculate the key's keccak256 hash (Gkeccak256 - 30 gas) and that calculation's associated stack operations. Caching an array's struct avoids recalculating the array offsets into memory/calldata. Similar findings
Use a local variable cache.
File: 2023-03-canto-identity/canto-namespace-protocol/src/Namespace.sol
124: uint256 trayID = _characterList[i].trayID; 125: uint8 tileOffset = _characterList[i].tileOffset; 128: if (_characterList[j].trayID == trayID) { 130: if (_characterList[j].tileOffset == tileOffset) revert FusingDuplicateCharactersNotAllowed(); 136: if (tileData.fontClass != 0 && _characterList[i].skinToneModifier != 0) { 142: characterModifier = _characterList[i].skinToneModifier;
File: 2023-03-canto-identity/canto-namespace-protocol/src/Utils.sol
232: characterToUnicodeBytes(_tiles[i].fontClass, _tiles[i].characterIndex, _tiles[i].characterModifier)
>=
costs less gas than >
.The compiler uses opcodes GT
and ISZERO
for solidity code that uses >
, but only requires LT
for >=
, which saves 3 gas
Use >=
instead if appropriate.
File: 2023-03-canto-identity/canto-bio-protocol/src/Bio.sol
60: if ((i > 0 && (i + 1) % 40 == 0) || prevByteWasContinuation || i == lengthInBytes - 1) { 123: if (bytes(_bio).length == 0 || bytes(_bio).length > 200) revert InvalidBioLength(bytes(_bio).length);
File: 2023-03-canto-identity/canto-namespace-protocol/src/Namespace.sol
112: if (numCharacters > 13 || numCharacters == 0) revert InvalidNumberOfCharacters(numCharacters);
File: 2023-03-canto-identity/canto-namespace-protocol/src/Tray.sol
155: if (startingTrayId + _amount - 1 > PRE_LAUNCH_MINT_CAP) revert MintExceedsPreLaunchAmount();
File: 2023-03-canto-identity/canto-namespace-protocol/src/Utils.sol
132: if (_characterIndex > 25) {
<x> += <y>
costs more gas than <x> = <x> + <y>
for state variablesUsing the addition/subtraction operator instead of plus-equals/minus-equals saves 113 gas
Use <x> = <x> + <y>
or <x> = <x> - <y>
instead.
File: 2023-03-canto-identity/canto-namespace-protocol/src/Namespace.sol
150: numBytes += numBytesChar;
string
.As a rule of thumb, use bytes for arbitrary-length raw byte data and string
for arbitrary-length string (UTF-8) data. If you can limit the length to a certain number of bytes, always use one of bytes1
to bytes32
because they are much cheaper.
Use fixed bytes instead of string
.
File: 2023-03-canto-identity/canto-bio-protocol/src/Bio.sol
43: function tokenURI(uint256 _id) public view override returns (string memory) { 45: string memory bioText = bio[_id]; 99: string memory text = '<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle">'; 103: string memory json = Base64.encode(
File: 2023-03-canto-identity/canto-namespace-protocol/src/Namespace.sol
90: function tokenURI(uint256 _id) public view override returns (string memory) { 92: string memory json = Base64.encode( 168: string memory nameToRegister = string(bName); 188: string memory associatedName = tokenToName[_id];
File: 2023-03-canto-identity/canto-namespace-protocol/src/Tray.sol
119: function tokenURI(uint256 _id) public view override returns (string memory) { 132: string memory json = Base64.encode(
File: 2023-03-canto-identity/canto-namespace-protocol/src/Utils.sol
222: function generateSVG(Tray.TileData[] memory _tiles, bool _isTray) internal pure returns (string memory) { 223: string memory innerData;
File: 2023-03-canto-identity/canto-pfp-protocol/src/ProfilePicture.sol
57: constructor(address _cidNFT, string memory _subprotocolName) ERC721("Profile Picture", "PFP") { 70: function tokenURI(uint256 _id) public view override returns (string memory) {
#0 - c4-judge
2023-04-11T05:37:45Z
0xleastwood marked the issue as grade-b