Platform: Code4rena
Start Date: 31/01/2023
Pot Size: $36,500 CANTO
Total HM: 5
Participants: 38
Period: 3 days
Judge: berndartmueller
Total Solo HM: 2
Id: 212
League: ETH
Rank: 7/38
Findings: 2
Award: $915.39
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: HardlyDifficult
Also found by: 0xAgro, 0xSmartContract, Aymen0909, DevABDee, JC, Matin, Rolezn, SleepingBugs, adriro, brevis, btk, chaduke, d3e4, enckrish, hihen, joestakey, libratus, merlin, nicobevi, rotcivegaf, shark, sorrynotsorry
2112.6793 CANTO - $636.55
Issue | Contexts | |
---|---|---|
LOW‑1 | Add to blacklist function | 8 |
LOW‑2 | Possible rounding issue | 1 |
LOW‑3 | Missing parameter validation in constructor | 6 |
LOW‑4 | Protect your NFT from copying in POW forks | 1 |
LOW‑5 | Solmate's SafeTransferLib doesn't check whether the ERC20 contract exists | 2 |
Total: 18 contexts over 56 issues
Issue | Contexts | |
---|---|---|
NC‑1 | Function writing that does not comply with the Solidity Style Guide | 3 |
NC‑2 | Use delete to Clear Variables | 1 |
NC‑3 | NatSpec return parameters should be included in contracts | 1 |
NC‑4 | Lines are too long | 2 |
NC‑5 | Implementation contract may not be initialized | 3 |
NC‑6 | NatSpec comments should be increased in contracts | 1 |
NC‑7 | Non-usage of specific imports | 2 |
NC‑8 | Use a more recent version of Solidity | 3 |
NC‑9 | Use bytes.concat() | 2 |
NC‑10 | Use standard decimals interpretations using 1eX | 1 |
Total: 19 contexts over 10 issues
blacklist
functionNFT thefts have increased recently, so with the addition of hacked NFTs to the platform, NFTs can be converted into liquidity. To prevent this, I recommend adding the blacklist function.
Marketplaces such as Opensea have a blacklist feature that will not list NFTs that have been reported theft, NFT projects such as Manifold have blacklist functions in their smart contracts.
Here is the project example; Manifold
Manifold Contract https://etherscan.io/address/0xe4e4003afe3765aca8149a82fc064c0b125b9e5a#code
modifier nonBlacklistRequired(address extension) { require(!_blacklistedExtensions.contains(extension), "Extension blacklisted"); _; }
Add to Blacklist function and modifier.
There might be a rounding issue as CID_FEE_BPS
= 1000, however there is no defined value for subprotocolFee
and it could be 0 or uint96.max. But values between 1-9 for subprotocolFee
will result in cidFee
being 0 due to rounding issue.
191: uint256 cidFee = (subprotocolFee * CID_FEE_BPS) / 10_000;
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/CidNFT.sol#L191
constructor
Some parameters of constructors are not checked for invalid values.
36: address _cidNFT
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/AddressRegistry.sol#L36
123: address _cidFeeWallet 124: address _noteContract 125: address _subprotocolRegistry
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/CidNFT.sol#L123-L125
65: address _noteContract 65: address _cidFeeWallet
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/SubprotocolRegistry.sol#L65-L65
Validate the parameters.
Ethereum has performed the long-awaited "merge" that will dramatically reduce the environmental impact of the network
There may be forked versions of Ethereum, which could cause confusion and lead to scams as duplicated NFT assets enter the market.
If the Ethereum Merge, which took place in September 2022, results in the Blockchain splitting into two Blockchains due to the 'THE DAO' attack in 2016, this could result in duplication of immutable tokens (NFTs).
In any case, duplicate NFTs will exist due to the ETH proof-of-work chain and other potential forks, and there’s likely to be some level of confusion around which assets are 'official' or 'authentic.'
Even so, there could be a frenzy for these copies, as NFT owners attempt to flip the proof-of-work versions of their valuable tokens.
As ETHPOW and any other forks spin off of the Ethereum mainnet, they will yield duplicate versions of Ethereum’s NFTs. An NFT is simply a blockchain token, and it can work as a deed of ownership to digital items like artwork and collectibles. A forked Ethereum chain will thus have duplicated deeds that point to the same tokenURI
About Merge Replay Attack: https://twitter.com/elerium115/status/1558471934924431363?s=20&t=RRheaYJwo-GmSnePwofgag
136: function tokenURI(uint256 _id) public view override returns (string memory) {
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/CidNFT.sol#L136
Add the following check:
if(block.chainid != 1) { revert(); }
Solmate's SafeTransferLib, which is often used to interact with non-compliant/unsafe ERC20 tokens, does not check whether the ERC20 contract exists. The following code will not revert in case the token doesn't exist (yet).
This is stated in the Solmate library: https://github.com/transmissions11/solmate/blob/main/src/utils/SafeTransferLib.sol#L9
6: import "solmate/utils/SafeTransferLib.sol";
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/CidNFT.sol#L6
6: import "solmate/utils/SafeTransferLib.sol";
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/SubprotocolRegistry.sol#L6
Add a contract exist control in functions;
pragma solidity >=0.8.0; function isContract(address _addr) private returns (bool isContract) { isContract = _addr.code.length > 0; }
Order of Functions; ordering helps readers identify which functions they can call and to find the constructor and fallback definitions easier. But there are contracts in the project that do not comply with this.
https://docs.soliditylang.org/en/v0.8.17/style-guide.html
Functions should be grouped according to their visibility and ordered:
All in-scope contracts
delete
to Clear Variablesdelete a
assigns the initial value for the type to a
. i.e. for integers it is equivalent to a = 0
, but it can also be used on arrays, where it assigns a dynamic array of length zero or a static array of the same length with all elements reset. For structs, it assigns a struct with all members reset. Similarly, it can also be used to set an address to zero address. It has no effect on whole mappings though (as the keys of mappings may be arbitrary and are generally unknown). However, individual keys and what they map to can be deleted: If a
is a mapping, then delete a[x]
will delete the value stored at x
.
The delete
key better conveys the intention and is also more idiomatic. Consider replacing assignments of zero with delete
statements.
284: activeData.positions[_nftIDToRemove] = 0;
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/CidNFT.sol#L284
It is recommended that Solidity contracts are fully annotated using NatSpec for all public interfaces (everything in the ABI). It is clearly stated in the Solidity official documentation. In complex projects such as Defi, the interpretation of all functions and their arguments and returns is important for code readability and auditability.
https://docs.soliditylang.org/en/v0.8.15/natspec-format.html
All in-scope contracts
Include return parameters in NatSpec comments
Recommendation Code Style: (from Uniswap3)
/// @notice Adds liquidity for the given recipient/tickLower/tickUpper position /// @dev The caller of this method receives a callback in the form of IUniswapV3MintCallback#uniswapV3MintCallback /// in which they must pay any token0 or token1 owed for the liquidity. The amount of token0/token1 due depends /// on tickLower, tickUpper, the amount of liquidity, and the current price. /// @param recipient The address for which the liquidity will be created /// @param tickLower The lower tick of the position in which to add liquidity /// @param tickUpper The upper tick of the position in which to add liquidity /// @param amount The amount of liquidity to mint /// @param data Any data that should be passed through to the callback /// @return amount0 The amount of token0 that was paid to mint the given amount of liquidity. Matches the value in the callback /// @return amount1 The amount of token1 that was paid to mint the given amount of liquidity. Matches the value in the callback function mint( address recipient, int24 tickLower, int24 tickUpper, uint128 amount, bytes calldata data ) external returns (uint256 amount0, uint256 amount1);
i.e.
function onERC721Received( address, /*operator*/ address, /*from*/ uint256, /*id*/ bytes calldata /*data*/ ) external pure returns (bytes4) { return ERC721TokenReceiver.onERC721Received.selector; }
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/CidNFT.sol#L339
Usually lines in source code are limited to 80 characters. Today's screens are much larger so it's reasonable to stretch this in some cases. Since the files will most likely reside in GitHub, and GitHub starts using a scroll bar in all cases when the length is over 164 characters, the lines below should be split when they reach that length Reference: https://docs.soliditylang.org/en/v0.8.10/style-guide.html#maximum-line-length
185: // The CID Protocol safeguards the NFTs of subprotocols. Note that these NFTs are usually pointers to other data / NFTs (e.g., to an image NFT for profile pictures)
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/CidNFT.sol#L185
78: /// @param _fee Fee (in $NOTE) for minting a new token of the subprotocol. Set to 0 if there is no fee. 10% is subtracted from this fee as a CID fee
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/SubprotocolRegistry.sol#L78
OpenZeppelin recommends that the initializer modifier be applied to constructors. Per OZs Post implementation contract should be initialized to avoid potential griefs or exploits. https://forum.openzeppelin.com/t/uupsupgradeable-vulnerability-post-mortem/15680/5
36: constructor(address _cidNFT)
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/AddressRegistry.sol#L36
119: constructor( string memory _name, string memory _symbol, string memory _baseURI, address _cidFeeWallet, address _noteContract, address _subprotocolRegistry ) ERC721(_name, _symbol)
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/CidNFT.sol#L119
65: constructor(address _noteContract, address _cidFeeWallet)
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/SubprotocolRegistry.sol#L65
It is recommended that Solidity contracts are fully annotated using NatSpec for all public interfaces (everything in the ABI). It is clearly stated in the Solidity official documentation. In complex projects such as Defi, the interpretation of all functions and their arguments and returns is important for code readability and auditability. https://docs.soliditylang.org/en/v0.8.15/natspec-format.html
All in-scope contracts
i.e.
Natspec documentation missing info which includes about @param _cidNFTID
/// @notice Register a CID NFT to the address of the caller. NFT has to be owned by the caller /// @dev Will overwrite existing registration if any exists function register(uint256 _cidNFTID) external { if (ERC721(cidNFT).ownerOf(_cidNFTID) != msg.sender) // We only guarantee that a CID NFT is owned by the user at the time of registration // ownerOf reverts if non-existing ID is provided revert NFTNotOwnedByUser(_cidNFTID, msg.sender); cidNFTs[msg.sender] = _cidNFTID; emit CIDNFTAdded(msg.sender, _cidNFTID); }
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/AddressRegistry.sol#L2
function onERC721Received( address, /*operator*/ address, /*from*/ uint256, /*id*/ bytes calldata /*data*/ ) external pure returns (bytes4) { return ERC721TokenReceiver.onERC721Received.selector; }
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/CidNFT.sol#L339
NatSpec comments should be increased in contracts
The current form of relative path import is not recommended for use because it can unpredictably pollute the namespace. Instead, the Solidity docs recommend specifying imported symbols explicitly. https://docs.soliditylang.org/en/v0.8.15/layout-of-source-files.html#importing-other-source-files
A good example:
import {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; import {SafeCastLib} from "solmate/utils/SafeCastLib.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {IProducer} from "src/interfaces/IProducer.sol"; import {GlobalState, UserState} from "src/Common.sol";
7: import "./SubprotocolRegistry.sol";
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/CidNFT.sol#L7
7: import "./CidSubprotocolNFT.sol";
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/SubprotocolRegistry.sol#L7
Use specific imports syntax per solidity docs recommendation.
<a href="https://blog.soliditylang.org/2021/04/21/solidity-0.8.4-release-announcement/">0.8.4</a>: bytes.concat() instead of abi.encodePacked(<bytes>,<bytes>)
<a href="https://blog.soliditylang.org/2022/02/16/solidity-0.8.12-release-announcement/">0.8.12</a>: string.concat() instead of abi.encodePacked(<str>,<str>)
<a href="https://blog.soliditylang.org/2022/03/16/solidity-0.8.13-release-announcement/">0.8.13</a>: Ability to use using for with a list of free functions
<a href="https://blog.soliditylang.org/2022/05/18/solidity-0.8.14-release-announcement/">0.8.14</a>:
ABI Encoder: When ABI-encoding values from calldata that contain nested arrays, correctly validate the nested array length against calldatasize() in all cases. Override Checker: Allow changing data location for parameters only when overriding external functions.
<a href="https://blog.soliditylang.org/2022/06/15/solidity-0.8.15-release-announcement/">0.8.15</a>:
Code Generation: Avoid writing dirty bytes to storage when copying bytes arrays. Yul Optimizer: Keep all memory side-effects of inline assembly blocks.
<a href="https://blog.soliditylang.org/2022/08/08/solidity-0.8.16-release-announcement/">0.8.16</a>:
Code Generation: Fix data corruption that affected ABI-encoding of calldata values represented by tuples: structs at any nesting level; argument lists of external functions, events and errors; return value lists of external functions. The 32 leading bytes of the first dynamically-encoded value in the tuple would get zeroed when the last component contained a statically-encoded array.
<a href="https://blog.soliditylang.org/2022/09/08/solidity-0.8.17-release-announcement/">0.8.17</a>:
Yul Optimizer: Prevent the incorrect removal of storage writes before calls to Yul functions that conditionally terminate the external EVM call.
pragma solidity >=0.8.0;
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/AddressRegistry.sol#L2
pragma solidity >=0.8.0;
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/CidNFT.sol#L2
pragma solidity >=0.8.0;
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/SubprotocolRegistry.sol#L2
Consider updating to a more recent solidity version.
bytes.concat()
Solidity version 0.8.4 introduces bytes.concat()
(vs abi.encodePacked(<bytes>,<bytes>)
)
140: return string(abi.encodePacked(baseURI, _id, ".json")
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/CidNFT.sol#L140
Use bytes.concat()
and upgrade to at least Solidity version 0.8.4 if required.
1eX
Use 1eX instead of 1**X
17: uint256 public constant REGISTER_FEE = 100 * 10**18;
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/SubprotocolRegistry.sol#L17
Replace 10**X with 1eX
#0 - c4-judge
2023-02-18T13:13:56Z
berndartmueller marked the issue as grade-a
🌟 Selected for report: HardlyDifficult
Also found by: 0xAgro, 0xSmartContract, 0xackermann, Aymen0909, Dravee, Matin, MiniGlome, NoamYakov, ReyAdmirado, Rolezn, Ruhum, c3phas, descharre, joestakey
925.4429 CANTO - $278.84
Issue | Contexts | Estimated Gas Saved | |
---|---|---|---|
GAS‑1 | State variables only set in the constructor should be declared immutable | 1 | 2100 |
GAS‑2 | Setting the constructor to payable | 3 | 39 |
GAS‑3 | Do not calculate constants | 1 | - |
GAS‑4 | Using delete statement can save gas | 1 | - |
GAS‑5 | Use hardcoded address instead address(this) | 5 | - |
GAS‑6 | Optimize names to save gas | 3 | 66 |
GAS‑7 | Public Functions To External | 2 | - |
GAS‑8 | Usage of uints /ints smaller than 32 bytes (256 bits) incurs overhead | 2 | - |
GAS‑9 | ++i /i++ Should Be unchecked{++i} /unchecked{i++} When It Is Not Possible For Them To Overflow, As Is The Case When Used In For- And While-loops | 1 | 35 |
GAS‑10 | Use solidity version 0.8.17 to gain some gas boost | 3 | 264 |
GAS‑11 | Using storage instead of memory saves gas | 1 | 350 |
GAS‑12 | Using 10**X for constants isn't gas efficient | 1 | - |
Total: 24 contexts over 11 issues
constructor
should be declared immutableAvoids a Gsset (20000 gas) in the constructor, and replaces each Gwarmacces (100 gas) with a PUSH32 (3 gas).
127: baseURI = _baseURI
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/CidNFT.sol#L127
constructor
to payable
Saves ~13 gas per instance
36: constructor(address _cidNFT)
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/AddressRegistry.sol#L36
119: constructor( string memory _name, string memory _symbol, string memory _baseURI, address _cidFeeWallet, address _noteContract, address _subprotocolRegistry ) ERC721(_name, _symbol)
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/CidNFT.sol#L119
65: constructor(address _noteContract, address _cidFeeWallet)
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/SubprotocolRegistry.sol#L65
Due to how constant variables are implemented (replacements at compile-time), an expression assigned to a constant variable is recomputed each time that the variable is used, which wastes some gas.
17: uint256 public constant REGISTER_FEE = 100 * 10**18;
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/SubprotocolRegistry.sol#L17
delete
statement can save gas284: activeData.positions[_nftIDToRemove] = 0;
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/CidNFT.sol#L284
address(this)
Instead of using address(this)
, it is more gas-efficient to pre-calculate and use the hardcoded address
. Foundry's script.sol and solmate's LibRlp.sol
contracts can help achieve this.
References: https://book.getfoundry.sh/reference/forge-std/compute-create-address
https://twitter.com/transmissions11/status/1518507047943245824
154: ) = address(this).delegatecall(abi.encodePacked(addSelector, _addList[i]));
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/CidNFT.sol#L154
187: nftToAdd.safeTransferFrom(msg.sender, address(this), _nftIDToAdd);
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/CidNFT.sol#L187
264: nftToRemove.safeTransferFrom(address(this), msg.sender, currNFTID); 264: nftToRemove.safeTransferFrom(address(this), msg.sender, currNFTID); 285: nftToRemove.safeTransferFrom(address(this), msg.sender, _nftIDToRemove);
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/CidNFT.sol#L264
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/CidNFT.sol#L264
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/CidNFT.sol#L285
Use hardcoded address
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.
See more <a href="https://medium.com/joyso/solidity-how-does-function-name-affect-gas-consumption-in-smart-contract-47d270d8ac92">here</a>
All in-scope contracts
Find a lower method ID name for the most called functions for example Call() vs. Call1() is cheaper by 22 gas For example, the function IDs in the Gauge.sol contract will be the most used; A lower method ID may be given.
The following functions could be set external to save gas and improve code quality. External call cost is less expensive than of public functions.
function tokenURI(uint256 _id) public view override returns (string memory) {
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/CidNFT.sol#L136
function remove( uint256 _cidNFTID, string calldata _subprotocolName, uint256 _key, uint256 _nftIDToRemove, AssociationType _type ) public {
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/CidNFT.sol#L237
uints
/ints
smaller than 32 bytes (256 bits) incurs overheadWhen 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
Each operation involving a uint8
costs an extra 22-28 gas (depending on whether the other operand is also a variable of type uint8
) as compared to ones involving uint256
, due to the compiler having to clear the higher bits of the memory word before operating on the uint8
, as well as the associated stack operations of doing so. Use a larger size then downcast where needed
189: uint96 subprotocolFee = subprotocolData.fee;
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/CidNFT.sol#L189
32: uint96 fee;
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/SubprotocolRegistry.sol#L32
++i
/i++
Should Be unchecked{++i}
/unchecked{i++}
When It Is Not Possible For Them To Overflow, As Is The Case When Used In For- And While-loopsThe unchecked keyword is new in solidity version 0.8.0, so this only applies to that version or higher, which these instances are. This saves 30-40 gas PER LOOP
150: for (uint256 i = 0; i < _addList.length; ++i) {
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/CidNFT.sol#L150
Upgrade to the latest solidity version 0.8.17 to get additional gas savings.
pragma solidity >=0.8.0;
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/AddressRegistry.sol#L2
pragma solidity >=0.8.0;
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/CidNFT.sol#L2
pragma solidity >=0.8.0;
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/SubprotocolRegistry.sol#L2
storage
instead of memory
saves gasWhen 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
89: SubprotocolData memory subprotocolData = subprotocols[_name];
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/SubprotocolRegistry.sol#L89
10**X
for constants isn't gas efficientIn Solidity, a constant expression in a variable will compute the expression everytime the variable is called. It's not the result of the expression that is stored, but the expression itself.
As Solidity supports the scientific notation, constants of form 10**X can be rewritten as 1eX to save the gas cost from the calculation with the exponentiation operator **.
17: uint256 public constant REGISTER_FEE = 100 * 10**18;
https://github.com/code-423n4/2023-01-canto-identity/tree/main/src/SubprotocolRegistry.sol#L17
Replace 10**X with 1eX
#0 - c4-judge
2023-02-18T12:38:00Z
berndartmueller marked the issue as grade-a