It is a good practice to give time for users to react and adjust to critical changes. A timelock provides more guarantees and reduces the level of trust required, thus decreasing risk for users. It also indicates that the project is legitimate (less risk of a malicious owner making a sandwich attack on a user).
Consider adding a timelock to:
core/InfinityExchange.sol:1266: function setProtocolFee(uint16 _protocolFeeBps) external onlyOwner {
4. abi.encodePacked() should not be used with dynamic types when passing the result to a hash function such as keccak256()
Use abi.encode() instead which will pad items to 32 bytes, which will prevent hash collisions (e.g. abi.encodePacked(0x123,0x456) => 0x123456 => abi.encodePacked(0x1,0x23456), but abi.encode(0x123,0x456) => 0x0...1230...456). If there is only one argument to abi.encodePacked() it can often be cast to bytes() or bytes32() instead.
core/InfinityExchange.sol:380: require(minNonce > userMinOrderNonce[msg.sender], 'nonce too low');
core/InfinityExchange.sol:394: require(orderNonces[i] >= userMinOrderNonce[msg.sender], 'nonce too low');
core/InfinityExchange.sol:587: require(verifyMatchOneToOneOrders(sellOrderHash, buyOrderHash, sell, buy), 'order not verified');
core/InfinityExchange.sol:621: require(verifyMatchOneToManyOrders(buyOrderHash, false, sell, buy), 'order not verified');
core/InfinityExchange.sol:649: require(verifyMatchOneToManyOrders(sellOrderHash, true, sell, buy), 'order not verified');
core/InfinityExchange.sol:684: require(verifyMatchOrders(sellOrderHash, buyOrderHash, sell, buy), 'order not verified');
core/InfinityExchange.sol:949: require(makerOrderValid && executionValid, 'order not verified');
staking/InfinityStaker.sol:68: require(amount != 0, 'stake amount cant be 0');
staking/InfinityStaker.sol:117: require(amount != 0, 'stake amount cant be 0');
7. Use a 2-step ownership transfer pattern
Contracts inheriting from OpenZeppelin's libraries have the default transferOwnership() function (a one-step process). It's possible that the onlyOwner role mistakenly transfers ownership to a wrong address, resulting in a loss of the onlyOwner role.
Consider overriding the default transferOwnership() function to first nominate an address as the pendingOwner and implementing an acceptOwnership() function which is called by the pendingOwner to confirm the transfer.
core/InfinityExchange.sol:5:import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol';
core/InfinityExchange.sol:50:contract InfinityExchange is ReentrancyGuard, Ownable {
Non-Critical Issues
1. Typos
adress
core/InfinityExchange.sol:58: /// @dev This is the adress that is used to send auto sniped orders for execution on chain
Storate
core/InfinityExchange.sol:77: /// @dev Storate variable that keeps track of valid currencies (tokens)
ordes
libs/OrderTypes.sol:39: ///@dev additional parameters like traits for trait orders, private sale buyer for OTC ordes etc
updateWethTranferGas vs updateWethTransferGas
core/InfinityExchange.sol:1260: function updateWethTranferGas(uint32 _wethTransferGasUnits) external onlyOwner {
mesage
libs/SignatureChecker.sol:14: * @param hashed the hash containing the signed mesage
paramer
libs/SignatureChecker.sol:48: * @param domainSeparator paramer to prevent signature being executed in other chains and environments
2. Use scientific notation for readability reasons
100000 should be changed to 1e51000000 should be changed to 1e620000 should be changed to 2e550000 should be changed to 5e5
core/InfinityExchange.sol:61: uint32 public WETH_TRANSFER_GAS_UNITS = 50000;
core/InfinityExchange.sol:201: // 20000 for the SSTORE op that updates maker nonce status from zero to a non zero status
core/InfinityExchange.sol:202: uint256 startGasPerOrder = gasleft() + ((startGas + 20000 - gasleft()) / ordersLength);
core/InfinityExchange.sol:381: require(minNonce < userMinOrderNonce[msg.sender] + 1000000, 'too many');
core/InfinityExchange.sol:725: uint256 protocolFee = (protocolFeeBps * execPrice) / 10000;
core/InfinityExchange.sol:775: uint256 protocolFee = (protocolFeeBps * execPrice) / 10000;
core/InfinityExchange.sol:819: uint256 protocolFee = (protocolFeeBps * execPrice) / 10000;
core/InfinityExchange.sol:873: uint256 protocolFee = (protocolFeeBps * execPrice) / 10000;
core/InfinityExchange.sol:1135: uint256 protocolFee = (PROTOCOL_FEE_BPS * amount) / 10000;
staking/InfinityStaker.sol:35: uint16 public GOLD_STAKE_THRESHOLD = 10000;
staking/InfinityStaker.sol:36: uint16 public PLATINUM_STAKE_THRESHOLD = 20000;
3. Use string.concat() or bytes.concat()
Solidity version 0.8.4 introduces bytes.concat() (vs abi.encodePacked(<bytes>,<bytes>))
Solidity version 0.8.12 introduces string.concat() (vs abi.encodePacked(<str>,<str>))
4. Adding a return statement when the function defines a named return variable, is redundant
While not consuming more gas with the Optimizer enabled: using both named returns and a return statement isn't necessary. Removing one of those can improve code clarity.
Affected code:
contracts/token/InfinityToken.sol:
113: function getAdmin() public view returns (address admin) {
114: return address(uint160(TimelockConfig.getConfig(TimelockConfig.ADMIN).value));
117: function getTimelock() public view returns (uint256 timelock) {
118: return TimelockConfig.getConfig(TimelockConfig.TIMELOCK).value;
121: function getInflation() public view returns (uint256 inflation) {
122: return TimelockConfig.getConfig(EPOCH_INFLATION).value;
125: function getCliff() public view returns (uint256 cliff) {
126: return TimelockConfig.getConfig(EPOCH_CLIFF).value;
129: function getMaxEpochs() public view returns (uint256 totalEpochs) {
130: return TimelockConfig.getConfig(MAX_EPOCHS).value;
133: function getEpochDuration() public view returns (uint256 epochDuration) {
134: return TimelockConfig.getConfig(EPOCH_DURATION).value;
contracts/token/TimelockConfig.sol:
76: function calculateConfigId(string memory name) external pure returns (bytes32 configId) {
77: return keccak256(abi.encodePacked(name));
80: function isConfig(bytes32 configId) external view returns (bool status) {
81: return _configSet.contains(configId);
84: function getConfigCount() external view returns (uint256 count) {
85: return _configSet.length();
88: function getConfigByIndex(uint256 index) external view returns (Config memory config) {
90: return Config(configId, _config[configId]);
93: function getConfig(bytes32 configId) public view returns (Config memory config) {
95: return Config(configId, _config[configId]);
98: function isPending(bytes32 configId) public view returns (bool status) {
99: return _pendingSet.contains(configId);
102: function getPendingCount() external view returns (uint256 count) {
103: return _pendingSet.length();
106: function getPendingByIndex(uint256 index) external view returns (PendingChange memory pendingRequest) {
108: return PendingChange(configId, _pending[configId].value, _pending[configId].timestamp);
111: function getPending(bytes32 configId) external view returns (PendingChange memory pendingRequest) {
113: return PendingChange(configId, _pending[configId].value, _pending[configId].timestamp);
Table of Contents
abi.encodePacked()
should not be used with dynamic types when passing the result to a hash function such askeccak256()
constant
instead of duplicating the same stringstring.concat()
orbytes.concat()
return
statement when the function defines a named return variable, is redundantLow Risk Issues
1. Missing address(0) checks
Consider adding an
address(0)
check for immutable variables:2. Prevent accidentally burning tokens
Transferring tokens to the zero address is usually prohibited to accidentally avoid "burning" tokens by sending them to an unrecoverable zero address.
Consider adding a check to prevent accidentally burning tokens here:
3. Add a timelock to critical functions
It is a good practice to give time for users to react and adjust to critical changes. A timelock provides more guarantees and reduces the level of trust required, thus decreasing risk for users. It also indicates that the project is legitimate (less risk of a malicious owner making a sandwich attack on a user).
Consider adding a timelock to:
4.
abi.encodePacked()
should not be used with dynamic types when passing the result to a hash function such askeccak256()
Similar issue in the past: here
Use
abi.encode()
instead which will pad items to 32 bytes, which will prevent hash collisions (e.g.abi.encodePacked(0x123,0x456)
=>0x123456
=>abi.encodePacked(0x1,0x23456)
, butabi.encode(0x123,0x456)
=>0x0...1230...456
). If there is only one argument toabi.encodePacked()
it can often be cast tobytes()
orbytes32()
instead.5. Events not indexed
6. Use a
constant
instead of duplicating the same string7. Use a 2-step ownership transfer pattern
Contracts inheriting from OpenZeppelin's libraries have the default
transferOwnership()
function (a one-step process). It's possible that theonlyOwner
role mistakenly transfers ownership to a wrong address, resulting in a loss of theonlyOwner
role. Consider overriding the defaulttransferOwnership()
function to first nominate an address as thependingOwner
and implementing anacceptOwnership()
function which is called by thependingOwner
to confirm the transfer.Non-Critical Issues
1. Typos
2. Use scientific notation for readability reasons
100000
should be changed to1e5
1000000
should be changed to1e6
20000
should be changed to2e5
50000
should be changed to5e5
3. Use
string.concat()
orbytes.concat()
Solidity version 0.8.4 introduces
bytes.concat()
(vsabi.encodePacked(<bytes>,<bytes>)
) Solidity version 0.8.12 introducesstring.concat()
(vsabi.encodePacked(<str>,<str>)
)Here, the version is
0.8.14
:4. Adding a
return
statement when the function defines a named return variable, is redundantWhile not consuming more gas with the Optimizer enabled: using both named returns and a return statement isn't necessary. Removing one of those can improve code clarity.
Affected code: