The Yieldoor Gas Optimizoor
Learn to write gas-efficient Solidity code using a real-world example!
Search for a command to run...
Learn to write gas-efficient Solidity code using a real-world example!
No comments yet. Be the first to comment.
Deep dives on vulnerability classes exploited in real-world smart contract attacks
Liquidation code can contain subtle bugs and vulnerabilities..
In recent times a number of web3 security companies have begun offering paid “AI Smart Contract Auditor” services. These services commonly require clients to upload code to a website and after paying a fee, receive an AI-generated audit report. A muc...
Liquidation code can contain subtle bugs and vulnerabilities..
At DeFi Security Summit 2024 I presented a workshop on how smart contract developers can use invariant fuzz testing to find high severity issues prior to external audit, based on my real-world experience doing private audits with Cyfrin. Since fuzz t...
Many high severity findings found during private audits by external auditors could have been found by the protocol developers themselves using invariant fuzz testing prior to engaging external auditors. While this doesn’t require developing an “attac...
On this page
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.
Foundry provides a number of in-built tools to measure gas cost including:
gas reports via forge test --gas-report after configuration in foundry.toml for contract deployment and function call costs
test function snapshots via forge snapshot to measure the gas costs of the entire unit test suite
gas 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.sol to add a test_Gas_* prefix around the three tests which call Leverager::liquidatePosition
changing test/BaseTest.sol to reduce fuzz runs to 100 by editing the relevant comments to /// forge-config: default.fuzz.runs = 100 to speed up the iterative optimization cycle
add “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 20956198 which generates a new file .gas-snapshot recording 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 20956198 which generates a file snapshots/LeveragerTest.json showing the initial gas cost for the manual snapshots wrapping calls to Leverager::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 20956198 to see whether the change reduced the per-test and overall all-tests gas costs. To override the existing .gas-snapshot file simply run it without the -diff flag. Important: all tests must pass for a new .gas-snapshot file to be generated
forge test --gas-snapshot-check=true --match-test test_Gas --fork-url ETH_RPC_URL --fork-block-number 20956198 to 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 via rm snapshots/LeveragerTest.json
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 immutable
otherwise 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:
feeRecipient is read twice at L328,329
swapRouter is 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 LiquidateContext to 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 _calculateTokenValues
declare a new local variable LiquidateContext memory ctx inside 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 SlotsNext 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 SlotsHaving 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 ctx
can be called by both liquidatePosition and isLiquidatable
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 VaultParamsNext inspecting the _isLiquidatable function observe that it:
copies VaultParams from storage to memory
only uses maxTimesLeverage and minCollateralPct from VaultParams
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 LiquidateContext struct to contain the additional fields
adding code to _getLiquidateContext to cache those fields
changing _isLiquidatable to 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 & _isLiquidatableA common mistake developers make is re-reading identical values from storage in child functions called by parent functions; in this case:
liquidatePosition calls _isLiquidatable which reads pricefeed from storage into memory
liquidatePosition itself reads pricefeed again, resulting in a duplicate storage read
Fix this by:
adding priceFeed to struct LiquidateContext
reading it once inside getLiquidateContext
reading it from the cached copy in _isLiquidatable and liquidatePosition
This again reduces the gas costs across the board:
- [lendingPoolLiquidation] 572396 → 572345
- [liquidateWithSwap] 614864 → 614813
- [liquidationNoSwap] 436658 → 436608
pricefeed Identical Reads In _calculateTokenValuesContinuing to examine the _isLiquidatable child function, observe it:
calls _calculateTokenValues which is also called by the main function liquidatePosition
_calculateTokenValues reads the same identical value of pricefeed from 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 OptimizationsFurther 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 openLeveragePosition
pass cached price feed when building new liquidation context in openLeveragedPosition
revert if new position is liquidatable prior to writing to storage
read only required VaultParams and fail fast in _checkWithinLimits
Leverager High Finding - feeRecipient Never SetOne 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 collectFeesWhile 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 OptimizationsSimilar gas optimizations can be made to the LendingPool contract including:
Strategy Gas OptimizationsThe Strategy contract can also benefit from the following gas optimizations:
use immutable for storage slots only set once in constructor
cache ongoingVestingPosition, protocolFee and feeRecipient in collectFees
use cached VestingPosition for final check in _withdrawPartOfVestingPosition
in rebalance cache updated ticks and use when adding liquidity
cache twap inside _priceWithinRange and pass to child functions
Vault Gas OptimizationsThe Vault contract was improved with some minor gas optimizations:
Internal libraries which execute functions over storage references also benefit from gas optimizations:
cache several fields used in InterestRateUtils::calculateBorrowingRate
cache lastUpdateTimestamp in ReserveLogic::latestBorrowingIndex
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.
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/solady
add "@solady/=lib/solady/src/", to foundry.toml remappings
Next systematically change the protocol’s contracts to use Solady libraries instead of OpenZeppelin:
Solady ERC20, SafeTransferLib and ReentrancyGuardTransient in yToken
Solady SafeTransferLib, Ownable and FixedPointMathLib in Strategy
Solady ERC721, Ownable, SafeTransferLib and ReentrancyGuardTransient in Leverager
Solady Ownable, SafeTransferLib and ReentrancyGuardTransient in LendingPool
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!
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!