Platform: Code4rena
Start Date: 23/11/2022
Pot Size: $24,500 CANTO
Total HM: 5
Participants: 37
Period: 5 days
Judge: berndartmueller
Total Solo HM: 2
Id: 185
League: ETH
Rank: 7/37
Findings: 2
Award: $596.84
š Selected for report: 0
š Solo Findings: 0
š Selected for report: 0xSmartContract
Also found by: Deivitto, Josiah, RaymondFam, aphak5010, cccz, cryptonue, gzeon, joestakey, keccak123, martin, peritoflores, rotcivegaf
3610.8402 CANTO - $583.15
L-N | Issue | Instances |
---|---|---|
[Lā01] | The symbol should be an abbreviation of the name | 2 |
[Lā02] | Use _safeMint() instead of _mint() | 2 |
N-N | Issue | Instances |
---|---|---|
[Nā01] | Event is missing indexed fields | 4 |
[Nā02] | Functions not used internally could be marked external | 4 |
[Nā03] | Typo | 3 |
[Nā04] | Unused error | 1 |
[Nā05] | The name of the contract file should be start with capital letter | 1 |
[Nā06] | Old version of OpenZeppelin on Canto folder, and different between Canto and CIP-001 folders | 1 |
[Nā07] | Move the event emitting to the bottom of the function | 6 |
[Nā08] | The msg.sender could be a EOA | 2 |
symbol
should be an abbreviation of the name
As in the eip-721 it says:
/// @notice An abbreviated name for NFTs in this contract function symbol() external view returns (string _symbol);
This could broke extensions like metamask or web like coinmarketcap, binance
The recommendation is to use from 3 to 5 letters for the symbol
File: CIP-001/src/Turnstile.sol 57: constructor() ERC721("Turnstile", "Turnstile") {}
File: Canto/contracts/turnstile.sol 58: constructor() ERC721("Turnstile", "Turnstile") {}
_safeMint
rather than _mint
OpenZeppelin recommends the usage of _safeMint()
instead of _mint()
, if the _recipient
is a contract, _safeMint()
checks whether they can handle ERC721 tokens: "WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible"
If the _recipient
of the register
it's a contract this asset could be stuck on it, if the contract not be prepare to receive and manipulate the asset
File: CIP-001/src/Turnstile.sol 92: _mint(_recipient, tokenId);
File: Canto/contracts/turnstile.sol 93: _mint(_recipient, tokenId);
Recommend use _safeMint
rather than _mint
, this could bring the possibility of reentrancy attacks. Use reentrancy guard or move the mint to the end of the function
function register(address _recipient, bytes memory _data) public onlyUnregistered returns (uint256 tokenId) { address smartContract = msg.sender; if (_recipient == address(0)) revert InvalidRecipient(); tokenId = _tokenIdTracker.current(); _tokenIdTracker.increment(); emit Register(smartContract, _recipient, tokenId); feeRecipient[smartContract] = NftData({ tokenId: tokenId, registered: true }); _safeMint(_recipient, tokenId, _data); }
indexed
fieldsIndex event fields make the field more quickly accessible to off-chain tools that parse events. However, note that each index field costs extra gas during emission, so it's not necessarily best to index the maximum allowed per event (three fields). Each event should use three indexed
fields if there are three or more fields, and gas usage is not particularly of concern for the events in question. If there are fewer than three fields, all of the fields should be indexed
.
File: Canto/contracts/turnstile.sol 28: event Register(address smartContract, address recipient, uint256 tokenId); 29: event Assign(address smartContract, uint256 tokenId); 30: event Withdraw(uint256 tokenId, address recipient, uint256 feeAmount); 31: event DistributeFees(uint256 tokenId, uint256 feeAmount);
external
File: Canto/contracts/turnstile.sol 87: function register(address _recipient) public onlyUnregistered returns (uint256 tokenId) { 108: function assign(uint256 _tokenId) public onlyUnregistered returns (uint256) { 128: function withdraw(uint256 _tokenId, address payable _recipient, uint256 _amount) 149: function distributeFees(uint256 _tokenId) public onlyOwner payable {
File: Canto/x/csr/keeper/evm_hooks.go /// @audit: `acount` to `account` 75: return sdkerrors.Wrapf(ErrFeeDistribution, "EVMHook::PostTxProcessing failed to distribute fees from fee collector to module acount, %d", err)
File: Canto/x/csr/keeper/evm.go /// @audit: `contructor` to `constructor` 34:// well as an arbitrary number of arguments which will be supplied to the contructor. All contracts deployed
File: CIP-001/src/Turnstile.sol /// @audit: `receipient` to `recipient` 83: /// can register a fee receipient.
File: Canto/contracts/turnstile.sol /// @audit: `receipient` to `recipient` 84: /// can register a fee receipient.
File: Canto/contracts/turnstile.sol 34: error NotSmartContract();
From: Canto/contracts/turnstile.sol To: Canto/contracts/Turnstile.sol
Canto
folder, and different between Canto
and CIP-001
foldersThe version of OpenZeppelin on Canto
it's minimum the 4.4.2, and in the CIP-001
it's 4.7.0
Update the version of Canto
folder
From: Canto/package.json 10: "@openzeppelin/contracts": "^4.4.2"
From: CIP-001/lib/openzeppelin-contracts/package.json 4: "version": "4.7.0",
File: CIP-001/src/Turnstile.sol 95: emit Register(smartContract, _recipient, tokenId); 112: emit Assign(smartContract, _tokenId); 139: emit Withdraw(_tokenId, _recipient, _amount);
File: Canto/contracts/turnstile.sol 96: emit Register(smartContract, _recipient, tokenId); 113: emit Assign(smartContract, _tokenId); 140: emit Withdraw(_tokenId, _recipient, _amount);
msg.sender
could be a EOAThe msg.sender
in the functions register
and assign
could be a EOA and not only smart contract
Consider add a check to ensure what the sender it's a smart contract:
if (smartContract == tx.origin) revert NotSmartContract();
isContract(address account)
, note: if the call it's in the constructor this return false
File: CIP-001/src/Turnstile.sol @@ -37,6 +37,7 @@ contract Turnstile is Ownable, ERC721Enumerable { error InvalidTokenId(); error NothingToWithdraw(); error NothingToDistribute(); + error NotSmartContract(); /// @dev only owner of _tokenId can call this function modifier onlyNftOwner(uint256 _tokenId) { @@ -86,6 +87,7 @@ contract Turnstile is Ownable, ERC721Enumerable { function register(address _recipient) public onlyUnregistered returns (uint256 tokenId) { address smartContract = msg.sender; + if (smartContract == tx.origin) revert NotSmartContract(); if (_recipient == address(0)) revert InvalidRecipient(); tokenId = _tokenIdTracker.current(); @@ -107,6 +109,7 @@ contract Turnstile is Ownable, ERC721Enumerable { function assign(uint256 _tokenId) public onlyUnregistered returns (uint256) { address smartContract = msg.sender; + if (smartContract == tx.origin) revert NotSmartContract(); if (!_exists(_tokenId)) revert InvalidTokenId(); emit Assign(smartContract, _tokenId);
File: Canto/contracts/turnstile.sol @@ -38,6 +38,7 @@ contract Turnstile is Ownable, ERC721Enumerable { error InvalidTokenId(); error NothingToWithdraw(); error NothingToDistribute(); + error NotSmartContract(); /// @dev only owner of _tokenId can call this function modifier onlyNftOwner(uint256 _tokenId) { @@ -87,6 +88,7 @@ contract Turnstile is Ownable, ERC721Enumerable { function register(address _recipient) public onlyUnregistered returns (uint256 tokenId) { address smartContract = msg.sender; + if (smartContract == tx.origin) revert NotSmartContract(); if (_recipient == address(0)) revert InvalidRecipient(); tokenId = _tokenIdTracker.current(); @@ -108,6 +110,7 @@ contract Turnstile is Ownable, ERC721Enumerable { function assign(uint256 _tokenId) public onlyUnregistered returns (uint256) { address smartContract = msg.sender; + if (smartContract == tx.origin) revert NotSmartContract(); if (!_exists(_tokenId)) revert InvalidTokenId(); emit Assign(smartContract, _tokenId);
#0 - c4-judge
2023-01-02T13:02:54Z
berndartmueller marked the issue as grade-b
#1 - c4-judge
2023-01-02T13:08:26Z
berndartmueller marked the issue as grade-a
š Selected for report: Tricko
Also found by: 0xhacksmithh, AkshaySrivastav, Awesome, Beepidibop, Deivitto, DijkstraDev, Dinesh11G, Englave, JC, Rahoz, RaymondFam, ReyAdmirado, SaeedAlipoor01988, Sathish9098, abiih, aphak5010, chaduke, chrisdior4, exolorkistis, gzeon, martin, nicobevi, oyc_109, peritoflores, rotcivegaf
84.7394 CANTO - $13.69
G-N | Issue | Instances |
---|---|---|
[Gā01] | Use assembly to check for address(0) | 1 |
[Gā02] | Use directly msg.sender | 6 |
[Gā03] | Add unchecked {} for subtractions where the operands cannot underflow because of a previous require() or if -statement | 4 |
[Gā04] | Avoid Address.sendValue of OpenZeppelin | 2 |
[Gā05] | Avoid Counters of OpenZeppelin | 2 |
address(0)
File: Canto/contracts/turnstile.sol 90: if (_recipient == address(0)) revert InvalidRecipient();
msg.sender
File: CIP-001/src/Turnstile.sol 50: address smartContract = msg.sender; 87: address smartContract = msg.sender; 108: address smartContract = msg.sender;
File: Canto/contracts/turnstile.sol 51: address smartContract = msg.sender; 88: address smartContract = msg.sender; 109: address smartContract = msg.sender;
unchecked {}
for subtractions where the operands cannot underflow because of a previous require()
or if
-statementrequire(a <= b); x = b - a
=> require(a <= b); unchecked { x = b - a }
File: CIP-001/src/Turnstile.sol /// @audit: If `_amount` is greater than `earnedFees` in the L135 assign `earnedFees` to `_amount` => at most the `earnedFees` and `_amount` are equal 137; balances[_tokenId] = earnedFees - _amount; /// @audit: impossibly overflow 151; balances[_tokenId] += msg.value;
File: Canto/contracts/turnstile.sol /// @audit: If `_amount` is greater than `earnedFees` in the L136 assign `earnedFees` to `_amount` => at most the `earnedFees` and `_amount` are equal 138; balances[_tokenId] = earnedFees - _amount; /// @audit: impossibly overflow 152; balances[_tokenId] += msg.value;
Address.sendValue
of OpenZeppelinUse directly call
to send ether:
(bool success, ) = _recipient.call{value: _amount}(""); if (!success) revert TransferFailed();
This save the gas of require(address(this).balance >= amount, "Address: insufficient balance");
check of the OpenZeppelin library Address
File: CIP-001/src/Turnstile.sol 141: Address.sendValue(_recipient, _amount);
File: Canto/contracts/turnstile.sol 142: Address.sendValue(_recipient, _amount);
If change the tokenId
from uint256
to uint248
, the struct reduce the gas cost from 2 slots to 1 slot
File: CIP-001/src/Turnstile.sol 15: struct NftData { 16: uint256 tokenId; 17: bool registered; 18: }
File: Canto/contracts/turnstile.sol 15: struct NftData { 16: uint256 tokenId; 17: bool registered; 18: }
Recommended Mitigation:
@@ -13,7 +13,7 @@ contract Turnstile is Ownable, ERC721Enumerable { using Counters for Counters.Counter; struct NftData { - uint256 tokenId; + uint248 tokenId; bool registered; } @@ -95,7 +95,7 @@ contract Turnstile is Ownable, ERC721Enumerable { emit Register(smartContract, _recipient, tokenId); feeRecipient[smartContract] = NftData({ - tokenId: tokenId, + tokenId: uint248(tokenId), registered: true }); } @@ -112,7 +112,7 @@ contract Turnstile is Ownable, ERC721Enumerable { emit Assign(smartContract, _tokenId); feeRecipient[smartContract] = NftData({ - tokenId: _tokenId, + tokenId: uint248(_tokenId), registered: true });
#0 - c4-judge
2022-11-29T19:46:50Z
berndartmueller marked the issue as grade-b