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: 63/110
Findings: 1
Award: $52.83
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
52.8286 USDC - $52.83
The for loop length can be cached to memory before the loop, even for memory variables. The demo of the loop gas comparison can be seen here.
src/Vault.sol 443: for (uint256 i = 0; i < epochsLength(); i++) {
Suggestion: Cache the length before the loop.
uint length = names.length;
unchecked{++i}
COSTS LESS GAS COMPARED TO i++
OR i += 1
Use ++i
 instead of i++
 to increment the value of an uint variable, and for guaranteed non-overflow/underflow, it can be unchecked.
And the optimizer need to be turned on.
The demo of the loop gas comparison can be seen here.
src/Vault.sol 443: for (uint256 i = 0; i < epochsLength(); i++) {
Suggestion: For readability, uncheck the whole for loop is the same.
unchecked{ for (uint256 i; i < length; ++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.
src/Vault.sol 443: for (uint256 i = 0; i < epochsLength(); i++) { src/VaultFactory.sol 159: marketIndex = 0; src/rewards/StakingRewards.sol 36: uint256 public periodFinish = 0;
The demo of the loop gas comparison can be seen here.
X = X + Y / X = X - Y
IS CHEAPER THAN X += Y / X -= Y
The demo of the gas comparison can be seen here.
Consider use X = X + Y / X = X - Y
to save gas.
src/VaultFactory.sol 195: marketIndex += 1;
!= 0
uses less gas than > 0
!= 0
costs less gas compared to > 0
for unsigned integers with the optimizer enabled (6 gas).
Here is the code as a demo.
Opcode discussion.
src/Vault.sol 187: require(msg.value > 0, "ZeroValue"); src/oracles/PegOracle.sol 98: require(price1 > 0, "Chainlink price <= 0"); 121: require(price2 > 0, "Chainlink price <= 0"); src/rewards/StakingRewards.sol 119: require(amount > 0, "Cannot withdraw 0"); 134: if (reward > 0) {
Suggestion:
> 0
with != 0
.Shortening revert strings to fit in 32 bytes will decrease deployment time gas and will decrease runtime gas when the revert condition is met. Revert strings that are longer than 32 bytes require at least one additional mstore, along with additional overhead for computing memory offset, etc.
src/SemiFungibleVault.sol 116-119: require( msg.sender == owner || isApprovedForAll(owner, receiver), "Only owner can withdraw, or owner has approved receiver for all" ); src/oracles/PegOracle.sol 23-24: require(_oracle1 != address(0), "oracle1 cannot be the zero address"); require(_oracle2 != address(0), "oracle2 cannot be the zero address"); src/rewards/StakingRewards.sol 217-220: require( tokenAddress != address(stakingToken), "Cannot withdraw the staking token" ); 226-229: require( block.timestamp > periodFinish, "Previous rewards period must be complete before changing the duration for the new period" );
Suggestion: Shortening the revert strings to fit in 32 bytes, or using custom errors as described next.
Custom errors from Solidity 0.8.4 are more gas efficient than revert strings (lower deployment cost and runtime cost when the revert condition is met) (reference)
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).
The demo of the gas comparison can be seen here.
The following files have multiple require()
statements can use custom errors:
src/SemiFungibleVault.sol 91: require((shares = previewDeposit(id, assets)) != 0, "ZERO_SHARES"); 116-119: require( msg.sender == owner || isApprovedForAll(owner, receiver), "Only owner can withdraw, or owner has approved receiver for all" ); src/Vault.sol 165: require((shares = previewDeposit(id, assets)) != 0, "ZeroValue"); 187: require(msg.value > 0, "ZeroValue"); src/oracles/PegOracle.sol 23-30: require(_oracle1 != address(0), "oracle1 cannot be the zero address"); require(_oracle2 != address(0), "oracle2 cannot be the zero address"); require(_oracle1 != _oracle2, "Cannot be same Oracle"); require( (priceFeed1.decimals() == priceFeed2.decimals()), "Decimals must be the same" ); 98-103: require(price1 > 0, "Chainlink price <= 0"); require( answeredInRound1 >= roundID1, "RoundID from Oracle is outdated!" ); require(timeStamp1 != 0, "Timestamp == 0 !"); 121-126: require(price2 > 0, "Chainlink price <= 0"); require( answeredInRound2 >= roundID2, "RoundID from Oracle is outdated!" ); require(timeStamp2 != 0, "Timestamp == 0 !"); src/rewards/StakingRewards.sol 96: require(amount != 0, "Cannot stake 0"); 119: require(amount > 0, "Cannot withdraw 0"); 220-205: require( rewardRate <= balance.div(rewardsDuration), "Provided reward too high" ); 217-220: require( tokenAddress != address(stakingToken), "Cannot withdraw the staking token" ); 226-229: require( block.timestamp > periodFinish, "Previous rewards period must be complete before changing the duration for the new period" );
The expression is re-calculated each time the constant is referenced.
src/Vault.sol 388-389: keccak256(abi.encodePacked(symbol)) == keccak256(abi.encodePacked("rY2K"))
The consequence is: each usage costs ~100gas more on each access (it is still a little better than storing the result in storage, but not much..)
Suggestion: Change these expressions from constant to immutable and implement the calculation in the constructor or hardcode these values in the constants and add a comment to say how the value was calculated.
reference: https://github.com/ethereum/solidity/issues/9232
Changing the visibility from public to private or internal can save gas when a constant isn’t used outside of its contract.
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 needed, the value can be read from the verified contract source code.
A cheap way to store constants in a single location is to create an internal constant in a library. If the variable is a local cache of another contract’s value, consider making the cache variable internal or private, which will require external users to query the contract with the source of truth, so that callers don’t get out of sync.
The followings can be changed from public to internal or private:
src/Controller.sol uint256 public constant VAULTS_LENGTH = 2;
Use bytes32 instead of string to save gas whenever possible. String is a dynamic data structure and therefore is more gas consuming then bytes32.
Bytes32 can be used instead of string in the following:
src/VaultFactory.sol 199-217: Vault hedge = new Vault( WETH, string(abi.encodePacked(_name,"HEDGE")), "hY2K", treasury, _token, _strikePrice, controller ); Vault risk = new Vault( WETH, string(abi.encodePacked(_name,"RISK")), "rY2K", treasury, _token, _strikePrice, controller );
Comparing to a constant (true or false) is a bit more expensive than directly checking the returned boolean value.
The demo of the gas comparison can be seen here.
Suggestion:
Use
if (value) / if (!value)
instead of
if (value == true) / if (value == false)
in the following:
src/Vault.sol 80: if(idExists[id] != true) 96: if((block.timestamp < id) && idDepegged[id] == false) 215-217: if( msg.sender != owner && isApprovedForAll(owner, receiver) == false) 314: if(idExists[epochEnd] == true) src/Controller.sol 93: if(vault.idExists(epochEnd) == false) 211: if(insrVault.idExists(epochEnd) == false || riskVault.idExists(epochEnd) == false) src/rewards/RewardsFactory.sol 96: if(Vault(_insrToken).idExists(_epochEnd) == false || Vault(_riskToken).idExists(_epochEnd) == false)
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.
src/Vault.sol 47-55: mapping(uint256 => uint256) public idFinalTVL; mapping(uint256 => uint256) public idClaimTVL; mapping(uint256 => uint256) public idEpochBegin; mapping(uint256 => bool) public idDepegged; mapping(uint256 => bool) public idExists; mapping(uint256 => uint256) public epochFee; src/VaultFactory.sol 119-120: mapping(uint256 => address[]) public indexVaults; //[0] hedge and [1] risk vault mapping(uint256 => uint256[]) public indexEpochs; //all epochs in the market src/rewards/StakingRewards.sol 43-47: mapping(address => uint256) public userRewardPerTokenPaid; mapping(address => uint256) public rewards; mapping(address => uint256) private _balances;
Since the share is 1:1 ratio with the deposit, there is no need to call previewDeposit()
:
// src/Vault.sol 165: require((shares = previewDeposit(id, assets)) != 0, "ZeroValue");
totalAssets(id)
totalAssets(id)
call is duplicate, it can be called before and pass the result.
// src/Controller.sol 220: insrVault.endEpoch(epochEnd, false); 221: riskVault.endEpoch(epochEnd, false); // src/Vault.sol function endEpoch(uint256 id, bool depeg) public onlyController marketExists(id) { idDepegged[id] = depeg; idFinalTVL[id] = totalAssets(id); }
getLatestPrice()
In triggerDepeg()
, the call to getLatestPrice()
is duplicate, including in the modifier. The result can be saved in a local variable, instead of multiple gas intensive calls.
// src/Controller.sol function triggerDepeg(uint256 marketIndex, uint256 epochEnd) public isDisaster(marketIndex, epochEnd) { // ... emit DepegInsurance(..., getLatestPrice(insrVault.tokenInsured()) ); } modifier isDisaster(uint256 marketIndex, uint256 epochEnd) { // ... if( vault.strikePrice() < getLatestPrice(vault.tokenInsured()) ) revert PriceNotAtStrikePrice(getLatestPrice(vault.tokenInsured())); // ... }
The ended epoch or settled epoch with depeg can delete the vault storage to get some gas refund.