The Yieldoor Gas Optimizoor
Learn to write gas-efficient Solidity code using a real-world example!
Decorated auditor deadrosesxyz recently put on their developer hat to create a leveraged yield-farming protocol Yieldoor whose code provided ample opportunity for gas optimization. Many articles have been written listing common gas optimization techniques but this real-world case study illustrates 15.43% overall gas savings achieved via refactoring using a step-by-step mental process for optimizing core protocol functions.
Looking to work with me for a Gas Audit on your protocol? DMs are open!
We’ll use the publicly available contest source code and start by focusing on the most interesting, complicated and mission-critical liquidation function Leverager::liquidatePosition.
Measuring Gas Cost
Foundry provides a number of in-built tools to measure gas cost including:
gas reports via
forge test --gas-reportafter configuration infoundry.tomlfor contract deployment and function call coststest function snapshots via
forge snapshotto measure the gas costs of the entire unit test suitegas section snapshots via cheatcodes to measure gas costs of arbitrary code segments inside unit tests
We’ll fork the original source code and use the second and third options by:
editing
test/Leverager.t.solto add atest_Gas_*prefix around the three tests which callLeverager::liquidatePositionchanging
test/BaseTest.solto reduce fuzz runs to 100 by editing the relevant comments to/// forge-config: default.fuzz.runs = 100to speed up the iterative optimization cycleadd “gas section snapshots” inside the tests functions around the calls to
Leverager::liquidatePosition:
vm.startSnapshotGas("lendingPoolLiquidation");
ILeverager(leverager).liquidatePosition(liqParams);
vm.stopSnapshotGas();
vm.startSnapshotGas("liquidateWithSwap");
ILeverager(leverager).liquidatePosition(liqParams);
vm.stopSnapshotGas();
vm.startSnapshotGas("liquidationNoSwap");
ILeverager(leverager).liquidatePosition(liqParams);
vm.stopSnapshotGas();
get the initial “test function snapshots” by running
forge snapshot --fork-url ETH_RPC_URL --fork-block-number 20956198which generates a new file.gas-snapshotrecording gas costs for all unit tests and overall gas cost (second option above)get the initial “gas section snapshot" by running
forge test --gas-snapshot-check=true --match-test test_Gas --fork-url ETH_RPC_URL --fork-block-number 20956198which generates a filesnapshots/LeveragerTest.jsonshowing the initial gas cost for the manual snapshots wrapping calls toLeverager::liquidatePosition(third option above):
{
"lendingPoolLiquidation": "575367",
"liquidateWithSwap": "618080",
"liquidationNoSwap": "439622"
}
Back up these files to .gas-snapshot.original and snapshots/LeveragerTest.original.json to maintain a starting point record for easy comparison at the end. After any code change we can then run:
forge snapshot --diff --fork-url ETH_RPC_URL --fork-block-number 20956198to see whether the change reduced the per-test and overall all-tests gas costs. To override the existing.gas-snapshotfile simply run it without the-diffflag. Important: all tests must pass for a new.gas-snapshotfile to be generatedforge test --gas-snapshot-check=true --match-test test_Gas --fork-url ETH_RPC_URL --fork-block-number 20956198to see whether the change reduced the gas costs of the manual snapshots. To override the existing file simply remove the old file before running this command viarm snapshots/LeveragerTest.json
Cache Identical Storage Reads
Generally the easiest and most “bang-for-buck” gas optimization is to cache identical storage reads to prevent re-reading the same value from storage multiple times. This can be resolved by:
if a contract is not upgradeable and a storage location is only ever set once in the constructor, the storage slot should be declared
immutableotherwise the storage slot can be read once, cached, and passed as a parameter where it is required both in the current function and to child functions
Inspecting Leverager::liquidatePosition shows some quick and easy wins:
feeRecipientis read twice at L328,329swapRouteris read two to four times at L354,355,363,364
Attempting to simply cache feeReceipient fails due to a “stack too deep” error. When caching storage it is more gas efficient to use local variables within a function but if that is not possible due to “stack too deep” errors then:
declare a struct
LiquidateContextto cache storage reads for the liquidation function:struct LiquidateContext { address feeRecipient; address swapRouter; }eliminate a local variable that is only read once by passing the result of
IVault(up.vault).twapPrice()directly to_calculateTokenValuesdeclare a new local variable
LiquidateContext memory ctxinside the liquidation function to store the cached storage reads
The changes and result are seen in commit 1661887; the gas cost reduced for one test but grew for the other two, though this is just the beginning where we are laying the groundwork for further optimizations.
It also makes sense to consolidate the Position memory pos variable inside our LiquidateContext struct and create a new _getLiquidateContext internal function to return the struct:
struct LiquidateContext {
// read from positions[liqParams.id]
Position pos;
address feeRecipient;
address swapRouter;
}
function _getLiquidateContext(uint256 _id) internal view returns(LiquidateContext memory ctx) {
ctx.pos = positions[_id];
}
/// @notice Liquidates a certain leveraged position.
/// @dev Check the ILeverager contract for comments on all LiquidateParams arguments
/// @dev Does not support partial liquidations
/// @dev Collects fees first, in order to properly calculate whether a position is actually liquidateable
function liquidatePosition(LiquidateParams calldata liqParams) external collectFees(liqParams.id) nonReentrant {
// read everything required from storage once
LiquidateContext memory ctx = _getLiquidateContext(liqParams.id);
collectFees Modifier Reads Identical Storage Slots
Next examine the modifiers to see if they re-read the same storage slots; it is common for developers to write modifiers that read the same storage slots as the function body resulting in inefficient code. In this case there is only one modifier of interest collectFees which copies the entire Position into memory again resulting in 10 identical storage reads:
modifier collectFees(uint256 _id) {
// @gas 10 identical storage reads since `Position` already read from
// storage to memory inside `liquidatePosition` function
Position memory up = positions[_id];
address strat = IVault(up.vault).strategy();
IStrategy(strat).collectFees();
_;
}
Since the collectFees modifier is only called by the liquidatePosition function we can simply inline it to save 10 storage reads:
function liquidatePosition(LiquidateParams calldata liqParams) external nonReentrant {
// read everything required from storage once
LiquidateContext memory ctx = _getLiquidateContext(liqParams.id);
// must collect fees before checking if a position is liquidatable
IStrategy(IVault(ctx.pos.vault).strategy()).collectFees();
With this change all three tests are now more gas efficient than the original code:
$ more snapshots/LeveragerTest.json
{
"lendingPoolLiquidation": "574354",
"liquidateWithSwap": "616824",
"liquidationNoSwap": "438617"
}
$ more snapshots/LeveragerTest.original.json
{
"lendingPoolLiquidation": "575367",
"liquidateWithSwap": "618080",
"liquidationNoSwap": "439622"
}
isLiquidatable Function Reads Identical Storage Slots
Having finished with the modifiers, start working through the function body looking for child functions. The first encountered function is isLiquidatable; looking at its implementation it also copies the entire Position from storage into memory again resulting in another 10 identical storage reads:
function isLiquidatable(uint256 _id) public view returns (bool liquidatable) {
// another 10 identical storage reads when called by `liquidatePosition`
Position memory pos = positions[_id];
In the original code during every liquidation transaction Position was being copied into memory 3 times for 30 identical storage reads! Solve this by introducing an internal helper function _isLiquidatable which:
takes as input the cached
LiquidateContext memory ctxcan be called by both
liquidatePositionandisLiquidatable
The changes are seen in commits (143cec6, e960acf):
function liquidatePosition(LiquidateParams calldata liqParams) external nonReentrant {
// read everything required from storage once
LiquidateContext memory ctx = _getLiquidateContext(liqParams.id);
// must collect fees before checking if a position is liquidatable
IStrategy(IVault(ctx.pos.vault).strategy()).collectFees();
require(_isLiquidateable(ctx), "isnt liquidateable");
function isLiquidateable(uint256 _id) public view returns (bool liquidateable) {
liquidateable = _isLiquidateable(_getLiquidateContext(_id));
}
// gas: internal helper function saves reading positions[_id] multiple times during liquidations
function _isLiquidateable(LiquidateContext memory ctx) internal view returns (bool liquidateable) {
VaultParams memory vp = vaultParams[ctx.pos.vault];
uint256 vaultSupply = IVault(ctx.pos.vault).totalSupply();
// Assuming a price of X, a LP position has its lowest value when the pool price is exactly X.
// Any price movement, would actually overvalue the position.
// For this reason, attackers cannot force a position to become liquidateable with a swap.
(uint256 vaultBal0, uint256 vaultBal1) = IVault(ctx.pos.vault).balances();
uint256 userBal0 = ctx.pos.shares * vaultBal0 / vaultSupply;
uint256 userBal1 = ctx.pos.shares * vaultBal1 / vaultSupply;
uint256 price = IVault(ctx.pos.vault).twapPrice();
uint256 totalValueUSD = _calculateTokenValues(ctx.pos.token0, ctx.pos.token1, userBal0, userBal1, price);
uint256 bPrice = IPriceFeed(pricefeed).getPrice(ctx.pos.denomination);
uint256 totalDenom = totalValueUSD * (10 ** ERC20(ctx.pos.denomination).decimals()) / bPrice;
uint256 bIndex = ILendingPool(lendingPool).getCurrentBorrowingIndex(ctx.pos.denomination);
uint256 owedAmount = ctx.pos.borrowedAmount * bIndex / ctx.pos.borrowedIndex;
/// here we make a calculation what would be the necessary collateral
/// if we had the same borrowed amount, but at max leverage. Check docs for better explanation why.
uint256 base = owedAmount * 1e18 / (vp.maxTimesLeverage - 1e18);
base = base < ctx.pos.initCollateralValue ? base : ctx.pos.initCollateralValue;
if (owedAmount > totalDenom || totalDenom - owedAmount < vp.minCollateralPct * base / 1e18) return true;
}
This results in further gas savings:
- [lendingPoolLiquidation] 574354 → 572916
- [liquidateWithSwap] 616824 → 615384
- [liquidationNoSwap] 438617 → 437178
_isLiquidatable Function Copies All VaultParams
Next inspecting the _isLiquidatable function observe that it:
copies
VaultParamsfrom storage to memoryonly uses
maxTimesLeverageandminCollateralPctfromVaultParams
Since VaultParams is defined as:
struct VaultParams {
bool leverageEnabled;
uint256 maxUsdLeverage;
uint256 maxTimesLeverage;
uint256 minCollateralPct;
uint256 maxCumulativeBorrowedUSD;
uint256 currBorrowedUSD;
}
This means that _isLiquidatable is performing 4 unnecessary storage reads every time it copies VaultParams from storage to memory! Fix this by:
expanding the
LiquidateContextstruct to contain the additional fieldsadding code to
_getLiquidateContextto cache those fieldschanging
_isLiquidatableto use the cached fields
// cache storage reads in memory
struct LiquidateContext {
// read from positions[liqParams.id]
Position pos;
// read from vaultParams[pos.vault]
uint256 maxTimesLeverage;
uint256 minCollateralPct;
address feeRecipient;
address swapRouter;
}
function _getLiquidateContext(uint256 _id) internal view returns(LiquidateContext memory ctx) {
ctx.pos = positions[_id];
VaultParams storage vpRef = vaultParams[ctx.pos.vault];
(ctx.maxTimesLeverage, ctx.minCollateralPct)
= (vpRef.maxTimesLeverage, vpRef.minCollateralPct);
}
function _isLiquidatable(LiquidateContext memory ctx) internal view returns (bool liquidateable) {
/* snip */
uint256 base = owedAmount * 1e18 / (ctx.maxTimesLeverage - 1e18);
base = base < ctx.pos.initCollateralValue ? base : ctx.pos.initCollateralValue;
if (owedAmount > totalDenom || totalDenom - owedAmount < ctx.minCollateralPct * base / 1e18) return true;
Resulting in further gas savings:
- [lendingPoolLiquidation] 572916 → 572396
- [liquidateWithSwap] 615384 → 614864
- [liquidationNoSwap] 437178 → 436658
pricefeed Identical Reads In liquidatePosition & _isLiquidatable
A common mistake developers make is re-reading identical values from storage in child functions called by parent functions; in this case:
liquidatePositioncalls_isLiquidatablewhich readspricefeedfrom storage into memoryliquidatePositionitself readspricefeedagain, resulting in a duplicate storage read
Fix this by:
adding
priceFeedtostruct LiquidateContextreading it once inside
getLiquidateContextreading it from the cached copy in
_isLiquidatableandliquidatePosition
This again reduces the gas costs across the board:
- [lendingPoolLiquidation] 572396 → 572345
- [liquidateWithSwap] 614864 → 614813
- [liquidationNoSwap] 436658 → 436608
pricefeed Identical Reads In _calculateTokenValues
Continuing to examine the _isLiquidatable child function, observe it:
calls
_calculateTokenValueswhich is also called by the main functionliquidatePosition_calculateTokenValuesreads the same identical value ofpricefeedfrom storage up to 4 times:
if (IPriceFeed(pricefeed).hasPriceFeed(token0)) {
chPrice0 = IPriceFeed(pricefeed).getPrice(token0);
usdValue += amount0 * chPrice0 / decimals0;
}
if (IPriceFeed(pricefeed).hasPriceFeed(token1)) {
chPrice1 = IPriceFeed(pricefeed).getPrice(token1);
usdValue += amount1 * chPrice1 / decimals1;
}
This is easily fixed by changing _calculateTokenValues to take the cached pricefeed as input, resulting in even greater gas savings:
- [lendingPoolLiquidation] 572345 → 571293
- [liquidateWithSwap] 614813 → 613761
- [liquidationNoSwap] 436608 → 435556
Leverager Further Optimizations
Further but potentially unsafe gas optimizations could be made by:
caching the result of external calls such as
ILendingPool(lendingPool).getCurrentBorrowingIndex(ctx.pos.denomination)using this to cache the calculated
owedAmount
Ideally duplicate computations and external calls should be cached if it is safe to do so, but this would be unsafe if it is intended that later duplicate computations and external calls can return different values. Caching is only a safe optimization when the same value is read from storage (or received from an external call) multiple times.
Additional safe gas optimizations made to other Leverager functions include:
don’t read new position from storage in
openLeveragePositionpass cached price feed when building new liquidation context in
openLeveragedPositionrevert if new position is liquidatable prior to writing to storage
read only required
VaultParamsand fail fast in_checkWithinLimits
Leverager High Finding - feeRecipient Never Set
One heuristic to look for when doing gas optimizations in non-upgradeable contracts is storage slots which can be set to immutable if they are only set once in the constructor. When checking all Leverager storage slots I found that feeRecipient is never set anywhere meaning that either:
all the fees will be sent to the zero address for tokens which allow this, or
liquidation will be bricked for tokens which revert when sending tokens to the zero address
Strategy High Finding - Incorrect Upper Tick in collectFees
While looking for identical storage reads to cache I noticed this code block where the third call to collectPositionFees was re-reading mainPosition.tickUpper from storage when it likely shouldn’t be:
if (mainPosition.liquidity != 0) collectPositionFees(mainPosition.tickLower, mainPosition.tickUpper);
if (secondaryPosition.liquidity != 0) {
collectPositionFees(secondaryPosition.tickLower, secondaryPosition.tickUpper);
}
if (ongoingVestingPosition) {
// @audit `mainPosition.tickUpper` likely not intended to be read from
// storage again here, should be `vestPosition.tickUpper`
collectPositionFees(vestPosition.tickLower, mainPosition.tickUpper);
}
The impact is that Strategy::collectFees may revert or fail to collect the correct fees since it is passing the wrong upper tick.
LendingPool Gas Optimizations
Similar gas optimizations can be made to the LendingPool contract including:
Strategy Gas Optimizations
The Strategy contract can also benefit from the following gas optimizations:
use
immutablefor storage slots only set once inconstructorcache
ongoingVestingPosition,protocolFeeandfeeRecipientincollectFeesuse cached
VestingPositionfor final check in_withdrawPartOfVestingPositionin
rebalancecache updated ticks and use when adding liquiditycache
twapinside_priceWithinRangeand pass to child functions
Vault Gas Optimizations
The Vault contract was improved with some minor gas optimizations:
Internal Library Gas Optimizations
Internal libraries which execute functions over storage references also benefit from gas optimizations:
cache several fields used in
InterestRateUtils::calculateBorrowingRatecache
lastUpdateTimestampinReserveLogic::latestBorrowingIndex
Interim Gas Optimization Results
Run forge snapshot --diff --fork-url ETH_RPC_URL --fork-block-number 20956198 to get the “test function snapshots” gas output:
test_canDepositAtAnyPrice(uint256,uint256,uint256) (gas: -9887 (-1.166%))
test_canDepositAtAnyPrice(uint256,uint256,uint256) (gas: -9931 (-1.170%))
test_compound() (gas: -15758 (-1.207%))
test_compound() (gas: -15758 (-1.207%))
test_addVestingPosition() (gas: -19229 (-1.217%))
test_addVestingPosition() (gas: -19229 (-1.217%))
test_fuzzDepositAndWithdraw(uint256,uint256) (gas: -10083 (-1.376%))
test_fuzzDepositAndWithdraw(uint256,uint256) (gas: -10086 (-1.376%))
test_Gas_LendingPool() (gas: -40343 (-1.465%))
test_fuzzRebalance(uint256,uint256) (gas: -11360 (-1.478%))
test_fuzzRebalance(uint256,uint256) (gas: -11361 (-1.478%))
test_cantOpenPositionDueToVolatileVault() (gas: -14377 (-1.562%))
test_fuzzDepositWithdrawRemainRatio(uint256) (gas: -19643 (-1.634%))
test_fuzzDepositWithdrawRemainRatio(uint256) (gas: -19660 (-1.636%))
testDeposit() (gas: -16235 (-1.735%))
testDeposit() (gas: -16236 (-1.735%))
test_LendingPoolRepaymentFromBorrower() (gas: -30923 (-1.738%))
test_LendingPoolRepaymentAndClaim() (gas: -30923 (-1.802%))
test_borrowInThirdToken() (gas: -62728 (-2.585%))
test_partialWithdraw() (gas: -67099 (-2.880%))
test_vaultLimitsWork2() (gas: -55670 (-2.916%))
test_fuzzWithdraw(uint256) (gas: -10889 (-2.960%))
test_fuzzWithdraw(uint256) (gas: -10893 (-2.961%))
test_vaultLimitsWork1() (gas: -61307 (-3.212%))
test_Gas_liquidationNoSwap() (gas: -84678 (-3.814%))
test_Gas_liquidationWithSwap() (gas: -85708 (-3.840%))
test_cantOpenPositionWhenReserveInactive() (gas: -68329 (-4.091%))
test_lendingRate() (gas: -70875 (-4.276%))
test_permissionedFunctions() (gas: -108575 (-4.305%))
test_borrowToken1() (gas: -69476 (-4.594%))
test_cantOpenDueToPoolPriceOff() (gas: 0 (NaN%))
Overall gas change: -1077249 (-2.503%)
So far across the entire test suite we’ve reduced gas costs by 1.166% → 4.594% with an overall gas reduction of 2.503% saving 1,077,249 gas, largely by refactoring the existing Solidity code to reduce the amount of identical storage reads.
External Library Gas Optimizations
The protocol uses a number of external libraries from OpenZeppelin. Further gas savings can be realized by changing the contracts to use similar functions from Solady. First install Solady using:
forge install Vectorized/soladyadd
"@solady/=lib/solady/src/",tofoundry.tomlremappings
Next systematically change the protocol’s contracts to use Solady libraries instead of OpenZeppelin:
Solady
ERC20,SafeTransferLibandReentrancyGuardTransientinyTokenSolady
SafeTransferLib,OwnableandFixedPointMathLibinStrategySolady
ERC721,Ownable,SafeTransferLibandReentrancyGuardTransientinLeveragerSolady
Ownable,SafeTransferLibandReentrancyGuardTransientinLendingPool
With the optimized external libraries run forge snapshot --diff --fork-url ETH_RPC_URL --fork-block-number 20956198 to refresh the “test function snapshots” gas output:
test_compound() (gas: -20293 (-1.555%))
test_compound() (gas: -20293 (-1.555%))
test_canDepositAtAnyPrice(uint256,uint256,uint256) (gas: -14501 (-1.711%))
test_addVestingPosition() (gas: -28079 (-1.777%))
test_addVestingPosition() (gas: -28080 (-1.777%))
test_canDepositAtAnyPrice(uint256,uint256,uint256) (gas: -15237 (-1.796%))
test_fuzzRebalance(uint256,uint256) (gas: -15522 (-2.019%))
test_fuzzRebalance(uint256,uint256) (gas: -15522 (-2.019%))
test_fuzzDepositAndWithdraw(uint256,uint256) (gas: -15992 (-2.183%))
test_fuzzDepositAndWithdraw(uint256,uint256) (gas: -15994 (-2.183%))
test_fuzzDepositWithdrawRemainRatio(uint256) (gas: -27003 (-2.247%))
test_fuzzDepositWithdrawRemainRatio(uint256) (gas: -27004 (-2.247%))
testDeposit() (gas: -21996 (-2.351%))
testDeposit() (gas: -21996 (-2.351%))
test_cantOpenPositionDueToVolatileVault() (gas: -27920 (-3.034%))
test_Gas_LendingPool() (gas: -85556 (-3.107%))
test_fuzzWithdraw(uint256) (gas: -12440 (-3.382%))
test_fuzzWithdraw(uint256) (gas: -12444 (-3.382%))
test_permissionedFunctions() (gas: -94015 (-3.728%))
test_LendingPoolRepaymentFromBorrower() (gas: -78629 (-4.418%))
test_LendingPoolRepaymentAndClaim() (gas: -78069 (-4.548%))
test_partialWithdraw() (gas: -125069 (-5.367%))
test_borrowInThirdToken() (gas: -135182 (-5.570%))
test_lendingRate() (gas: -92864 (-5.602%))
test_vaultLimitsWork2() (gas: -110189 (-5.773%))
test_borrowToken1() (gas: -89333 (-5.907%))
test_cantOpenPositionWhenReserveInactive() (gas: -98835 (-5.917%))
test_vaultLimitsWork1() (gas: -115929 (-6.073%))
test_Gas_liquidationNoSwap() (gas: -136030 (-6.126%))
test_Gas_liquidationWithSwap() (gas: -138334 (-6.198%))
test_cantOpenDueToPoolPriceOff() (gas: 0 (NaN%))
Overall gas change: -1718350 (-3.992%)
Optimizing the external libraries increased:
minimum gas saving from 1.166% to 1.555%
maximum gas saving from 4.594% to 6.198%
overall gas saving from 2.503% to 3.992%, saving 1,718,350 gas instead of only 1,077,249 which is a nice 37% improvement!
Enabling The Optimizer
We can do even better by adding the following to foundry.toml to enable the optimizer:
optimizer = true
optimizer_runs = 1_000_000
Now running forge snapshot --diff --fork-url ETH_RPC_URL --fork-block-number 20956198 to refresh the “test function snapshots” gas output gives:
test_fuzzWithdraw(uint256) (gas: -29626 (-8.053%))
test_fuzzWithdraw(uint256) (gas: -29628 (-8.054%))
test_compound() (gas: -139490 (-10.689%))
test_compound() (gas: -139525 (-10.691%))
test_fuzzDepositWithdrawRemainRatio(uint256) (gas: -143294 (-11.923%))
test_fuzzDepositWithdrawRemainRatio(uint256) (gas: -143340 (-11.927%))
test_addVestingPosition() (gas: -201759 (-12.766%))
test_addVestingPosition() (gas: -201764 (-12.767%))
test_Gas_LendingPool() (gas: -390653 (-14.188%))
test_canDepositAtAnyPrice(uint256,uint256,uint256) (gas: -120991 (-14.260%))
test_canDepositAtAnyPrice(uint256,uint256,uint256) (gas: -120980 (-14.271%))
test_borrowInThirdToken() (gas: -354141 (-14.593%))
test_partialWithdraw() (gas: -341000 (-14.634%))
testDeposit() (gas: -137266 (-14.672%))
testDeposit() (gas: -137286 (-14.673%))
test_LendingPoolRepaymentAndClaim() (gas: -261501 (-15.235%))
test_cantOpenPositionDueToVolatileVault() (gas: -141161 (-15.338%))
test_LendingPoolRepaymentFromBorrower() (gas: -273610 (-15.374%))
test_fuzzRebalance(uint256,uint256) (gas: -119467 (-15.540%))
test_fuzzRebalance(uint256,uint256) (gas: -119520 (-15.546%))
test_Gas_liquidationWithSwap() (gas: -351131 (-15.731%))
test_vaultLimitsWork2() (gas: -300941 (-15.765%))
test_vaultLimitsWork1() (gas: -305623 (-16.010%))
test_fuzzDepositAndWithdraw(uint256,uint256) (gas: -120873 (-16.496%))
test_fuzzDepositAndWithdraw(uint256,uint256) (gas: -120924 (-16.502%))
test_Gas_liquidationNoSwap() (gas: -367226 (-16.539%))
test_lendingRate() (gas: -286293 (-17.271%))
test_borrowToken1() (gas: -265396 (-17.548%))
test_cantOpenPositionWhenReserveInactive() (gas: -293424 (-17.566%))
test_permissionedFunctions() (gas: -684835 (-27.154%))
test_cantOpenDueToPoolPriceOff() (gas: 0 (NaN%))
Overall gas change: -6642668 (-15.434%)
Enabling the optimizer has provided major gas improvements across the board, increasing:
minimum gas saving from 1.555% to 8.053%
maximum gas saving from 6.198% to 27.154%
overall gas saving from 3.992% to 15.434%, saving 6,642,668 gas instead of only 1,718,350 resulting in an amazing 3.86x improvement!
Finally let’s get the “gas section snapshots” we put around the key Leverager::liquidatePosition function by running forge test --gas-snapshot-check=true --match-test test_Gas --fork-url ETH_RPC_URL --fork-block-number 20956198:
- [lendingPoolLiquidation] 575367 → 484711 (-15.75%)
- [liquidateWithSwap] 618080 → 536423 (-13.211%)
- [liquidationNoSwap] 439622 → 365286 (-16.909%)
We’ve reduced the gas cost of calling Leverager::liquidatePosition by anywhere from 13.211% to 16.909% which matches well with the previously observed overall gas reduction of 15.434%.
Looking to work with me for a Gas Audit on your protocol? DMs are open!