While reviewing the AccountFacetImpl contract, several areas for gas optimization were identified. These optimizations aim to reduce redundant storage reads, minimize external calls, and improve general efficiency.
General Gas Optimizations:
Accessing storage multiple times within the same function is expensive. Cache frequently accessed variables in memory when possible.
Performing decimal conversions multiple times in the same function increases gas costs. It is more efficient to calculate these once and reuse the result.
Combining multiple external calls into one can reduce gas, as each external call involves overhead.
Instead of repeatedly accessing storage, we cache the withdrawCooldown and deallocateCooldown values, reducing gas consumption.
IERC20Metadata(appLayout.collateral).decimals() call is external and costly. By caching this once in the collateralDecimals variable, we avoid making this call multiple times.
Instead of subtracting from balances[msg.sender] directly each time, we cache the balance, perform the subtraction in memory, and update storage once. This saves gas by reducing the number of storage operations.
The ERC20 transfer is performed after all state changes are made. This is not only a security measure but also ensures that we only make the external call once, saving gas.
Root Cause
No response
Internal pre-conditions
No response
External pre-conditions
No response
Attack Path
No response
Impact
Expected gas savings:
Minimizing redundant reads and writes from storage can save significant gas, especially when done frequently.
Each external call avoided saves roughly 700-2600 gas, depending on the network conditions and contract design.
Reducing the number of IERC20Metadata calls saves considerable gas by minimizing expensive external calls.
PoC
We can implement a test case to benchmark the gas usage of both the original and optimized withdraw function. Below is an example of how you can write a PoC to measure the gas savings.
Example test in hardhat:
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Gas Optimization PoC", function () {
let accountFacet, collateral, user;
before(async function () {
// Deploy mock contracts and set up
const Collateral = await ethers.getContractFactory("MockERC20");
collateral = await Collateral.deploy("Test Collateral", "TC", 18);
await collateral.deployed();
const AccountFacet = await ethers.getContractFactory("AccountFacetImpl");
accountFacet = await AccountFacet.deploy();
await accountFacet.deployed();
[user] = await ethers.getSigners();
// Deposit some initial amount for testing
await collateral.mint(user.address, ethers.utils.parseUnits("1000", 18));
await collateral.approve(accountFacet.address, ethers.utils.parseUnits("1000", 18));
await accountFacet.deposit(user.address, ethers.utils.parseUnits("500", 18));
});
it("should benchmark gas usage for original vs optimized withdraw", async function () {
// Measure gas usage for original withdraw
const originalTx = await accountFacet.withdraw(user.address, ethers.utils.parseUnits("10", 18));
const originalReceipt = await originalTx.wait();
console.log("Original Withdraw Gas Usage:", originalReceipt.gasUsed.toString());
// Measure gas usage for optimized withdraw
const optimizedTx = await accountFacet.optimizedWithdraw(user.address, ethers.utils.parseUnits("10", 18));
const optimizedReceipt = await optimizedTx.wait();
console.log("Optimized Withdraw Gas Usage:", optimizedReceipt.gasUsed.toString());
expect(optimizedReceipt.gasUsed).to.be.below(originalReceipt.gasUsed);
});
});
Petite Spruce Mammoth
Low/Info
Gas optimization in
AccountFacetImpl.sol
Summary
While reviewing the
AccountFacetImpl
contract, several areas for gas optimization were identified. These optimizations aim to reduce redundant storage reads, minimize external calls, and improve general efficiency. General Gas Optimizations:safeTransfer
andsafeTransferFrom
calls are gas-heavy. These should be minimized and optimized when possible. We can optimizewithdraw
function using these techniques. Original code: https://github.com/sherlock-audit/2024-09-symmio-v0-8-4-update-contest/blob/main/protocol-core/contracts/facets/Account/AccountFacetImpl.sol#L26-L36 Optimized code:withdrawCooldown
anddeallocateCooldown
values, reducing gas consumption.IERC20Metadata(appLayout.collateral).decimals()
call is external and costly. By caching this once in thecollateralDecimals
variable, we avoid making this call multiple times.balances[msg.sender]
directly each time, we cache the balance, perform the subtraction in memory, and update storage once. This saves gas by reducing the number of storage operations.Root Cause
No response
Internal pre-conditions
No response
External pre-conditions
No response
Attack Path
No response
Impact
Expected gas savings:
PoC
We can implement a test case to benchmark the gas usage of both the original and optimized
withdraw
function. Below is an example of how you can write a PoC to measure the gas savings.Example test in hardhat:
Mitigation
No response