Open code423n4 opened 2 years ago
Very high quality report, may implement some of your suggestions. Thank you!
Implemented most of your suggestions in https://github.com/jpegd/core/pull/21. Custom errors have not been implemented as it would be too big of a change at this point and doing it properly may cause unexpected behaviour/bugs due to the codebase changing too much.
Table of Contents:
NFTEscrow._executeTransfer()
: Cheap Contract Deployment Through ClonesLPFarming.newEpoch()
: L128 and L133 should be unchecked due to parent if/else conditionLPFarming.withdraw()
: L248 should be unchecked due to L243LPFarming._withdrawReward()
:poolInfo[_pid].accRewardPerShare
should get cachedyVaultLPFarming.withdraw()
: L124 should be unchecked due to L119yVaultLPFarming._withdrawReward()
:accRewardPerShare
should get cachedJPEGLock.unlock()
: usestorage
instead of copying struct in memory L69FungibleAssetVaultForDAO._collateralPriceUsd()
:oracle
should get cachedFungibleAssetVaultForDAO._collateralPriceUsd()
: return statement should be uncheckedFungibleAssetVaultForDAO.deposit()
:collateralAsset
should get cachedFungibleAssetVaultForDAO.repay)
: L184 should be unchecked due to L182FungibleAssetVaultForDAO.withdraw()
: L196 should be unchecked due to L194FungibleAssetVaultForDAO.withdraw()
:collateralAmount
should get cachedNFTVault._normalizeAggregatorAnswer()
: return statement should be uncheckedNFTVault._calculateAdditionalInterest()
:totalDebtAmount
should get cachedNFTVault.sol
:struct PositionPreview
can be tightly packed to save 1 storage slotNFTVault.showPosition()
: L659 should be unchecked due to L649NFTVault.showPosition()
:positions[_nftIndex].liquidatedAt
should get cachedNFTVault.showPosition()
: Help the optimizer by saving a storage variable's reference instead of repeatedly fetching it (positions[_nftIndex]
)NFTVault.borrow()
:totalDebtPortion
should get cachedNFTVault.repay()
: L781 should be unchecked due to ternary operatorNFTVault.repay()
:totalDebtPortion
andtotalDebtAmount
should get cachedController.setStrategy()
: boolean comparison L87StrategyPUSDConvex.balanceOfJPEG()
:jpeg
should get cachedStrategyPUSDConvex.balanceOfJPEG()
: use areturn
statement instead ofbreak
StrategyPUSDConvex.withdraw()
: L281 should be unchecked due to L279StrategyPUSDConvex.harvest()
: L362 should be unchecked due to L359-L360 and how performanceFee is set L183yVault.earn()
:token
andcontroller
should get cachedyVault.withdraw()
: L178 should be unchecked due to L177yVault.withdraw()
:token
should get cachedyVault.withdrawJPEG()
:farm
should get cached> 0
is less efficient than!= 0
for unsigned integers (with proof)>=
is cheaper than>
++i
costs less gas compared toi++
ori += 1
calldata
instead ofmemory
NFTEscrow._executeTransfer()
: Cheap Contract Deployment Through ClonesSee
@audit
tag:There's a way to save a significant amount of gas on deployment using Clones: https://www.youtube.com/watch?v=3Mw-pMmJ7TA .
This is a solution that was adopted, as an example, by Porter Finance. They realized that deploying using clones was 10x cheaper:
I suggest applying a similar pattern.
LPFarming.newEpoch()
: L128 and L133 should be unchecked due to parent if/else conditionSolidity version 0.8+ comes with implicit overflow and underflow checks on unsigned integers. When an overflow or an underflow isn't possible (as an example, when a comparison is made before the arithmetic operation), some gas can be saved by using an
unchecked
block: https://docs.soliditylang.org/en/v0.8.10/control-structures.html#checked-or-unchecked-arithmeticI suggest wrapping with an
unchecked
block here (see@audit
tag):LPFarming.withdraw()
: L248 should be unchecked due to L243See
@audit
tag:LPFarming._withdrawReward()
:poolInfo[_pid].accRewardPerShare
should get cachedThe code can be optimized by minimising the number of SLOADs. SLOADs are expensive (100 gas) compared to MLOADs/MSTOREs (3 gas). Here, the storage value should get cached in memory (see the
@audit
tags for further details):yVaultLPFarming.withdraw()
: L124 should be unchecked due to L119See
@audit
tag:yVaultLPFarming._withdrawReward()
:accRewardPerShare
should get cachedSee
@audit
tags:JPEGLock.unlock()
: usestorage
instead of copying struct in memory L69See
@audit
tag:Here, a copy in memory is costing 3 SLOADs and 3 MSTORES. The, 2 variables are only read once through MLOAD (
position.owner
andposition.unlockAt
) and one is read twice (position.lockAmount
). I suggest replacing thememory
keyword withstorage
at L69 and only copyingposition.lockAmount
in memory.FungibleAssetVaultForDAO._collateralPriceUsd()
:oracle
should get cachedSee
@audit
tags:FungibleAssetVaultForDAO._collateralPriceUsd()
: return statement should be uncheckedSee
@audit
tag:Due to the ternary condition and the fact that
int256 answer = oracle.latestAnswer();
, the return statement can't underflow and should be unchecked.FungibleAssetVaultForDAO.deposit()
:collateralAsset
should get cachedSee
@audit
tags:FungibleAssetVaultForDAO.repay)
: L184 should be unchecked due to L182See
@audit
tag:FungibleAssetVaultForDAO.withdraw()
: L196 should be unchecked due to L194See
@audit
tag:FungibleAssetVaultForDAO.withdraw()
:collateralAmount
should get cachedSee
@audit
tags:NFTVault._normalizeAggregatorAnswer()
: return statement should be uncheckedSee
@audit
tag:NFTVault._calculateAdditionalInterest()
:totalDebtAmount
should get cachedSee
@audit
tags:NFTVault.sol
:struct PositionPreview
can be tightly packed to save 1 storage slotFrom (see
@audit
tags):To:
NFTVault.showPosition()
: L659 should be unchecked due to L649See
@audit
tag:NFTVault.showPosition()
:positions[_nftIndex].liquidatedAt
should get cachedSee
@audit
tags:NFTVault.showPosition()
: Help the optimizer by saving a storage variable's reference instead of repeatedly fetching it (positions[_nftIndex]
)To help the optimizer, declare a
storage
type variable and use it instead of repeatedly fetching the reference in a map or an array.The effect can be quite significant.
Here, instead of repeatedly calling
positions[_nftIndex]
, save its reference like this:Position storage _position = positions[_nftIndex]
and use it.Impacted lines (see
@audit
tags):This practice already exists in the solution, as seen in
NFTVault.borrow()
:NFTVault.borrow()
:totalDebtPortion
should get cachedSee
@audit
tags:NFTVault.repay()
: L781 should be unchecked due to ternary operatorSee
@audit
tag:NFTVault.repay()
:totalDebtPortion
andtotalDebtAmount
should get cachedSee
@audit
tags:Controller.setStrategy()
: boolean comparison L87Comparing to a constant (
true
orfalse
) is a bit more expensive than directly checking the returned boolean value. I suggest usingif(directValue)
instead ofif(directValue == true)
andif(!directValue)
instead ofif(directValue == false)
here (see@audit
tag):StrategyPUSDConvex.balanceOfJPEG()
:jpeg
should get cachedSee
@audit
tags:StrategyPUSDConvex.balanceOfJPEG()
: use areturn
statement instead ofbreak
See
@audit
tag:Here, instead of making a memory operation with
availableBalance += extraReward.earned();
and then usingbreak;
before returning the memory variableavailableBalance
, it would've been more optimized to just returnavailableBalance + extraReward.earned()
:StrategyPUSDConvex.withdraw()
: L281 should be unchecked due to L279See
@audit
tag:StrategyPUSDConvex.harvest()
: L362 should be unchecked due to L359-L360 and how performanceFee is set L183See
@audit
tags:yVault.earn()
:token
andcontroller
should get cachedSee
@audit
tags:yVault.withdraw()
: L178 should be unchecked due to L177See
@audit
tag:yVault.withdraw()
:token
should get cachedSee
@audit
tags:yVault.withdrawJPEG()
:farm
should get cachedSee
@audit
tags:Upgrade pragma to at least 0.8.4
Across the whole solution, the declared pragma is
^0.8.0
.Using newer compiler versions and the optimizer give gas optimizations. Also, additional safety checks are available for free.
The advantages here are:
Consider upgrading pragma to at least 0.8.4:
No need to explicitly initialize variables with default values
If a variable is not set/initialized, it is assumed to have the default value (
0
foruint
,false
forbool
,address(0)
for address...). Explicitly initializing it with its default value is an anti-pattern and wastes gas.As an example:
for (uint256 i = 0; i < numIterations; ++i) {
should be replaced withfor (uint256 i; i < numIterations; ++i) {
Instances include:
I suggest removing explicit initializations for default values.
> 0
is less efficient than!= 0
for unsigned integers (with proof)!= 0
costs less gas compared to> 0
for unsigned integers inrequire
statements with the optimizer enabled (6 gas)Proof: While it may seem that
> 0
is cheaper than!=
, this is only true without the optimizer enabled and outside a require statement. If you enable the optimizer at 10k AND you're in arequire
statement, this will save gas. You can see this tweet for more proofs: https://twitter.com/gzeon/status/1485428085885640706I suggest changing
> 0
with!= 0
here:Also, please enable the Optimizer.
>=
is cheaper than>
Strict inequalities (
>
) are more expensive than non-strict ones (>=
). This is due to some supplementary checks (ISZERO, 3 gas)I suggest using
>=
instead of>
to avoid some opcodes here:An array's length should be cached to save gas in for-loops
Reading array length at each iteration of the loop takes 6 gas (3 for mload and 3 to place memory_offset) in the stack.
Caching the array length in the stack saves around 3 gas per iteration.
Here, I suggest storing the array's length in a variable before the for-loop, and use it instead:
This is already done here:
++i
costs less gas compared toi++
ori += 1
++i
costs less gas compared toi++
ori += 1
for unsigned integer, as pre-increment is cheaper (about 5 gas per iteration). This statement is true even with the optimizer enabled.i++
incrementsi
and returns the initial value ofi
. Which means:But
++i
returns the actual incremented value:In the first case, the compiler has to create a temporary variable (when used) for returning
1
instead of2
Instances include:
I suggest using
++i
instead ofi++
to increment the value of an uint variable.This is already done here:
Increments can be unchecked
In Solidity 0.8+, there's a default overflow check on unsigned integers. It's possible to uncheck this in for-loops and save some gas at each iteration, but at the cost of some code readability, as this uncheck cannot be made inline.
ethereum/solidity#10695
Instances include:
The code would go from:
to:
The risk of overflow is inexistant for a
uint256
here.Use
calldata
instead ofmemory
When arguments are read-only on external functions, the data location should be
calldata
:Consider making some constants as non-public to save gas
Reducing from
public
toprivate
orinternal
can save gas when a constant isn't used outside of its contract. I suggest changing the visibility frompublic
tointernal
orprivate
here:Public functions to external
The following functions could be set external to save gas and improve code quality. External call cost is less expensive than of public functions.
Reduce the size of error messages (Long revert Strings)
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.
Revert strings > 32 bytes:
I suggest shortening the revert strings to fit in 32 bytes, or using custom errors as described next.
Use Custom Errors instead of Revert Strings to save Gas
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: https://blog.soliditylang.org/2021/04/21/custom-errors/:
Custom errors are defined using the
error
statement, which can be used inside and outside of contracts (including interfaces and libraries).Instances include:
I suggest replacing revert strings with custom errors.