Platform: Code4rena
Start Date: 14/09/2022
Pot Size: $50,000 USDC
Total HM: 25
Participants: 110
Period: 5 days
Judge: hickuphh3
Total Solo HM: 9
Id: 162
League: ETH
Rank: 79/110
Findings: 2
Award: $52.80
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Respx
Also found by: 0x1f8b, 0xDecorativePineapple, 0xNazgul, 0xPanas, 0xSmartContract, 0xc0ffEE, 0xmuxyz, Aymen0909, Bahurum, Bnke0x0, CodingNameKiki, Deivitto, Jeiwan, Lambda, Picodes, PwnPatrol, R2, RaymondFam, Rolezn, Ruhum, Saintcode_, SooYa, Tointer, V_B, ajtra, ak1, async, auditor0517, brgltd, c3phas, carrotsmuggler, cccz, csanuragjain, datapunk, djxploit, durianSausage, eierina, erictee, gogo, imare, joestakey, jonatascm, kv, ladboy233, leosathya, lukris02, oyc_109, pashov, pauliax, rbserver, robee, rokinot, rvierdiiev, scaraven, simon135, unforgiven, wagmi, zzzitron
36.6223 USDC - $36.62
[L-01]. Outdated compiler version [L-02]. Missing checks for address(0x0) when assigning values to address state variables
[N-01]. Public functions not called by the contract should be declared external instead [N-02]. Constants should be defined rather than using magic numbers [N-03]. Event is missing indexed fields [N-04]. File is missing NatSpec
The latest version is 0.8.16 (the project is using 0.8.15). It's best practice to use the latest version due to fix bugs and have improvements from older versions.
SemiFungibleVault.sol#L2 VaultFactory.sol#L2 RewardsFactory.sol#L2 Controller.sol#L2 StakingRewards.sol#L2 PegOracle.sol#L2 Vault.sol#L2
RewardsFactory.sol#L68-L70 StakingRewards.sol#L81-L83
Contracts are allowed to override their parents' functions and change the visibility from external to public.
Vault.sol#L277 Vault.sol#L287 Vault.sol#L295 Vault.sol#L307 Vault.sol#L336 Vault.sol#L350 Vault.sol#L360 Vault.sol#L438 PegOracle.sol#L89 SemiFungibleVault.sol#L85-L89 SemiFungibleVault.sol#L189-L193 SemiFungibleVault.sol#L221-L225 SemiFungibleVault.sol#L237 SemiFungibleVault.sol#L244 SemiFungibleVault.sol#L251 SemiFungibleVault.sol#L263 VaultFactory.sol#L178-L186 VaultFactory.sol#L248-L253 VaultFactory.sol#L295 VaultFactory.sol#L308-L309 VaultFactory.sol#L327-L328 VaultFactory.sol#L345-L346 VaultFactory.sol#L366 VaultFactory.sol#L385-L386 RewardsFactory.sol#L145-L146 Controller.sol#L148-L149 Controller.sol#L198
PegOracle.sol#L68 PegOracle.sol#L70
Index 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.
SemiFungibleVault.sol#L35-L41 SemiFungibleVault.sol#L51-L58 VaultFactory.sol#L49-L56 VaultFactory.sol#L69-L80 VaultFactory.sol#L91 VaultFactory.sol#L97 VaultFactory.sol#L103 VaultFactory.sol#L113 RewardsFactory.sol#L39-L44 Controller.sol#L49-L56 StakingRewards.sol#L51-L56
SemiFungibleVault.sol VaultFactory.sol RewardsFactory.sol Controller.sol StakingRewards.sol PegOracle.sol Vault.sol
🌟 Selected for report: pfapostol
Also found by: 0x040, 0x1f8b, 0x4non, 0xNazgul, 0xSmartContract, 0xc0ffEE, 0xkatana, Aymen0909, Bnke0x0, Deivitto, Diana, JAGADESH, KIntern_NA, Lambda, MiloTruck, R2, RaymondFam, Respx, ReyAdmirado, Rohan16, RoiEvenHaim, Rolezn, Ruhum, Saintcode_, Samatak, Sm4rty, SnowMan, Tomio, Tomo, WilliamAmbrozic, _Adam, __141345__, ajtra, ak1, async, c3phas, ch0bu, cryptostellar5, d3e4, delfin454000, dharma09, djxploit, durianSausage, eierina, erictee, fatherOfBlocks, gianganhnguyen, gogo, ignacio, imare, jag, jonatascm, leosathya, lukris02, malinariy, oyc_109, pashov, pauliax, peanuts, peiw, prasantgupta52, robee, rokinot, rotcivegaf, rvierdiiev, seyni, simon135, slowmoses, sryysryy, tnevler, zishansami
16.1756 USDC - $16.18
[G01] Post-increment/decrement cost more gas then pre-increment/decrement [G02] != 0 is cheaper than > 0 especially in state variables [G03] I = I + (-) X is cheaper in gas cost than I += (-=) X [G04] Require / Revert strings longer than 32 bytes cost extra gas [G05] Use custom errors rather than REVERT()/REQUIRE() strings to save deployment gas [G06] Using private rather than public for constants [G07] Don't compare boolean expressions to boolean literals [G08] Using bools for storage incurs overhead [G09] ABI.ENCODE() is less efficient than ABI.ENCODEPACKED() [G10] Using storage instead of memory for structs/arrays [G11] Multiple address mappings can be combined into a single mapping of an address to a struct, where appropriate [G12] State variables only set in the constructor should be declared immutable [G13] Should use unchecked in arithmetic operations when it's not possible to overflow [G14] Initialize variables with default values are not needed [G15] Not use local variable when it's going to be used once [G16] Reordering code in Controller.getLatestPrice to save gas [G17] Optimize getNextEpoch to save gas
++I (--I) cost less gas than I++ (I--) especially in a loop.
contract TestPost { function testPost() public { uint256 i; i++; } } contract TestPre { function testPre() public { uint256 i; ++i; } }
Replace all > 0 for != 0. In cases where we used a variable from state assigned to a local variable it's the same gas save.
contract TestA { uint256 _paramA; function Test () public returns (bool) { return _paramA > 0; } } contract TestB { uint256 _paramA; function Test () public returns (bool) { return _paramA != 0; } } contract TestC { uint256 _paramA; function Test () public returns (bool) { uint256 aux = _paramA; return aux > 0; } } contract TestD { uint256 _paramA; function Test () public returns (bool) { uint256 aux = _paramA; return aux != 0; } }
Vault.sol#L311 StakingRewards.sol#L134
This is especially with state variables. In the following example I'm trying to demostrate the save gas in a loop of 10 iterations.
contract TestA { uint256 i; function Test () public { for(;i<10;){ i += 1; } } } contract TestB { uint256 i; function Test () public { for(;i<10;){ i = i + 1; } } }
Each extra memory word of bytes past the original 32 incurs an MSTORE which costs 3 gas
PegOracle.sol#L23 PegOracle.sol#L24 SemiFungibleVault.sol#L116 StakingRewards.sol#L217 StakingRewards.sol#L226
Custom errors save ~50 gas each time they're hitby avoiding having to allocate and store the revert string. Not defining the strings also save deployment gas.
PegOracle.sol#L23 PegOracle.sol#L24 PegOracle.sol#L25 PegOracle.sol#L28 PegOracle.sol#L98 PegOracle.sol#L99 PegOracle.sol#L103 PegOracle.sol#L121 PegOracle.sol#L122 PegOracle.sol#L126 SemiFungibleVault.sol#L91 SemiFungibleVault.sol#L116 Vault.sol#L165 Vault.sol#L187 StakingRewards.sol#L96 StakingRewards.sol#L119 StakingRewards.sol#L202 StakingRewards.sol#L217 StakingRewards.sol#L226
If needed, the value can be read from the verified contract source code. Savings are due to the compiler not having to create non-payable getter functions for deployment calldata, and not adding another entry to the method ID table.
if (<x> == true) => if (<x>), if (<x> == false) => if (!<x>)
contract TestA { function Test (bool paramA) public { require(paramA == true, "Test bool"); } } contract TestB { function Test (bool paramA) public { require(paramA, "Test bool"); } }
Controller.sol#L93 Controller.sol#L211 Vault.sol#L80 Vault.sol#L96 Vault.sol#L217 Vault.sol#L314 RewardsFactory.sol#L96
Use uint256(1) and uint256(2) for true/false to avoid a Gwarmaccess (100 gas), and to avoid Gsset (20000 gas) when changing from 'false' to 'true', after having been 'true' in the past
RewardsFactory.sol#L118 RewardsFactory.sol#L150
When retrieving data from a memory location, assigning the data to a memory variable causes all fields of the struct/array to be read from memory, resulting in a Gcoldsload (2100 gas) for each field of the struct/array. When reading fields from new memory variables, they cause an extra MLOAD instead of a cheap stack read. Rather than declaring a variable with the memory keyword, it is much cheaper to declare a variable with the storage keyword and cache all fields that need to be read again in a stack variable, because the fields actually read will only result in a Gcoldsload. The only case where the entire struct/array is read into a memory variable is when the entire struct/array is returned by a function, passed to a function that needs memory, or when the array/struct is read from another store array/struc.
VaultFactory.sol#L313 VaultFactory.sol#L331 VaultFactory.sol#L352
Saves a storage slot for the mapping. Depending on the circumstances and sizes of types, can avoid a Gsset (20000 gas) per mapping combined. Reads and subsequent writes can also be cheaper when a function requires both values and they both fit in the same storage slot. Finally, if both fields are accessed in the same function, can save ~42 gas per access due to not having to recalculate the key's keccak256 hash (Gkeccak256 - 30 gas) and that calculation's associated stack operations.
In the case of the variables StakingRewards.userRewardPerTokenPaid and StakingRewards._balances could save gas combining it in a struct.
StakingRewards.sol#L43 StakingRewards.sol#L47
Avoids a Gsset (20000 gas) in the constructor, and replaces the first access in each transaction (Gcoldsload - 2100 gas) and each access thereafter (Gwarmacces - 100 gas) with a PUSH32 (3 gas).
RewardsFactory.sol#L9 RewardsFactory.sol#L10 RewardsFactory.sol#L11 VaultFactory.sol#L12 VaultFactory.sol#L13 SemiFungibleVault.sol#L19 SemiFungibleVault.sol#L20 SemiFungibleVault.sol#L21 PegOracle.sol#L10 PegOracle.sol#L11 PegOracle.sol#L13
This situation ocurrs especially in loops
Example,
- for (uint256 i = 0; i < epochsLength(); i++) { + for (uint256 i = 0; i < epochsLength();) { if (epochs[i] == _epoch) { if (i == epochsLength() - 1) { return 0; } return epochs[i + 1]; } + unchecked { + ++i; + } }
If a variable is not set/initialized, it is assumed to have the default value (0 for uint, false for bool, address(0) for address,...). Explicitly initializing it with its default value is an anti-pattern and wastes gas.
Vault.sol#L443 VaultFactory.sol#L159 StakingRewards.sol#L36
Not store a value in a local variable when it's going to be used once, instead of that, use directly the value.
- MarketVault memory marketVault = MarketVault(marketIndex, epochBegin, epochEnd, hedge, risk, _withdrawalFee); - _createEpoch(marketVault); + _createEpoch(MarketVault(marketIndex, epochBegin, epochEnd, hedge, risk, _withdrawalFee));
- address hedge = indexVaults[index][0]; - address risk = indexVaults[index][1]; - MarketVault memory marketVault = MarketVault(index, epochBegin, epochEnd, Vault(hedge), Vault(risk), _withdrawalFee); + MarketVault memory marketVault = MarketVault(index, epochBegin, epochEnd, Vault(indexVaults[index][0]), Vault(indexVaults[index][1]), _withdrawalFee);
- Vault insr = Vault(vaults[0]); - Vault risk = Vault(vaults[1]); - insr.changeTreasury(_treasury); - risk.changeTreasury(_treasury); + Vault(vaults[0]).changeTreasury(_treasury); + Vault(vaults[1]).changeTreasury(_treasury);
VaultFactory.sol#L314-L317 VaultFactory.sol#L332-L335 VaultFactory.sol#L353-L356
- bool isSequencerUp = answer == 0; - if (!isSequencerUp) { + if (!(answer == 0)) { revert SequencerDown(); }
- uint256 timeSinceUp = block.timestamp - startedAt; - if (timeSinceUp <= GRACE_PERIOD_TIME) { + if ((block.timestamp - startedAt) <= GRACE_PERIOD_TIME) { revert GracePeriodNotOver(); }
- uint256 decimals = 10**(18-(priceFeed.decimals())); - price = price * int256(decimals); + price = price * int256(10**(18-(priceFeed.decimals())));
In the following part of the code of getLatestPrice it's possible to move the price calculation after the two if's (if(answeredInRound < roundID) and if(timeStamp == 0)) due to those if's not depend on the price. With this change it's possible to save gas in the situations when those if's are true.
- uint256 decimals = 10**(18-(priceFeed.decimals())); - price = price * int256(decimals); - if(price <= 0) - revert OraclePriceZero(); if(answeredInRound < roundID) revert RoundIDOutdated(); if(timeStamp == 0) revert TimestampZero(); + uint256 decimals = 10**(18-(priceFeed.decimals())); + price = price * int256(decimals); + if(price <= 0) + revert OraclePriceZero(); return price;
Store epochsLength() in a local variable due to it's wasting gas in every loop. Would be a good option to use directly to epochs.length insted of call epochsLength() to avoid JUMP and save gas too.
+ uint256 length = epochsLength(); - for (uint256 i = 0; i < epochsLength(); i++) { + for (uint256 i = 0; i < length; i++) { if (epochs[i] == _epoch) { - if (i == epochsLength() - 1) { + if (i == length - 1) { return 0; } return epochs[i + 1]; } }