# The Yieldoor Gas Optimizoor

Decorated auditor [deadrosesxyz](https://x.com/deadrosesxyz) recently put on their developer hat to create a leveraged yield-farming protocol [Yieldoor](https://x.com/deadrosesxyz/status/1892939105190723964) 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](https://x.com/DevDacian)!

We’ll use the publicly available contest [source code](https://github.com/sherlock-audit/2025-02-yieldoor) and start by focusing on the most interesting, complicated and mission-critical liquidation function [`Leverager::liquidatePosition`](https://github.com/sherlock-audit/2025-02-yieldoor/blob/main/yieldoor/src/Leverager.sol#L299-L380).

## Measuring Gas Cost

Foundry provides a number of in-built tools to measure gas cost including:

* [gas reports](https://book.getfoundry.sh/forge/gas-reports) via `forge test --gas-report` after configuration in `foundry.toml` for contract deployment and function call costs
    
* [test function snapshots](https://book.getfoundry.sh/forge/gas-function-snapshots) via `forge snapshot` to measure the gas costs of the entire unit test suite
    
* [gas section snapshots](https://book.getfoundry.sh/forge/gas-section-snapshots) via cheatcodes to measure gas costs of arbitrary code segments inside unit tests
    

We’ll [fork](https://github.com/devdacian/2025-02-yieldoor) the original source code and use the second and third options by:

* [editing](https://github.com/devdacian/2025-02-yieldoor/commit/fe1694b2e383b6a14c4a79e24dce6d4fcb603412) `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`:
    

```solidity
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"](https://github.com/devdacian/2025-02-yieldoor/commit/ee93931a44dddd9fa89ce5a2b01ad47c97347e10) 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):
    

```json
{
  "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`
    

## 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 `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](https://github.com/devdacian/2025-02-yieldoor/blob/b5a0f779dce4236b02665606adb610099451a51a/yieldoor/src/Leverager.sol#L328-L329)
    
* `swapRouter` is read two to four times at [L354,355,363,364](https://github.com/devdacian/2025-02-yieldoor/blob/b5a0f779dce4236b02665606adb610099451a51a/yieldoor/src/Leverager.sol#L354-L364)
    

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:
    
    ```solidity
    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](https://github.com/devdacian/2025-02-yieldoor/commit/1661887e5112f35921edffe46befd8dfed8af011); 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](https://github.com/devdacian/2025-02-yieldoor/commit/0bba016dc580bafb9d1c7c8b72c46cfdaa438367) the `Position memory pos` variable inside our `LiquidateContext` struct and create a new `_getLiquidateContext` internal function to return the struct:

```solidity
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:

```solidity
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](https://github.com/devdacian/2025-02-yieldoor/commit/5506e9225c3e28b75e9e99e3cd116981a0c2fc9b) it to save 10 storage reads:

```solidity
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:

```json
$ 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:

```solidity

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](https://github.com/devdacian/2025-02-yieldoor/commit/143cec641860a9b2a69747c14704fee758d93a44), [e960acf](https://github.com/devdacian/2025-02-yieldoor/commit/e960acf8b06aa964c9a003989d4e043977faf5cc)):

```solidity
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:

```solidity
- [lendingPoolLiquidation] 574354 → 572916
- [liquidateWithSwap] 616824 → 615384
- [liquidationNoSwap] 438617 → 437178
```

## `_isLiquidatable` Function Copies All `VaultParams`

Next 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:

```solidity
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](https://github.com/devdacian/2025-02-yieldoor/commit/7afe13d3d3e12c7b0f070253989aa6241f13952d) 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
    

```solidity
// 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:

```solidity
- [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:

* `liquidatePosition` calls `_isLiquidatable` which reads `pricefeed` from storage into memory
    
* `liquidatePosition` itself reads `pricefeed` again, resulting in a duplicate storage read
    

[Fix](https://github.com/devdacian/2025-02-yieldoor/commit/0f8561cc49d168b8031a9faa7ca4e5bf29a70bd2) 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:

```solidity
- [lendingPoolLiquidation] 572396 → 572345
- [liquidateWithSwap] 614864 → 614813
- [liquidationNoSwap] 436658 → 436608
```

## `pricefeed` Identical Reads In `_calculateTokenValues`

Continuing 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:
    

```solidity
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](https://github.com/devdacian/2025-02-yieldoor/commit/506fc7fb95472e483648c4d705c3baf2911b2500) by changing `_calculateTokenValues` to take the cached `pricefeed` as input, resulting in even greater gas savings:

```solidity
- [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:

* [cache `swapRouter` in `openLeveragedPosition`](https://github.com/devdacian/2025-02-yieldoor/commit/3bce2ec63bd7c9543b3dc5135758a027b3facde3) and in [`withdraw`](https://github.com/devdacian/2025-02-yieldoor/commit/d8f980e1fad601b10a78ffe2e5a92b87e9389bbd)
    
* [don’t read new position from storage in `openLeveragePosition`](https://github.com/devdacian/2025-02-yieldoor/commit/894ca3e68ebc3e5a0323ad19991c983494465bf1)
    
* [pass cached price feed when building new liquidation context in `openLeveragedPosition`](https://github.com/devdacian/2025-02-yieldoor/commit/093c9dd89fa96e29ba3cd2676a71bdea52095f32)
    
* [revert if new position is liquidatable prior to writing to storage](https://github.com/devdacian/2025-02-yieldoor/commit/23ec199ea5eed70e6cf4ee7986d5a71fef8ae8c9)
    
* [revert fast prior to reading storage in `withdraw`](https://github.com/devdacian/2025-02-yieldoor/commit/fa2c32d07f73eabc33a4c99952513fc2bc982f79)
    
* [read only required `VaultParams` and fail fast in `_checkWithinLimits`](https://github.com/devdacian/2025-02-yieldoor/commit/458620cf8a4b56c51d6f58cd5665010cfa5fefe7)
    

## `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:

```solidity
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:

* [don’t initialize to default values](https://github.com/devdacian/2025-02-yieldoor/commit/2c1e276692efe8e37b6bd030b552d38ad22fd91d)
    
* [cache `yTokenAddress` in `redeem`](https://github.com/devdacian/2025-02-yieldoor/commit/8bb3d1c28f4511f2ce8e76894f6a353a75bbd9b3) and [`deposit`](https://github.com/devdacian/2025-02-yieldoor/commit/ffcf5240c5247f98f5d721b1a8cec6fd7b6ddbe4)
    
* [cache `totalBorrows` in `repay`](https://github.com/devdacian/2025-02-yieldoor/commit/83d719c0579dfa32a214084eb589fcac25ef525c)
    
* [don’t copy entire `ReserveData` in `pullFunds`, `pushFunds`](https://github.com/devdacian/2025-02-yieldoor/commit/7d6e1f8e7125403c214f5a9020c4283204ab9fb3) and [`getLeverageParams`](https://github.com/devdacian/2025-02-yieldoor/commit/8bc72a7c0397656fc194c81bdb3496f824a87746)
    

## `Strategy` Gas Optimizations

The `Strategy` contract can also benefit from the following gas optimizations:

* [use `immutable` for storage slots only set once in `constructor`](https://github.com/devdacian/2025-02-yieldoor/commit/73230c759676e8c3ff29dfde518350e2c42e0832)
    
* [cache `ongoingVestingPosition`, `protocolFee` and `feeRecipient` in `collectFees`](https://github.com/devdacian/2025-02-yieldoor/commit/284562b40b75f8d5c50b76429a0c336bc497a500)
    
* [cache `tickTwapDeviation` in `_priceWithinRange`](https://github.com/devdacian/2025-02-yieldoor/commit/afe32d320b5a676a0d75f3e1600e2b99f34db247)
    
* [cache `maxObservationDeviation` in `checkPoolActivity`](https://github.com/devdacian/2025-02-yieldoor/commit/122bab2fd0b5631f7b4c9d3fdd91da4dfa30727f)
    
* [cache `twap` in `twapTick`](https://github.com/devdacian/2025-02-yieldoor/commit/b2c0e19559720b84d868183405895bc6fc21dfc9)
    
* [cache new lower/upper ticks in `_setSecondaryPositionsTicks`](https://github.com/devdacian/2025-02-yieldoor/commit/d35a8cd8b0446ddff4514d4cc3559d93de3353c2)
    
* [use cached `VestingPosition` for final check in `_withdrawPartOfVestingPosition`](https://github.com/devdacian/2025-02-yieldoor/commit/4510339875679caef78dd0273c3147b1be60ffff)
    
* [in `rebalance` cache updated ticks and use when adding liquidity](https://github.com/devdacian/2025-02-yieldoor/commit/4573df718a2cf6d9f17b76e33579658e90a75c2c)
    
* [cache `twap` inside `_priceWithinRange` and pass to child functions](https://github.com/devdacian/2025-02-yieldoor/commit/c8ed6e4937cc046919a0792cff93c911c407f673)
    
* [in `rebalance` cache main/secondary positions and use cache when collecting fees and removing positions](https://github.com/devdacian/2025-02-yieldoor/commit/eb028b147fae62c06b12f1cac0ac1c8456d3130e)
    

## `Vault` Gas Optimizations

The `Vault` contract was improved with some minor gas optimizations:

* [make `token0` and `token1` `immutable`](https://github.com/devdacian/2025-02-yieldoor/commit/3f6dfbd778cfae2f945af1d28411d8d521ca95b2)
    
* [cache `strategy` and `depositFee` in `deposit`](https://github.com/devdacian/2025-02-yieldoor/commit/78debb2786258e0c8d76f96cf3c368033b769f00)
    
* [cache `strategy` in `withdraw`](https://github.com/devdacian/2025-02-yieldoor/commit/d54d8e87829469bbb154acbec868f79f6e041fa3) and [`addVestingPosition`](https://github.com/devdacian/2025-02-yieldoor/commit/e192b169f19762e0f95dd8f7a9ad6bd8fb741ba1)
    

## Internal Library Gas Optimizations

Internal libraries which execute functions over storage references also benefit from gas optimizations:

* [cache several fields used in `InterestRateUtils::calculateBorrowingRate`](https://github.com/devdacian/2025-02-yieldoor/commit/4c471f0da3722e45c477295760808b3598c1a2ed)
    
* [cache `lastUpdateTimestamp` in `ReserveLogic::latestBorrowingIndex`](https://github.com/devdacian/2025-02-yieldoor/commit/a53568845ffc08853e30978973f8fdc9130c6d9b)
    
* [lazy loading, storage caching and only write to storage if values changed in `ReserveLogic::_updateIndexes`](https://github.com/devdacian/2025-02-yieldoor/commit/14964179a850981acf524670457f9b78da21bf0a)
    
* [cache `borrowingIndex` in `ReserveLogic::borrowedLiquidity`](https://github.com/devdacian/2025-02-yieldoor/commit/69b6ae3fa652fa12310a5ea0accaebd63a43c0dc)
    

## 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:

```solidity
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](https://github.com/OpenZeppelin/openzeppelin-contracts/). Further gas savings can be realized by changing the contracts to use similar functions from [Solady](https://github.com/Vectorized/solady). First install Solady using:

* `forge install Vectorized/solady`
    
* add `"@solady/=lib/solady/src/",` to `foundry.toml` [remappings](https://github.com/devdacian/2025-02-yieldoor/commit/8c3a41c8b09b38594a8cbff20d7cc607e0a47129)
    

Next systematically change the protocol’s contracts to use Solady libraries instead of OpenZeppelin:

* [Solady `ERC20`, `SafeTransferLib` and `ReentrancyGuardTransient` in `yToken`](https://github.com/devdacian/2025-02-yieldoor/commit/81abeca91249cc9598900bab7a881da2ad19dd97)
    
* [Solady `ERC20`, `SafeTransferLib` and `Ownable` in `Vault`](https://github.com/devdacian/2025-02-yieldoor/commit/9f309c35c56a39db157a4ba298546ca9515eb49e)
    
* [Solady `SafeTransferLib`, `Ownable` and `FixedPointMathLib` in `Strategy`](https://github.com/devdacian/2025-02-yieldoor/commit/b34eadfbb99a8773fb5ad81530dcfc7181f2222c)
    
* [Solady `ERC721`, `Ownable`, `SafeTransferLib` and `ReentrancyGuardTransient` in `Leverager`](https://github.com/devdacian/2025-02-yieldoor/commit/88fa8f94f91d4eec1415ad3781393649cf39b5d5)
    
* [Solady `Ownable`, `SafeTransferLib` and `ReentrancyGuardTransient` in `LendingPool`](https://github.com/devdacian/2025-02-yieldoor/commit/9fd22082c59ce374d91faba32911ca4c77e1ca04)
    
* [Solady `FixedPointMathLib` in `LiquidtyAmounts`](https://github.com/devdacian/2025-02-yieldoor/commit/77842fa3e0ab9df421a0c7685d4761c5bd0d63ae)
    

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:

```solidity
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](https://github.com/devdacian/2025-02-yieldoor/commit/e2068a25fce214466b1517e673706f79f6344244):

```solidity
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:

```solidity
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`:

```solidity
- [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](https://x.com/DevDacian)!

## Additional Resources

* [Solidity Gas Optimization Examples](https://github.com/devdacian/solidity-gas-optimization)
    
* [Solidity Gas Optimization Tips](https://www.cyfrin.io/blog/solidity-gas-efficiency-tips-tackle-rising-fees-base-other-l2)
    
* [Solidity Gas Optimization CheatSheet](https://0xmacro.com/writing/solidity-gas-optimizations-cheat-sheet)
    
* [Book of Solidity Gas Optimizations](https://www.rareskills.io/post/gas-optimization)
