Platform: Code4rena
Start Date: 14/06/2022
Pot Size: $50,000 USDC
Total HM: 19
Participants: 99
Period: 5 days
Judge: HardlyDifficult
Total Solo HM: 4
Id: 136
League: ETH
Rank: 40/99
Findings: 3
Award: $116.95
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: WatchPug
Also found by: BowTiedWardens, GreyArt, Ruhum, berndartmueller, cccz, csanuragjain, defsec, joestakey, m9800, peritoflores, reassor, shenwilly, throttle, zer0dot
21.1924 USDC - $21.19
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L1267
The ** feeBps** does not have any upper or lower bounds. Values that are too large will lead to reversions in several critical functions or the platform user will lost all funds when paying the fee. If the authorization sets to fee as %1000, the exchange owner can steal funds.
Code Review
Consider defining upper and lower bounds on the feeBps variable.
#0 - HardlyDifficult
2022-07-11T00:05:02Z
🌟 Selected for report: joestakey
Also found by: 0x1f8b, 0x29A, 0x52, 0xDjango, 0xNazgul, 0xNineDec, 0xf15ers, 0xkowloon, 0xmint, 8olidity, BowTiedWardens, Chom, Cityscape, Czar102, ElKu, FSchmoede, Funen, GimelSec, GreyArt, IllIllI, KIntern, Kaiziron, Kenshin, Lambda, MadWookie, MiloTruck, PPrieditis, Picodes, Ruhum, Sm4rty, StErMi, TerrierLover, TomJ, Treasure-Seeker, VAD37, WatchPug, Wayne, _Adam, a12jmx, abhinavmir, antonttc, apostle0x01, asutorufos, berndartmueller, cccz, cloudjunky, codexploder, cryptphi, csanuragjain, defsec, delfin454000, fatherOfBlocks, georgypetrov, hake, hansfriese, horsefacts, hyh, k, kenta, nxrblsrpr, oyc_109, peritoflores, rajatbeladiya, reassor, rfa, robee, sach1r0, saian, samruna, shenwilly, simon135, sorrynotsorry, sseefried, throttle, unforgiven, wagmi, zzzitron
51.8223 USDC - $51.82
The afunctions that change critical parameters should emit events. Events allow capturing the changed parameters so that off-chain tools/interfaces can register such changes with timelocks that allow users to evaluate them and consider if they would like to engage/exit based on how they perceive the changes as affecting the trustworthiness of the protocol or profitability of the implemented financial services. The alternative of directly querying on-chain contract state for such changes is not considered practical for most users/usages.
Missing events and timelocks do not promote transparency and if such changes immediately affect users’ perception of fairness or trustworthiness, they could exit the protocol causing a reduction in liquidity which could negatively impact protocol TVL and reputation.
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L364 https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L375 https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L351
See similar High-severity H03 finding OpenZeppelin’s Audit of Audius (https://blog.openzeppelin.com/audius-contracts-audit/#high) and Medium-severity M01 finding OpenZeppelin’s Audit of UMA Phase 4 (https://blog.openzeppelin.com/uma-audit-phase-4/)
None
Add events to all functions that change critical parameters.
The critical procedures should be two step process.
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L364 https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L375 https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L351
Code Review
Lack of two-step procedure for critical operations leaves them error-prone. Consider adding two step procedure on the critical functions.
Missing checks for zero-addresses&values may lead to infunctional protocol, if the variable addresses are updated incorrectly.
There are a few validations that could be added to the system:
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L49
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L104
Code Review
Consider adding zero-address and zero value checks.
The contracts inherit OpenZeppelin's Ownable contract which enables the onlyOwner role to transfer ownership to another address. It's possible that the onlyOwner role mistakenly transfers ownership to the wrong address, resulting in a loss of the onlyOwner role. The current ownership transfer process involves the current owner calling Unlock.transferOwnership(). This function checks the new owner is not the zero address and proceeds to write the new owner's address into the owner's state variable. If the nominated EOA account is not a valid account, it is entirely possible the owner may accidentally transfer ownership to an uncontrolled account, breaking all functions with the onlyOwner() modifier. Lack of two-step procedure for critical operations leaves them error-prone if the address is incorrect, the new address will take on the functionality of the new role immediately
for Ex : -Alice deploys a new version of the whitehack group address. When she invokes the whitehack group address setter to replace the address, she accidentally enters the wrong address. The new address now has access to the role immediately and is too late to revert
None
Implement zero address check and Consider implementing a two step process where the owner nominates an account and the nominated account needs to call an acceptOwnership() function for the transfer of ownership to fully succeed. This ensures the nominated EOA account is a valid and active account.
In case a hack is occuring or an exploit is discovered, the team should be able to pause functionality until the necessary changes are made to the system.
To use a thorchain example again, the team behind thorchain noticed an attack was going to occur well before the system transferred funds to the hacker. However, they were not able to shut the system down fast enough. (According to the incidence report here: https://github.com/HalbornSecurity/PublicReports/blob/master/Incident%20Reports/Thorchain_Incident_Analysis_July_23_2021.pdf)
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L50
Code Review
Pause functionality on the contract would have helped secure the funds quickly.
Block timestamps have historically been used for a variety of applications, such as entropy for random numbers (see the Entropy Illusion for further details), locking funds for periods of time, and various state-changing conditional statements that are time-dependent. Miners have the ability to adjust timestamps slightly, which can prove to be dangerous if block timestamps are used incorrectly in smart contracts.
https://github.com/code-423n4/2022-04-mimo/blob/main/core/contracts/inception/InceptionVaultsCore.sol#L51
Manual Code Review
Block timestamps should not be used for entropy or generating random numbers—i.e., they should not be the deciding factor (either directly or through some derivation) for winning a game or changing an important state.
Time-sensitive logic is sometimes required; e.g., for unlocking contracts (time-locking), completing an ICO after a few weeks, or enforcing expiry dates. It is sometimes recommended to use block.number and an average block time to estimate times; with a 10 second block time, 1 week equates to approximately, 60480 blocks. Thus, specifying a block number at which to change a contract state can be more secure, as miners are unable to easily manipulate the block number.
The system is heavily relies on the owner role. Therefore, It contains centralization risk If the owner is EOA and captured.
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L50 https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L15
None
We advise the client to carefully manage the executor accounts' private key to avoid any potential risks of being hacked. In general, we strongly recommend centralized privileges or roles in the protocol to be improved via a decentralized mechanism or smart-contract-based accounts with enhanced security practices, e.g., Multi-Signature wallets.
#0 - nneverlander
2022-06-22T18:17:32Z
Thanks
#1 - HardlyDifficult
2022-07-10T21:30:26Z
🌟 Selected for report: IllIllI
Also found by: 0v3rf10w, 0x1f8b, 0x29A, 0xAsm0d3us, 0xDjango, 0xKitsune, 0xNazgul, 0xf15ers, 0xkatana, 0xkowloon, BowTiedWardens, Chom, ElKu, FSchmoede, Funen, GimelSec, Kaiziron, Kenshin, Lambda, MadWookie, MiloTruck, PPrieditis, Picodes, PwnedNoMore, StErMi, Tadashi, TerrierLover, TomJ, Tomio, Wayne, Waze, _Adam, antonttc, apostle0x01, asutorufos, c3phas, codexploder, defsec, delfin454000, fatherOfBlocks, hake, hansfriese, hyh, joestakey, k, kenta, oyc_109, peritoflores, reassor, rfa, robee, sach1r0, simon135, slywaters, zer0dot
43.9443 USDC - $43.94
[S]: Suggested optimation, save a decent amount of gas without compromising readability;
[M]: Minor optimation, the amount of gas saved is minor, change when you see fit;
[N]: Non-preferred, the amount of gas saved is at cost of readability, only apply when gas saving is a top priority.
> 0
can be replaced with != 0
for gas optimization - [S]Shortening revert strings to fit in 32 bytes will decrease deploy time gas and will decrease runtime gas when the revert condition has been met.
Revert strings that are longer than 32 bytes require at least one additional mstore, along with additional overhead for computing memory offset, etc.
Revert strings > 32 bytes are here:
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L395 https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L1141
Manual Review
Shorten the revert strings to fit in 32 bytes. That will affect gas optimization.
For the arithmetic operations that will never over/underflow, using the unchecked directive (Solidity v0.8 has default overflow/underflow checks) can save some gas from the unnecessary internal over/underflow checks.
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L201
None
Consider applying unchecked arithmetic where overflow/underflow is not possible. Example can be seen from below.
Unchecked{i++};
Since _amount can be 0. Checking if (_amount != 0) before the transfer can potentially save an external call and the unnecessary gas cost of a 0 token transfer.
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L346
All Contracts
None
Consider checking amount != 0.
Boolean is default initialized to false. There is no need assign false to variable.
2022-06-infinity/contracts/core/InfinityExchange.sol::148 => for (uint256 i = 0; i < numMakerOrders; ) { 2022-06-infinity/contracts/core/InfinityExchange.sol::200 => for (uint256 i = 0; i < ordersLength; ) { 2022-06-infinity/contracts/core/InfinityExchange.sol::219 => for (uint256 i = 0; i < ordersLength; ) { 2022-06-infinity/contracts/core/InfinityExchange.sol::272 => for (uint256 i = 0; i < numSells; ) { 2022-06-infinity/contracts/core/InfinityExchange.sol::308 => for (uint256 i = 0; i < numMakerOrders; ) { 2022-06-infinity/contracts/core/InfinityExchange.sol::349 => for (uint256 i = 0; i < ordersLength; ) { 2022-06-infinity/contracts/core/InfinityExchange.sol::393 => for (uint256 i = 0; i < numNonces; ) { 2022-06-infinity/contracts/core/InfinityExchange.sol::1048 => for (uint256 i = 0; i < numNfts; ) { 2022-06-infinity/contracts/core/InfinityExchange.sol::1086 => for (uint256 i = 0; i < numTokens; ) { 2022-06-infinity/contracts/core/InfinityExchange.sol::1109 => for (uint256 i = 0; i < numNfts; ) { 2022-06-infinity/contracts/core/InfinityExchange.sol::1190 => for (uint256 i = 0; i < numNfts; ) { 2022-06-infinity/contracts/core/InfinityExchange.sol::1206 => for (uint256 i = 0; i < numTokens; ) { 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::76 => for (uint256 i = 0; i < ordersLength; ) { 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::82 => for (uint256 j = 0; j < nftsLength; ) { 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::197 => uint256 numConstructedItems = 0; 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::199 => for (uint256 i = 0; i < nftsLength; ) { 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::214 => uint256 numTakerItems = 0; 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::216 => for (uint256 i = 0; i < nftsLength; ) { 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::244 => uint256 numCollsMatched = 0; 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::246 => for (uint256 i = 0; i < order2NftsLength; ) { 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::247 => for (uint256 j = 0; j < order1NftsLength; ) { 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::289 => uint256 numTokenIdsPerCollMatched = 0; 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::290 => for (uint256 k = 0; k < item2TokensLength; ) { 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::291 => for (uint256 l = 0; l < item1TokensLength; ) { 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::318 => uint256 sum = 0; 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::320 => for (uint256 i = 0; i < ordersLength; ) {
Code Review
bool x = false costs more gas than bool x without having any different functionality.
Using double require instead of operator && can save more gas.
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L215-L220
Code Review
Example
using &&: function check(uint x)public view{ require(x == 0 && x < 1 ); } // gas cost 21630 using double require: require(x == 0 ); require( x < 1); } } // gas cost 21622
Strict inequalities add a check of non equality which costs around 3 gas.
2022-06-infinity/contracts/MockERC721.sol::11 => for (uint256 i = 0; i < 100; i++) { 2022-06-infinity/contracts/core/InfinityExchange.sol::148 => for (uint256 i = 0; i < numMakerOrders; ) { 2022-06-infinity/contracts/core/InfinityExchange.sol::200 => for (uint256 i = 0; i < ordersLength; ) { 2022-06-infinity/contracts/core/InfinityExchange.sol::219 => for (uint256 i = 0; i < ordersLength; ) { 2022-06-infinity/contracts/core/InfinityExchange.sol::272 => for (uint256 i = 0; i < numSells; ) { 2022-06-infinity/contracts/core/InfinityExchange.sol::308 => for (uint256 i = 0; i < numMakerOrders; ) { 2022-06-infinity/contracts/core/InfinityExchange.sol::349 => for (uint256 i = 0; i < ordersLength; ) { 2022-06-infinity/contracts/core/InfinityExchange.sol::393 => for (uint256 i = 0; i < numNonces; ) { 2022-06-infinity/contracts/core/InfinityExchange.sol::1048 => for (uint256 i = 0; i < numNfts; ) { 2022-06-infinity/contracts/core/InfinityExchange.sol::1086 => for (uint256 i = 0; i < numTokens; ) { 2022-06-infinity/contracts/core/InfinityExchange.sol::1109 => for (uint256 i = 0; i < numNfts; ) { 2022-06-infinity/contracts/core/InfinityExchange.sol::1190 => for (uint256 i = 0; i < numNfts; ) { 2022-06-infinity/contracts/core/InfinityExchange.sol::1206 => for (uint256 i = 0; i < numTokens; ) { 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::76 => for (uint256 i = 0; i < ordersLength; ) { 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::82 => for (uint256 j = 0; j < nftsLength; ) { 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::197 => uint256 numConstructedItems = 0; 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::199 => for (uint256 i = 0; i < nftsLength; ) { 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::214 => uint256 numTakerItems = 0; 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::216 => for (uint256 i = 0; i < nftsLength; ) { 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::244 => uint256 numCollsMatched = 0; 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::246 => for (uint256 i = 0; i < order2NftsLength; ) { 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::247 => for (uint256 j = 0; j < order1NftsLength; ) { 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::289 => uint256 numTokenIdsPerCollMatched = 0; 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::290 => for (uint256 k = 0; k < item2TokensLength; ) { 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::291 => for (uint256 l = 0; l < item1TokensLength; ) { 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::318 => uint256 sum = 0; 2022-06-infinity/contracts/core/InfinityOrderBookComplication.sol::320 => for (uint256 i = 0; i < ordersLength; ) {
Code Review
Use >= or <= instead of > and < when possible.
Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met)
Source Custom Errors in Solidity:
Starting from Solidity v0.8.4, there is a convenient and gas-efficient way to explain to users why an operation failed through the use of custom errors. Until now, you could already use strings to give more information about failures (e.g., revert("Insufficient funds.");), but they are rather expensive, especially when it comes to deploy cost, and it is difficult to use dynamic information in them.
Custom errors are defined using the error statement, which can be used inside and outside of contracts (including interfaces and libraries).
Instances include:
All require Statements
Code Review
Recommended to replace revert strings with custom errors.
A division/multiplication by any number x being a power of 2 can be calculated by shifting log2(x) to the right/left.
While the DIV opcode uses 5 gas, the SHR opcode only uses 3 gas. Furthermore, Solidity's division operation also includes a division-by-0 prevention which is bypassed using shifting.
2022-06-infinity/contracts/staking/InfinityStaker.sol::39 => /// the user will get back 100/4 tokens. 2022-06-infinity/contracts/staking/InfinityStaker.sol::235 => (userstakedAmounts[user][Duration.THREE_MONTHS].amount * 2) + 2022-06-infinity/contracts/staking/InfinityStaker.sol::237 => (userstakedAmounts[user][Duration.TWELVE_MONTHS].amount * 4)) / (10**18);
None
A division/multiplication by any number x being a power of 2 can be calculated by shifting log2(x) to the right/left.
Solidity 0.6.5 introduced immutable as a major feature. It allows setting contract-level variables at construction time which gets stored in code rather than storage.
Consider the following generic example:
contract C { /// The owner is set during contruction time, and never changed afterwards. address public owner = msg.sender; }
In the above example, each call to the function owner() reads from storage, using a sload. After EIP-2929, this costs 2100 gas cold or 100 gas warm. However, the following snippet is more gas efficient:
contract C { /// The owner is set during contruction time, and never changed afterwards. address public immutable owner = msg.sender; }
In the above example, each storage read of the owner state variable is replaced by the instruction push32 value, where value is set during contract construction time. Unlike the last example, this costs only 3 gas.
https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L25
None
Consider using immutable variable.
The contracts assigns two constants to the result of a keccak operation, which results in gas waste since the expression is computed each time the constant is accessed.
See this issue for more context: ethereum/solidity#9232 (https://github.com/ethereum/solidity/issues/9232)
2022-06-infinity/contracts/core/InfinityExchange.sol::1186 => // keccak256('OrderItem(address collection,TokenInfo[] tokens)TokenInfo(uint256 tokenId,uint256 numTokens)') 2022-06-infinity/contracts/core/InfinityExchange.sol::1191 => bytes32 hash = keccak256(abi.encode(ORDER_ITEM_HASH, nfts[i].collection, _tokensHash(nfts[i].tokens))); 2022-06-infinity/contracts/core/InfinityExchange.sol::1197 => bytes32 nftsHash = keccak256(abi.encodePacked(hashes)); 2022-06-infinity/contracts/core/InfinityExchange.sol::1202 => // keccak256('TokenInfo(uint256 tokenId,uint256 numTokens)') 2022-06-infinity/contracts/core/InfinityExchange.sol::1207 => bytes32 hash = keccak256(abi.encode(TOKEN_INFO_HASH, tokens[i].tokenId, tokens[i].numTokens)); 2022-06-infinity/contracts/core/InfinityExchange.sol::1213 => bytes32 tokensHash = keccak256(abi.encodePacked(hashes)); 2022-06-infinity/contracts/libs/SignatureChecker.sol::61 => bytes32 digest = keccak256(abi.encodePacked('\x19\x01', domainSeparator, orderHash)); 2022-06-infinity/contracts/token/InfinityToken.sol::25 => bytes32 public constant EPOCH_INFLATION = keccak256('Inflation'); 2022-06-infinity/contracts/token/InfinityToken.sol::26 => bytes32 public constant EPOCH_DURATION = keccak256('EpochDuration'); 2022-06-infinity/contracts/token/InfinityToken.sol::27 => bytes32 public constant EPOCH_CLIFF = keccak256('Cliff'); 2022-06-infinity/contracts/token/InfinityToken.sol::28 => bytes32 public constant MAX_EPOCHS = keccak256('MaxEpochs'); 2022-06-infinity/contracts/token/TimelockConfig.sol::9 => bytes32 public constant ADMIN = keccak256('Admin'); 2022-06-infinity/contracts/token/TimelockConfig.sol::10 => bytes32 public constant TIMELOCK = keccak256('Timelock');
None
Replace the constant directive with immutable, or assign the already hashed value to the constants
> 0
can be replaced with != 0
for gas optimization!= 0
is a cheaper operation compared to > 0
, when dealing with uint.
2022-06-infinity/contracts/core/InfinityExchange.sol::392 => require(numNonces > 0, 'cannot be empty');
None
Consider to replace > 0
with != 0
for gas optimization.