Platform: Code4rena
Start Date: 07/07/2023
Pot Size: $121,650 USDC
Total HM: 36
Participants: 111
Period: 7 days
Judge: Picodes
Total Solo HM: 13
Id: 258
League: ETH
Rank: 46/111
Findings: 1
Award: $247.48
๐ Selected for report: 0
๐ Solo Findings: 0
๐ Selected for report: c3phas
Also found by: 0x11singh99, 0xAnah, 0xn006e7, LeoS, Rageur, Raihan, ReyAdmirado, Rolezn, SAAJ, SAQ, SM3_SS, Udsen, alymurtazamemon, hunter_w3b, koxuan, naman1778, petrichor, ybansal2403
247.4795 USDC - $247.48
Issue | Instance | |
---|---|---|
[G-01] | Amounts should be checked for 0 before calling a transfer | 2 |
[G-02] | Access mappings directly rather than using accessor functions | 4 |
[G-03] | Using a positive conditional flow to save a NOT opcode | 5 |
[G-04] | 2**<n> should be re-written as type(uint<n>).max | 7 |
[G-05] | Internal/Private functions only called once can be inlined to save gas | 16 |
[G-06] | Use constants instead of type(uintx).max | 11 |
[G-07] | IFโs/require() statements that check input arguments should be at the top of the function | 4 |
[G-08] | Use assembly to write address storage values | 1 |
[G-09] | Use hardcode address instead address(this) | 18 |
[G-10] | abi.encode() is less efficient than abi.encodePacked() | 1 |
[G-11] | Using XOR (^) and OR ( | ) bitwise equivalents |
[G-12] | Donโt apply the same value to state variables | 1 |
[G-13] | Using private rather than public for constants, saves gas | 1 |
[G-14] | Using calldata instead of memory for read-only arguments in external functions saves gas | 3 |
[G-15] | Avoid emitting storage values | 6 |
[G-16] | Use assembly to perform efficient back-to-back calls | 2 |
[G-17] | Multiple accesses of a mapping/array should use a storage pointer | 1 |
[G-18] | Cache state variables outside of loop to avoid reading storage on every iteration | 1 |
[G-19] | Duplicated require()/revert() checks should be refactored to a modifier or function. | 1 |
it is generally a good practice to check the amount for 0 before calling a transfer function in order to save gas. This is because if the amount is 0, there is no need to execute the transfer function, which can save gas costs.
832 prizeToken.safeTransfer(_to, _amount);
1126 emit Transfer(address(0), _receiver, _shares);
Accessing mappings directly instead of using accessor functions can potentially save gas costs in some cases. This is because accessing a mapping directly involves a single storage read operation, while using an accessor function may involve multiple storage reads and writes.
316 DrawAccumulatorLib.add( vaultAccumulator[_prizeVault],
541 DrawAccumulatorLib.getDisbursedBetween( vaultAccumulator[_vault],
963 DrawAccumulatorLib.getDisbursedBetween( vaultAccumulator[_vault],
660 uint96(userObservations[_vault][_from].details.balance)
Accessing mappings directly rather than using accessor functions and using positive conditional flow to save a NOT opcode are both techniques that can be used to optimize smart contracts for gas efficiency.
428 if ( !_isWinner(msg.sender, _winner, _tier, _prizeIndex, _vaultPortion, _tierOdds, _drawDuration)
459 if (!targetAtOrAfter)
530 if (!_isFromDelegate && _fromDelegate != SPONSORSHIP_ADDRESS)
561 if (!_isToDelegate && _toDelegate != SPONSORSHIP_ADDRESS)
93 if (!targetAfterOrAt)
1200 if (!_isVaultCollateralized()) revert VaultUnderCollateralized();
In Solidity, the expression 2**<n> is commonly used to represent the maximum value that can be stored in a variable of size n bits. However, this expression can be expensive in terms of gas cost, particularly for large values of n, because it requires a lot of computational steps to calculate the result. To avoid this gas cost, the expression 2**<n> can be replaced with the built-in constant type(uint<n>).max. This constant represents the maximum value that can be stored in a variable of size n bits, and it is calculated by setting all bits in the variable to 1.
40 uint256 _numberOfPrizes = 4 ** _tier;
19 uint256 aAdjusted = _a > _timestamp ? _a : _a + 2 ** 32;
20 uint256 bAdjusted = _b > _timestamp ? _b : _b + 2 ** 32;
35 uint256 aAdjusted = _a > _timestamp ? _a : _a + 2 ** 32;
36 uint256 bAdjusted = _b > _timestamp ? _b : _b + 2 ** 32;
52 uint256 aAdjusted = _a > _timestamp ? _a : _a + 2 ** 32;
53 uint256 bAdjusted = _b > _timestamp ? _b : _b + 2 ** 32;
Inlining internal or private functions that are only called once can be an effective way to optimize gas costs in Solidity contracts.
When a function is inlined, its code is inserted directly into the calling function at compile time, rather than being called as a separate function at runtime. This can eliminate the gas cost associated with the function call, as well as any overhead associated with setting up the function's stack frame.
24 function getPerTimeUnit( uint256 _count, uint256 _durationSeconds ) internal pure returns (SD59x18)
39 function getVRGDAPrice( uint256 _targetPrice, uint256 _timeSinceStart, uint256 _sold, SD59x18 _perTimeUnit, SD59x18 _decayConstant ) internal pure returns (uint256)
331 function _nextDraw(uint8 _nextNumberOfTiers, uint96 _prizeTokenLiquidity) internal
518 function _consumeLiquidity( Tier memory _tierStruct, uint8 _tier, uint104 _liquidity ) internal returns (Tier memory)
120 function getTotalRemaining( Accumulator storage accumulator, uint16 _startDrawId, SD59x18 _alpha ) internal view returns (uint256)
166 function getDisbursedBetween( Accumulator storage _accumulator, uint16 _startDrawId, uint16 _endDrawId, SD59x18 _alpha ) internal view returns (uint256)
32 function estimatePrizeFrequencyInDraws(SD59x18 _tierOdds) internal pure returns (uint256)
52 function canaryPrizeCount( uint8 _numberOfTiers, uint8 _canaryShares, uint8 _reserveShares, uint8 _tierShares ) internal pure returns (UD60x18)
73 function isWinner( uint256 _userSpecificRandomNumber, uint128 _userTwab, uint128 _vaultTwabTotalSupply, SD59x18 _vaultContributionFraction, SD59x18 _tierOdds )internal pure returns (bool)
104 function calculatePseudoRandomNumber( address _user, uint8 _tier, uint32 _prizeIndex, uint256 _winningRandomNumber )internal pure returns (uint256)
133 function estimatedClaimCount( uint8 _numberOfTiers, uint16 _grandPrizePeriod ) internal pure returns (uint32)
31 function lte(uint32 _a, uint32 _b, uint32 _timestamp) internal pure returns (bool)
47 function checkedSub(uint32 _a, uint32 _b, uint32 _timestamp) internal pure returns (uint32)
75 function increaseBalances( uint32 PERIOD_LENGTH, uint32 PERIOD_OFFSET, Account storage _account, uint96 _amount, uint96 _delegateAmount ) internal
144 function decreaseBalances( uint32 PERIOD_LENGTH, uint32 PERIOD_OFFSET, Account storage _account, uint96 _amount, uint96 _delegateAmount, string memory _revertMessage ) internal
263 function getBalanceAt( uint32 PERIOD_OFFSET, ObservationLib.Observation[MAX_CARDINALITY] storage _observations, AccountDetails memory _accountDetails, uint32 _targetTime ) internal view returns (uint256)
Using constants instead of type(uintX).max can be an effective way to optimize gas costs in Solidity contracts. type(uintX).max is a built-in constant in Solidity that represents the maximum value that can be stored in a variable of size X bits. However, this constant can be expensive in terms of gas cost, particularly for large values of X, because it requires a lot of computational steps to calculate the result.
56 targetTimeUnwrapped > 0 && decayConstantUnwrapped > type(int256).max / targetTimeUnwrapped
58 return type(uint256).max;
70 return type(uint256).max;
82 if (targetPriceInt > type(int256).max / expResult)
83 return type(uint256).max;
89 if (targetPriceInt > type(int256).max / extraPrecisionExpResult)
90 return type(uint256).max;
282 asset_.safeApprove(address(yieldVault_), type(uint256).max);
376 return _isVaultCollateralized() ? type(uint96).max : 0;
384 return _isVaultCollateralized() ? type(uint96).max : 0;
677 _asset.safeApprove(address(liquidationPair_), type(uint256).max);
Placing if statements and require() statements that check input arguments at the top of a function can be an effective way to optimize gas costs in Solidity contracts. When an if statement or require() statement fails, the function immediately exits and all subsequent code is not executed. Therefore, placing these statements at the top of a function can reduce the amount of gas consumed by the function, as any unnecessary computation that would have been performed after the failed check is avoided.
473 function _getPreviousOrAtObservation( uint32 PERIOD_OFFSET, ObservationLib.Observation[MAX_CARDINALITY] storage _observations, AccountDetails memory _accountDetails, uint32 _targetTime ) private view returns (ObservationLib.Observation memory prevOrAtObservation) { uint32 currentTime = uint32(block.timestamp); uint16 oldestTwabIndex; uint16 newestTwabIndex; // If there are no observations, return a zeroed observation if (_accountDetails.cardinality == 0) { return ObservationLib.Observation({ cumulativeBalance: 0, balance: 0, timestamp: PERIOD_OFFSET }); }
612 function _getTierLiquidityToReclaim( uint8 _numberOfTiers, uint8 _nextNumberOfTiers, UD60x18 _prizeTokenPerShare ) internal view returns (uint256) { UD60x18 reclaimedLiquidity; // need to redistribute to the canary tier and any new tiers (if expanding) uint8 start; uint8 end; // if we are expanding, need to reset the canary tier and all of the new tiers if (_nextNumberOfTiers < _numberOfTiers) { start = _nextNumberOfTiers - 1; end = _numberOfTiers; } else { // just reset the canary tier start = _numberOfTiers - 1; end = _numberOfTiers; } for (uint8 i = start; i < end; i++) { Tier memory tierLiquidity = _tiers[i]; uint8 shares = _computeShares(i, _numberOfTiers); UD60x18 liq = _getTierRemainingLiquidity( shares, fromUD34x4toUD60x18(tierLiquidity.prizeTokenPerShare), _prizeTokenPerShare ); reclaimedLiquidity = reclaimedLiquidity.add(liq); } return fromUD60x18(reclaimedLiquidity); }
561 function _getNextOrNewestObservation( uint32 PERIOD_OFFSET, ObservationLib.Observation[MAX_CARDINALITY] storage _observations, AccountDetails memory _accountDetails, uint32 _targetTime ) private view returns (ObservationLib.Observation memory nextOrNewestObservation) { uint32 currentTime = uint32(block.timestamp); uint16 oldestTwabIndex; // If there are no observations, return a zeroed observation if (_accountDetails.cardinality == 0) { return ObservationLib.Observation({ cumulativeBalance: 0, balance: 0, timestamp: PERIOD_OFFSET }); }
550 function liquidate( address _account, address _tokenIn, uint256 _amountIn, address _tokenOut, uint256 _amountOut ) public virtual override returns (bool) { _requireVaultCollateralized(); if (msg.sender != address(_liquidationPair)) revert LiquidationCallerNotLP(msg.sender, address(_liquidationPair)); if (_tokenIn != address(_prizePool.prizeToken())) revert LiquidationTokenInNotPrizeToken(_tokenIn, address(_prizePool.prizeToken())); if (_tokenOut != address(this)) revert LiquidationTokenOutNotVaultShare(_tokenOut, address(this)); if (_amountOut == 0) revert LiquidationAmountOutZero(); uint256 _liquidableYield = _liquidatableBalanceOf(_tokenOut); if (_amountOut > _liquidableYield) revert LiquidationAmountOutGTYield(_amountOut, _liquidableYield); _prizePool.contributePrizeTokens(address(this), _amountIn); if (_yieldFeePercentage != 0) { _increaseYieldFeeBalance( (_amountOut * FEE_PRECISION) / (FEE_PRECISION - _yieldFeePercentage) - _amountOut ); } uint256 _vaultAssets = IERC20(asset()).balanceOf(address(this)); if (_vaultAssets != 0 && _amountOut >= _vaultAssets) { _yieldVault.deposit(_vaultAssets, address(this)); } _mint(_account, _amountOut); return true; }
Using assembly to write address storage values can be an effective way to optimize gas costs in Solidity contracts.
299 function setDrawManager(address _drawManager) external { if (drawManager != address(0)) { revert DrawManagerAlreadySet(); } drawManager = _drawManager;
Using hardcoded addresses instead of address(this) can be an effective way to optimize gas costs in Solidity contracts. In Solidity, address(this) is used to refer to the address of the current contract. However, calling address(this) requires the EVM to execute a CALL opcode, which can be expensive in terms of gas cost. By hardcoding the contract's address in the contract code, the gas cost of executing a CALL opcode can be avoided. This is because the EVM can simply use the hardcoded address instead of executing a CALL opcode to retrieve the contract's address.
335 _permit(IERC20Permit(asset()), msg.sender, address(this), _assets, _deadline, _v, _r, _s);
468 _permit(IERC20Permit(asset()), msg.sender, address(this), _assets, _deadline, _v, _r, _s);
502 _permit(IERC20Permit(asset()), msg.sender, address(this), _assets, _deadline, _v, _r, _s);
562 if (_tokenOut != address(this))
563 revert LiquidationTokenOutNotVaultShare(_tokenOut, address(this));
570 _prizePool.contributePrizeTokens(address(this), _amountIn);
578 uint256 _vaultAssets = IERC20(asset()).balanceOf(address(this));
581 _yieldVault.deposit(_vaultAssets, address(this));
801 return _yieldVault.maxWithdraw(address(this)) + super.totalAssets();
809 return _twabController.totalSupply(address(this));
830 if (_token != address(this)) revert LiquidationTokenOutNotVaultShare(_token, address(this));
932 uint256 _vaultAssets = _asset.balanceOf(address(this));
954 address(this),
959 _yieldVault.deposit(_assets, address(this));
986 _twabController.delegateOf(address(this), _receiver) != _twabController.SPONSORSHIP_ADDRESS()
1026 _yieldVault.withdraw(_assets, address(this), address(this));
1176 uint256 _withdrawableAssets = _yieldVault.maxWithdraw(address(this));
83 emit NewFactoryVault(_vault, VaultFactory(address(this)));
110 return uint256(keccak256(abi.encode(_user, _tier, _prizeIndex, _winningRandomNumber)));
Using XOR (^) and OR (|) bitwise equivalents can be an effective way to optimize gas costs in Solidity contracts. In Solidity, the XOR and OR bitwise operators can be expensive in terms of gas cost, particularly for large values. This is because these operators require a lot of computational steps to perform the bitwise operation.
545 (_to == address(0) && _fromDelegate != SPONSORSHIP_ADDRESS) ||
575 (_from == address(0) && _toDelegate != SPONSORSHIP_ADDRESS) ||
Not applying the same value to state variables can be an effective way to optimize gas costs in Solidity contracts.
369 claimCount = 0; canaryClaimCount = 0; largestTierClaimed = 0;
24 address public constant SPONSORSHIP_ADDRESS = address(1);
Using calldata instead of memory for read-only arguments in external functions can be an effective way to optimize gas costs in Solidity contracts. When an external function is called, its arguments are initially stored in calldata, which is a read-only data location. In contrast, memory is a data location where the function can read and write data.
653 function setHooks(VaultHooks memory hooks) external
57 string memory _name,
58 string memory _symbol,
Avoiding emitting storage values can be an effective way to optimize gas costs in Solidity contracts. In Solidity, emitting events with storage values can be expensive in terms of gas cost, particularly if the storage values are large or complex data types. This is because emitting an event with storage values requires the EVM to perform a copy operation to memory, which can be costly in terms of gas.
375 emit DrawClosed( lastClosedDrawId, winningRandomNumber_, _numTiers, _nextNumberOfTiers, _reserve, prizeTokenPerShare, _lastClosedDrawStartedAt );
692 emit ObservationRecorded( _vault, _user, _account.details.balance, _account.details.delegateBalance, _isNewObservation, _observation );
735 emit ObservationRecorded( _vault, _user, _account.details.balance, _account.details.delegateBalance, _isNewObservation, _observation );
777 emit TotalSupplyObservationRecorded( _vault, _account.details.balance, _account.details.delegateBalance, _isNewObservation, _observation );
811 emit TotalSupplyObservationRecorded( _vault, _account.details.balance, _account.details.delegateBalance, _isNewObservation, _observation );
1111 emit RecordedExchangeRate(_lastRecordedExchangeRate);
Using assembly to perform efficient back-to-back calls can be an effective way to optimize gas costs in Solidity contracts. In Solidity, when a contract calls another contract, it typically uses the CALL opcode. However, using CALL can be expensive in terms of gas cost, particularly if the contract frequently makes back-to-back calls to the same contract.
665 function setLiquidationPair( LiquidationPair liquidationPair_ ) external onlyOwner returns (address) { if (address(liquidationPair_) == address(0)) revert LPZeroAddress(); IERC20 _asset = IERC20(asset()); address _previousLiquidationPair = address(_liquidationPair); if (_previousLiquidationPair != address(0)) { _asset.safeApprove(_previousLiquidationPair, 0); } _asset.safeApprove(address(liquidationPair_), type(uint256).max); _liquidationPair = liquidationPair_;
169 function _computeMaxFee(uint8 _tier, uint8 _numTiers) internal view returns (uint256) { uint8 _canaryTier = _numTiers - 1; if (_tier != _canaryTier) { // canary tier return _computeMaxFee(prizePool.getTierPrizeSize(_canaryTier - 1)); } else { return _computeMaxFee(prizePool.getTierPrizeSize(_canaryTier)); }
Using a storage pointer when making multiple accesses to a mapping or array can be an effective way to optimize gas costs in Solidity contracts.
483 function withdrawClaimRewards(address _to, uint256 _amount) external { uint256 _available = claimerRewards[msg.sender]; if (_amount > _available) { revert InsufficientRewardsError(_amount, _available); } claimerRewards[msg.sender] -= _amount; _transfer(_to, _amount)
Caching state variables outside of a loop can be an effective way to optimize gas costs in Solidity contracts. In Solidity, reading state variables from storage can be expensive in terms of gas cost, particularly if the state variable is large or complex. This is because reading from storage requires loading data from the storage to memory, which can be slow and costly.
361 for (uint8 i = start; i < end; i++) { _tiers[i] = Tier({ drawId: closedDrawId, prizeTokenPerShare: prizeTokenPerShare, prizeSize: uint96( _computePrizeSize(i, _nextNumberOfTiers, _prizeTokenPerShare, newPrizeTokenPerShare) ) });
Refactoring duplicated require() or revert() checks to a modifier or function can be an effective way to optimize gas costs in Solidity contracts. In Solidity, require() and revert() are used to validate input and state conditions and to revert the transaction if the conditions are not met. However, repeating the same require() or revert() check multiple times in a contract can be expensive in terms of gas cost, particularly if the check is complex or involves accessing storage.
320 if (_numberOfTiers < MINIMUM_NUMBER_OF_TIERS) { revert NumberOfTiersLessThanMinimum(_numberOfTiers); } if (_numberOfTiers > MAXIMUM_NUMBER_OF_TIERS) { revert NumberOfTiersGreaterThanMaximum(_numberOfTiers); }
#0 - c4-judge
2023-07-18T18:59:39Z
Picodes marked the issue as grade-a
#1 - c4-sponsor
2023-07-20T20:42:33Z
asselstine marked the issue as sponsor confirmed
#2 - PierrickGT
2023-09-07T23:12:51Z
G-01: handled by safeTransfer
G-02: we do access the mappings directly
G-03: we would lose in code legibility
G-04: we won't make this change
G-05: useful to have a lib that we can use later for other use cases
G-06: does not save any gas
G-07: has already been fixed
G-08: we would lose in code legibility
G-09: does not save any gas
G-10: encodePacked
can't be used
G-11, 12: we would lose in code legibility
G-13: constants can't be private
G-14: memory needs to be used
G-15: has been fixed
G-16: we would lose in code legibility
G-17: irrelevant suggestion
G-18, 19: has been removed