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: 45/98
Findings: 2
Award: $34.80
π 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
During the audit, 8 low and 9 non-critical issues were found.
β | Title | Risk Rating | Instance Count |
---|---|---|---|
L-1 | Docs may mislead users | Low | 1 |
L-2 | Return the amount available for minting | Low | 1 |
L-3 | Add comment about _safeMint | Low | 2 |
L-4 | Use the two-step-transfer of ownership | Low | 2 |
L-5 | Critical Address Changes Should Use Two-step Procedure | Low | 4 |
L-6 | Use Checks Effects Interactions pattern | Low | 1 |
L-7 | Malicious token contracts can pass the ownership check | Low | 2 |
L-8 | Might need to change mint() function in ProfilePicture.sol | Low | 1 |
NC-1 | Use gender-neutral pronouns | Non-Critical | 2 |
NC-2 | Validate tray price | Non-Critical | 1 |
NC-3 | Order of Functions | Non-Critical | 4 |
NC-4 | Order of Layout | Non-Critical | 3 |
NC-5 | Missing NatSpec | Non-Critical | 2 |
NC-6 | Commented code | Non-Critical | 1 |
NC-7 | Inconsistency when using uint and uint256 | Non-Critical | 5 |
NC-8 | Maximum line length exceeded | Non-Critical | 9 |
NC-9 | Missing leading underscores | Non-Critical | 13 |
Docs says: "A fusing fee that is proportial to the length of the name is charged". Although it seems obvious that a shorter name is more unique and therefore more expensive. Reading the protocol documentation, the user may think that the longer the name, the higher the price and, therefore, they may choose a shorter name and pay more.
Change to "A fusing fee that is inversely proportional to the length of the name is charged" or "A fusing fee that is proportional to the length of the name is charged. The shorter the name, the higher the fee".
For convenience, the amount available for minting should be returned in the error message.
Change to:
if (startingTrayId + _amount - 1 > PRE_LAUNCH_MINT_CAP) revert MintExceedsPreLaunchAmount(PRE_LAUNCH_MINT_CAP - startingTrayId - 1);
If the comment in the Tray.sol is also valid for ProfilePicture.sol, then duplicate the comment in the ProfilePicture.sol. Otherwise, _safeMint can be used.
_mint(msg.sender, tokenId);
_mint(msg.sender, _amount); // We do not use _safeMint on purpose here to disallow callbacks and save gas
"{Owned} from "solmate/auth/Owned.sol" has one-step transfer of ownership. If the owner accidentally transfers ownership to an incorrect address, protected functions may become permanently inaccessible.
Consider using a two-step-transfer of ownership: the current owner would nominate a new owner, and to become the new owner, the nominated account would have to approve the change, so that the address is proven to be valid.
Lack of two-step procedure for critical operations leaves them error-prone.
Consider adding two step procedure on the critical functions.
For reducing the attack surface for malicious contracts trying to hijack control flow after an external call, it is best practice to use this pattern.
uint256 tokenId = ++numMinted; if (ERC721(_nftContract).ownerOf(_nftID) != msg.sender) revert PFPNotOwnedByCaller(msg.sender, _nftContract, _nftID);
Change to:
if (ERC721(_nftContract).ownerOf(_nftID) != msg.sender) revert PFPNotOwnedByCaller(msg.sender, _nftContract, _nftID); uint256 tokenId = ++numMinted;
Verification that the user owns NFT can be passed using malicious token contracts, this can negatively affect the operation of the protocol.
The user may mint many PFP NFT with the same reference _nftContract
and _nftID
.
If it is not intended that the user can mint multiple PFP NFTs using the same reference NFT, then it is necessary to keep track of which NFTs the user has already used.
Any user can mint a Bio NFT by calling Bio.mint and passing his biography.
A user can therefore precompute which trays he will get.
Change to:
Any user can mint a Bio NFT by calling Bio.mint and passing their biography.
A user can therefore precompute which trays they will get.
Price validation can be added.
Consider setting a minimum and maximum allowable price per tray and check if the function parameter matches it.
According to Style Guide, ordering helps readers identify which functions they can call and to find the constructor and fallback definitions easier.
Functions should be grouped according to their visibility and ordered:
Public functions should be placed after external.
According to Order of Layout, inside each contract, library or interface, use the following order:
Place structs before state variables.
NatSpec is missing for 2 functions in 1 contract.
Add NatSpec for all functions.
Delete commented code.
Some variables is declared as uint
and some as uint256
.
Below 5 instances when uint
is used, while uint256
is used in all other 110 instances.
uint lengthInBytes = bioTextBytes.length;
uint lines = (lengthInBytes - 1) / 40 + 1;
uint bytesOffset;
for (uint i; i < lengthInBytes; ++i) {
for (uint i; i < lines; ++i) {
Stick to one style.
According to Style Guide, maximum suggested line length is 120 characters. Longer lines make the code harder to read.
Make the lines shorter.
Private constants, immutables, and state variables should have a leading underscore.
ICidNFT private immutable cidNFT;
mapping(uint256 => ProfilePictureData) private pfp;
address private revenueAddress;
mapping(uint256 => Tray.TileData[]) private nftCharacters;
uint256 private constant TILES_PER_TRAY = 7;
uint256 private constant SUM_ODDS = 109;
uint256 private constant NUM_CHARS_EMOJIS = 420;
uint256 private constant NUM_CHARS_LETTERS = 26;
uint256 private constant NUM_CHARS_LETTERS_NUMBERS = 36;
uint256 private constant PRE_LAUNCH_MINT_CAP = 1_000;
address private revenueAddress;
mapping(uint256 => TileData[TILES_PER_TRAY]) private tiles;
uint256 private prelaunchMinted = type(uint256).max;
Add leading underscores where needed.
#0 - c4-judge
2023-04-11T05:49:59Z
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
12.034 USDC - $12.03
During the audit, 1 gas issue were found.
Total savings ~560.
β | Title | Instance Count | Saved |
---|---|---|---|
G-1 | Use unchecked blocks for subtractions where underflow is impossible | 16 | 560 |
In Solidity 0.8+, thereβs a default overflow and underflow check on unsigned integers. When an overflow or underflow isnβt possible (after require or if-statement), some gas can be saved by using unchecked blocks.
if ((i > 0 && (i + 1) % 40 == 0) || prevByteWasContinuation || i == lengthInBytes - 1) {
(lengthInBytes - 1) - checked in line 49.if (i != lengthInBytes - 1) {
checked in line 49.bioTextBytes[i - 2] == 0xE2 &&
checked in line 79.bioTextBytes[i - 1] == 0x80 &&
checked in line 79.uint256 fusingCosts = 2**(13 - numCharacters) * 1e18;
checked in line 112.prelaunchMinted = _nextTokenId() - 1;
safe because startTokenId = 1.tileData.fontClass = 3 + uint8((res - 80) / 8);
safe because here res >= 80.tileData.fontClass = 5 + uint8((res - 96) / 4);
safe because here res >= 96.tileData.fontClass = 7 + uint8((res - 104) / 2);
safe because here res >= 104.supportsSkinToneModifier = _characterIndex >= EMOJIS_LE_THREE_BYTES - EMOJIS_MOD_SKIN_TONE_THREE_BYTES;
byteOffset = EMOJIS_BYTE_OFFSET_FOUR_BYTES + (_characterIndex - EMOJIS_LE_THREE_BYTES) * 4;
checked in line 83.supportsSkinToneModifier = _characterIndex >= EMOJIS_LE_FOUR_BYTES - EMOJIS_MOD_SKIN_TONE_FOUR_BYTES;
byteOffset = EMOJIS_BYTE_OFFSET_SIX_BYTES + (_characterIndex - EMOJIS_LE_FOUR_BYTES) * 6;
checked in line 87.byteOffset = EMOJIS_BYTE_OFFSET_SEVEN_BYTES + (_characterIndex - EMOJIS_LE_SIX_BYTES) * 7;
checked in line 91.byteOffset = EMOJIS_BYTE_OFFSET_EIGHT_BYTES + (_characterIndex - EMOJIS_LE_SEVEN_BYTES) * 8;
checked in line 94.byteOffset = EMOJIS_BYTE_OFFSET_FOURTEEN_BYTES + (_characterIndex - EMOJIS_LE_EIGHT_BYTES) * 14;
checked in line 97.This saves ~35.
So, ~35*16 = 560
#0 - c4-judge
2023-04-10T23:55:31Z
0xleastwood marked the issue as grade-b