Anytime you are reading from storage more than once, it is cheaper in gas cost to cache the variable in memory: a SLOAD cost 100gas, while MLOAD and MSTORE cost 3 gas.
In particular, in for loops, when using the length of a storage array as the condition being checked after each loop, caching the array length in memory can yield significant gas savings if the array length is high
PROOF OF CONCEPT
Instances include:
lending-market/GovernorBravoDelegate.sol
scope: queue()
newProposal.targets.length is read newProposal.targets.length times
Calldata instead of memory for RO function parameters
PROBLEM
If a reference type function parameter is read-only, it is cheaper in gas to use calldata instead of memory.
Calldata is a non-modifiable, non-persistent area where function arguments are stored, and behaves mostly like memory.
Try to use calldata as a data location because it will avoid copies and also makes sure that the data cannot be modified.
PROOF OF CONCEPT
Instances include:
lending-market/GovernorBravoDelegate.sol
scope: queueOrRevertInternal
l77 string memory signature, bytes memory data
lending-market/AccountantDelegator.sol
scope: delegateTo
l82 bytes memory data
scope: delegateToImplementation
l98 bytes memory data
scope: delegateToViewImplementation
l109 bytes memory data
lending-market/TreasuryDelegator.sol
scope: delegateToImplementation
73 bytes memory data
scope: delegateToViewImplementation
l84 bytes memory data
scope: delegateTo
l101 bytes memory data
TOOLS USED
Manual Analysis
MITIGATION
Replace memory with calldata
Comparison Operators
IMPACT
In the EVM, there is no opcode for >= or <=.
When using greater than or equal, two operations are performed: > and =.
Using strict comparison operators hence saves gas, approximately 20 gas in require and if statements
Hardcode storage variables with their initial value instead of writing it during contract deployment with constructor parameters.
Custom Errors
IMPACT
Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met) while providing the same amount of information, as explained here.
It not only saves gas upon deployment - ~5500 gas saved per custom error instead of a require statement, but it is also cheaper in a function call, 22 gas saved per require statement replaced with a custom error.
Custom errors are defined using the error statement
PROOF OF CONCEPT
Instances include:
lending-market/WETH.sol
l29 require(_balanceOf[msg.sender] >= wad, "sender balance insufficient for withdrawal")
l69 require(_balanceOf[src] >= wad)
l72 require(_allowance[src][msg.sender] >= wad)
l96 require(owner != address(0), "ERC20: approve from the zero address")
l97 require(spender != address(0), "ERC20: approve to the zero address")
lending-market/GovernorBravoDelegate.sol
l25 require(address(timelock) == address(0), "GovernorBravo::initialize: can only initialize once");
l26 require(msg.sender == admin, "GovernorBravo::initialize: admin only");
l27 require(timelock_ != address(0), "GovernorBravo::initialize: invalid timelock address")
l42 require(unigovProposal.targets.length == unigovProposal.values.length && unigovProposal.targets.length == unigovProposal.signatures.length && unigovProposal.targets.length == unigovProposal.calldatas.length,
"GovernorBravo::propose: proposal function information arity mismatch")
l46 require(unigovProposal.targets.length != 0, "GovernorBravo::propose: must provide actions");
l47 require(unigovProposal.targets.length <= proposalMaxOperations, "GovernorBravo::propose: too many actions")
l53 require(proposals[unigovProposal.id].id == 0)
l78 require(!timelock.queuedTransactions(keccak256(abi.encode(target, value, signature, data, eta))), "GovernorBravo::queueOrRevertInternal: identical proposal action already queued at eta")
l87 require(state(proposalId) == ProposalState.Queued, "GovernorBravo::execute: proposal can only be executed if it is queued")
l115 require(proposalCount >= proposalId && proposalId > initialProposalId, "GovernorBravo::state: invalid proposal id")
l132 require(msg.sender == admin, "GovernorBravo::_initiate: admin only")
l133 require(initialProposalId == 0, "GovernorBravo::_initiate: can only initiate once")
l146 require(msg.sender == admin, "GovernorBravo:_setPendingAdmin: admin only")
l164 require(msg.sender == pendingAdmin && msg.sender != address(0), "GovernorBravo:_acceptAdmin: pending admin only")
l182 require(c >= a, "addition overflow")
l187 require(b <= a, "subtraction underflow")
lending-market/Comptroller.sol
l178 require(oErr == 0, "exitMarket: getAccountSnapshot failed")
l237 require(!mintGuardianPaused[cToken], "mint is paused")
l343 require(!borrowGuardianPaused[cToken], "borrow is paused")
l351 require(msg.sender == cToken, "sender must be cToken")
l373 require(nextTotalBorrows < borrowCap, "market borrow cap reached")
l491 require(borrowBalance >= repayAmount, "Can not repay more than the total borrow")
l556 require(!seizeGuardianPaused, "seize is paused")
l614 require(!transferGuardianPaused, "transfer is paused")
l852 require(msg.sender == admin, "only admin can set close factor")
l960 require(allMarkets[i] != CToken(cToken), "market already added")
l998 require(msg.sender == admin || msg.sender == borrowCapGuardian, "only admin or borrow cap guardian can set borrow caps")
l1003 require(numMarkets != 0 && numMarkets == numBorrowCaps, "invalid input")
l1016 require(msg.sender == admin, "only admin can set borrow cap guardian")
l1051 require(markets[address(cToken)].isListed, "cannot pause a market that is not listed");
l1052 require(msg.sender == pauseGuardian || msg.sender == admin, "only pause guardian and admin can pause");
l1053 require(msg.sender == admin || state == true, "only admin can unpause")
l1061 require(markets[address(cToken)].isListed, "cannot pause a market that is not listed");
l1062 require(msg.sender == pauseGuardian || msg.sender == admin, "only pause guardian and admin can pause");
l1063 require(msg.sender == admin || state == true, "only admin can unpause")
l1071 require(msg.sender == pauseGuardian || msg.sender == admin, "only pause guardian and admin can pause");
l1072 require(msg.sender == admin || state == true, "only admin can unpause")
l1080 require(msg.sender == pauseGuardian || msg.sender == admin, "only pause guardian and admin can pause");
l1081 require(msg.sender == admin || state == true, "only admin can unpause")
l1089 require(msg.sender == unitroller.admin(), "only unitroller admin can change brains");
l1090 require(unitroller._acceptImplementation() == 0, "change not authorized");
l1095 require(msg.sender == admin, "Only admin can call this function"); // Only the timelock can call this function
l1096 require(!proposal65FixExecuted, "Already executed this one-off function"); // Require that this function is only called once
l1097 require(affectedUsers.length == amounts.length, "Invalid input")
l1158 require(market.isListed, "comp market is not listed")
l1349 require(markets[address(cToken)].isListed, "market must be listed")
l1395 require(adminOrInitializing(), "only admin can grant comp")
l1397 require(amountLeft == 0, "insufficient comp for grant")
l1408 require(adminOrInitializing(), "only admin can set comp speed")
l1411 require(numTokens == supplySpeeds.length && numTokens == borrowSpeeds.length, "Comptroller::_setCompSpeeds invalid input")
l1424 require(adminOrInitializing(), "only admin can set comp speed")
lending-market/AccountantDelegate.sol
l17 require(msg.sender == admin, "AccountantDelegate::initialize: only admin can call this function");
l18 require(noteAddress_ != address(0), "AccountantDelegate::initialize: note Address invalid")
l29 require(note.balanceOf(msg.sender) == note._initialSupply(), "AccountantDelegate::initiatlize: Accountant has not received payment")
l48 require(msg.sender == address(cnote), "AccountantDelegate::supplyMarket: Only the CNote contract can supply market")
l60 require(msg.sender == address(cnote), "AccountantDelegate::redeemMarket: Only the CNote contract can redeem market")
l83 require(cNoteConverted >= noteDifferential, "Note Loaned to LendingMarket must increase in value")
Replace require and revert statements with custom errors.
For instance, in lending-market/GovernorBravoDelegate.sol:
Replace
require(address(timelock) == address(0), "GovernorBravo::initialize: can only initialize once")
with
if (address(timelock) != address(0)) {
revert IsInitialized();
}
and define the custom error in the contract
error IsInitialized();
Default value initialization
IMPACT
If a variable is not set/initialized, it is assumed to have the default value (0, false, 0x0 etc depending on the data type).
Explicitly initializing it with its default value is an anti-pattern and wastes 22 gas per variable initialized.
PROOF OF CONCEPT
Instances include:
lending-market/GovernorBravoDelegate.sol
l68 uint i = 0
l90 uint i = 0
lending-market/Comptroller.sol
l126 uint i = 0
l206 uint i = 0
l735 uint i = 0
l959 uint i = 0
l1005 uint i = 0
l1106 uint i = 0
l1347 uint i = 0
l1353 uint j = 0
l1359 uint j = 0
l1364 uint j = 0
l1413 uint i = 0
BaseV1-core.sol
l46 uint public totalSupply = 0
l207 uint i = 0
l223 uint nextIndex = 0
l224 uint index = 0
l337 uint i = 0
BaseV1-periphery.sol
l136 uint i = 0
l158 uint _totalSupply = 0
l362 uint i = 0
TOOLS USED
Manual Analysis
MITIGATION
Remove explicit initialization for default values.
Event emitting of local variable
PROBLEM
When emitting an event, using a local variable instead of a storage variable saves gas.
When a require statement is use multiple times, it is cheaper to use a modifier instead.
PROOF OF CONCEPT
Instances include:
lending-market/Comptroller.sol
l1051 require(markets[address(cToken)].isListed, "cannot pause a market that is not listed")
l1061 require(markets[address(cToken)].isListed, "cannot pause a market that is not listed")
l1052 require(msg.sender == pauseGuardian || msg.sender == admin, "only pause guardian and admin can pause")
l1062 require(msg.sender == pauseGuardian || msg.sender == admin, "only pause guardian and admin can pause")
l1071 require(msg.sender == pauseGuardian || msg.sender == admin, "only pause guardian and admin can pause")
l1080 require(msg.sender == pauseGuardian || msg.sender == admin, "only pause guardian and admin can pause")
l1053 require(msg.sender == admin || state == true, "only admin can unpause")
l1063 require(msg.sender == admin || state == true, "only admin can unpause")
l1072 require(msg.sender == admin || state == true, "only admin can unpause")
l1081 require(msg.sender == admin || state == true, "only admin can unpause")
l1408 require(adminOrInitializing(), "only admin can set comp speed")
l1424 require(adminOrInitializing(), "only admin can set comp speed")
Prefix increments are cheaper than postfix increments: it returns the incremented variable instead of returning a temporary variable storing the initial value of the variable. It saves 5 gas per iteration
You can also improve gas savings by using custom errors
Return statements
IMPACT
Named returns are the most gas efficient return statements, but there is no gas saving if the named return is unused and a return statement is used - costing an extra 2,000 gas per function call.
Replace the return statements as explained, using a local variable with the named return instead.
Revert strings length
IMPACT
Revert strings cost more gas to deploy if the string is larger than 32 bytes. Each string exceeding that 32-byte size adds an extra 9,500 gas upon deployment.
PROOF OF CONCEPT
Revert strings exceeding 32 bytes include:
lending-market/WETH.sol
l29 sender balance insufficient for withdrawal
l96 ERC20: approve from the zero address
l97 ERC20: approve to the zero address
lending-market/GovernorBravoDelegate.sol
l25 require(address(timelock) == address(0), "GovernorBravo::initialize: can only initialize once");
l26 require(msg.sender == admin, "GovernorBravo::initialize: admin only");
l27 require(timelock_ != address(0), "GovernorBravo::initialize: invalid timelock address")
l42 require(unigovProposal.targets.length == unigovProposal.values.length && unigovProposal.targets.length == unigovProposal.signatures.length && unigovProposal.targets.length == unigovProposal.calldatas.length,
"GovernorBravo::propose: proposal function information arity mismatch")
l46 require(unigovProposal.targets.length != 0, "GovernorBravo::propose: must provide actions");
l47 require(unigovProposal.targets.length <= proposalMaxOperations, "GovernorBravo::propose: too many actions")
l78 require(!timelock.queuedTransactions(keccak256(abi.encode(target, value, signature, data, eta))), "GovernorBravo::queueOrRevertInternal: identical proposal action already queued at eta")
l87 require(state(proposalId) == ProposalState.Queued, "GovernorBravo::execute: proposal can only be executed if it is queued")
l115 require(proposalCount >= proposalId && proposalId > initialProposalId, "GovernorBravo::state: invalid proposal id")
l132 require(msg.sender == admin, "GovernorBravo::_initiate: admin only")
l133 require(initialProposalId == 0, "GovernorBravo::_initiate: can only initiate once")
l146 require(msg.sender == admin, "GovernorBravo:_setPendingAdmin: admin only")
l164 require(msg.sender == pendingAdmin && msg.sender != address(0), "GovernorBravo:_acceptAdmin: pending admin only")
lending-market/Comptroller.sol
l178 require(oErr == 0, "exitMarket: getAccountSnapshot failed")
l491 require(borrowBalance >= repayAmount, "Can not repay more than the total borrow")
l998 require(msg.sender == admin || msg.sender == borrowCapGuardian, "only admin or borrow cap guardian can set borrow caps")
l1016 require(msg.sender == admin, "only admin can set borrow cap guardian")
l1051 require(markets[address(cToken)].isListed, "cannot pause a market that is not listed");
l1052 require(msg.sender == pauseGuardian || msg.sender == admin, "only pause guardian and admin can pause");
l1061 require(markets[address(cToken)].isListed, "cannot pause a market that is not listed");
l1062 require(msg.sender == pauseGuardian || msg.sender == admin, "only pause guardian and admin can pause");
l1071 require(msg.sender == pauseGuardian || msg.sender == admin, "only pause guardian and admin can pause");
l1080 require(msg.sender == pauseGuardian || msg.sender == admin, "only pause guardian and admin can pause");
l1089 require(msg.sender == unitroller.admin(), "only unitroller admin can change brains");
l1095 require(msg.sender == admin, "Only admin can call this function");
l1096 require(!proposal65FixExecuted, "Already executed this one-off function");
l1411 require(numTokens == supplySpeeds.length && numTokens == borrowSpeeds.length, "Comptroller::_setCompSpeeds invalid input")
lending-market/AccountantDelegate.sol
l17 require(msg.sender == admin, "AccountantDelegate::initialize: only admin can call this function");
l18 require(noteAddress_ != address(0), "AccountantDelegate::initialize: note Address invalid")
l29 require(note.balanceOf(msg.sender) == note._initialSupply(), "AccountantDelegate::initiatlize: Accountant has not received payment")
l48 require(msg.sender == address(cnote), "AccountantDelegate::supplyMarket: Only the CNote contract can supply market")
l60 require(msg.sender == address(cnote), "AccountantDelegate::redeemMarket: Only the CNote contract can redeem market")
l83 require(cNoteConverted >= noteDifferential, "Note Loaned to LendingMarket must increase in value")
Write the error strings so that they do not exceed 32 bytes. For further gas savings, consider also using custom errors.
Tautologies
PROBLEM
Tautologies should be avoided as they waste gas.
PROOF OF CONCEPT
Instances include:
lending-market/NoteInterest.sol
l97 uint newRatePerYear = ir >= 0 ? ir : 0
ir is a uint256, hence always >= 0.
TOOLS USED
Manual Analysis
MITIGATION
Replace
uint newRatePerYear = ir >= 0 ? ir : 0;
with
uint newRatePerYear = ir;
Tight Variable Packing
PROBLEM
Solidity contracts have contiguous 32 bytes (256 bits) slots used in storage.
By arranging the variables, it is possible to minimize the number of slots used within a contract's storage and therefore reduce deployment costs.
address type variables are each of 20 bytes size (way less than 32 bytes). However, they here take up a whole 32 bytes slot (they are contiguous).
As uint8 and bool type variables are of size 1 byte, there's a slot here that can get saved by moving an address closer to a bool and or a uint8
PROOF OF CONCEPT
Instances include:
stableswap/BaseV1-core.sol
uint8 public constant decimals = 18;
// Used to denote stable or volatile pair, not immutable since construction happens in the initialize method for CREATE2 deterministic addresses
bool public immutable stable;
uint public totalSupply = 0;
mapping(address => mapping (address => uint)) public allowance;
mapping(address => uint) public balanceOf;
bytes32 internal DOMAIN_SEPARATOR;
// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
bytes32 internal constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
mapping(address => uint) public nonces;
uint internal constant MINIMUM_LIQUIDITY = 10**3;
address public immutable token0;
address public immutable token1;
address immutable factory;
TOOLS USED
Manual Analysis
MITIGATION
Place factory before decimals to save one storage slot
+address immutable factory;
uint8 public constant decimals = 18;
// Used to denote stable or volatile pair, not immutable since construction happens in the initialize method for CREATE2 deterministic addresses
bool public immutable stable;
uint public totalSupply = 0;
mapping(address => mapping (address => uint)) public allowance;
mapping(address => uint) public balanceOf;
bytes32 internal DOMAIN_SEPARATOR;
// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
bytes32 internal constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
mapping(address => uint) public nonces;
uint internal constant MINIMUM_LIQUIDITY = 10**3;
address public immutable token0;
address public immutable token1;
Unchecked arithmetic
IMPACT
The default "checked" behavior costs more gas when adding/diving/multiplying, because under-the-hood those checks are implemented as a series of opcodes that, prior to performing the actual arithmetic, check for under/overflow and revert if it is detected.
if it can statically be determined there is no possible way for your arithmetic to under/overflow (such as a condition in an if statement), surrounding the arithmetic in an unchecked block will save gas
PROOF OF CONCEPT
Instances include:
lending-market/WETH.sol
l30 _balanceOf[msg.sender] -= wad //cannot underflow because of check line 29
l73 _allowance[src][msg.sender] -= wad //cannot underflow because of check line 72
l76 _balanceOf[src] -= wad //cannot underflow because of check line 69
l136 i++
l276 msg.value - amountCANTO //cannot underflow because of check line 276
l362 i++
TOOLS USED
Manual Analysis
MITIGATION
Place the arithmetic operations in an unchecked block
Unnecessary functions
IMPACT
As of Solidity 0.8.0, underflow and overflow checks are default in mathematical operations. Using functions to perform these checks is redundant and wastes gas.
PROOF OF CONCEPT
Instances include:
lending-market/GovernorBravoDelegate.sol
180 function add256
186 function sub256
lending-market/NoteInterest.sol
using SafeMath for uint
TOOLS USED
Manual Analysis
MITIGATION
Remove these functions
Unnecessary computation
IMPACT
There are several instances where a local variable is used but is not necessary.
For instance, when emitting an event that includes a new and an old value, it is cheaper in gas to avoid caching the old value in memory. Instead, emit the event, then save the new value in storage.
l916 uint oldLiquidationIncentiveMantissa = liquidationIncentiveMantissa;
// Set liquidation incentive to new incentive
liquidationIncentiveMantissa = newLiquidationIncentiveMantissa;
// Emit event with old incentive, new incentive
emit NewLiquidationIncentive(oldLiquidationIncentiveMantissa, newLiquidationIncentiveMantissa)
scope: _setBorrowCapGuardian()
l1019 address oldBorrowCapGuardian = borrowCapGuardian;
// Store borrowCapGuardian with value newBorrowCapGuardian
borrowCapGuardian = newBorrowCapGuardian;
// Emit NewBorrowCapGuardian(OldBorrowCapGuardian, NewBorrowCapGuardian)
emit NewBorrowCapGuardian(oldBorrowCapGuardian, newBorrowCapGuardian)
scope: _setPauseGuardian()
l1038 address oldPauseGuardian = pauseGuardian;
// Store pauseGuardian with value newPauseGuardian
pauseGuardian = newPauseGuardian;
// Emit NewPauseGuardian(OldPauseGuardian, NewPauseGuardian)
emit NewPauseGuardian(oldPauseGuardian, pauseGuardian);
Gas Report
Table of Contents
Caching storage variables in memory to save gas
IMPACT
Anytime you are reading from storage more than once, it is cheaper in gas cost to cache the variable in memory: a SLOAD cost 100gas, while MLOAD and MSTORE cost 3 gas.
In particular, in
for
loops, when using the length of a storage array as the condition being checked after each loop, caching the array length in memory can yield significant gas savings if the array length is highPROOF OF CONCEPT
Instances include:
lending-market/GovernorBravoDelegate.sol
scope:
queue()
newProposal.targets.length
is readnewProposal.targets.length
timesscope:
queueOrRevertInternal()
timelock
is read twicescope:
execute()
proposal.targets.length
is readproposal.targets.length
timesscope:
_acceptAdmin()
pendingAdmin
is read 3 timeslending-market/Comptroller.sol
scope:
queue()
allMarkets.length
is readallMarkets.length
timesscope:
_setBorrowPaused()
admin
is read twicescope:
_setTransferPaused()
admin
is read twicescope:
_setSeizePaused()
admin
is read twicelending-market/AccountantDelegate.sol
scope:
initialize()
note
is read 4 timesscope:
supplyMarket()
cnote
is read twicescope:
redeemMarket()
cnote
is read 3 timesscope:
sweepInterest()
note
is read 3 timescnote
is read 3 timeslending-market/cnote.sol
scope:
_setAccountantContract()
_accountant
is read twicescope:
borrowFresh()
_accountant
is read twicescope:
repayBorrowFresh()
_accountant
is read twicescope:
mintFresh()
_accountant
is read 3 timesscope:
redeemFresh()
_accountant
is read 3 timescomptroller
is read twicestableswap/BaseV1-core.sol
scope:
acceptPauser()
pendingPauser
is read twiceTOOLS USED
Manual Analysis
MITIGATION
cache these storage variables in memory
Calldata instead of memory for RO function parameters
PROBLEM
If a reference type function parameter is read-only, it is cheaper in gas to use calldata instead of memory. Calldata is a non-modifiable, non-persistent area where function arguments are stored, and behaves mostly like memory.
Try to use calldata as a data location because it will avoid copies and also makes sure that the data cannot be modified.
PROOF OF CONCEPT
Instances include:
lending-market/GovernorBravoDelegate.sol
scope:
queueOrRevertInternal
lending-market/AccountantDelegator.sol
scope:
delegateTo
scope:
delegateToImplementation
scope:
delegateToViewImplementation
lending-market/TreasuryDelegator.sol
scope:
delegateToImplementation
scope:
delegateToViewImplementation
scope:
delegateTo
TOOLS USED
Manual Analysis
MITIGATION
Replace
memory
withcalldata
Comparison Operators
IMPACT
In the EVM, there is no opcode for
>=
or<=
. When using greater than or equal, two operations are performed:>
and=
.Using strict comparison operators hence saves gas, approximately
20
gas inrequire
andif
statementsPROOF OF CONCEPT
Instances include:
lending-market/WETH.sol
lending-market/GovernorBravoDelegate.sol
lending-market/Comptroller.sol
lending-market/AccountantDelegate.sol
lending-market/CNote.sol
stableswap/BaseV1-core.sol
stableswap/BaseV1-periphery.sol
TOOLS USED
Manual Analysis
MITIGATION
Replace
<=
with<
, and>=
with>
. Do not forget to increment/decrement the compared variableexample:
However, if
1
is negligible compared to the value of the variable, we can omit the increment.Constant expressions
IMPACT
Constant expressions are re-calculated each time it is in use, costing an extra
97
gas than a constant every time they are called.PROOF OF CONCEPT
Instances include:
lending-market/GovernorBravoDelegate.sol
TOOLS USED
Manual Analysis
MITIGATION
Mark these as
immutable
instead ofconstant
Constructor parameters should be avoided when possible
IMPACT
Constructor parameters are expensive. The contract deployment will be cheaper in gas if they are hard coded instead of using constructor parameters.
PROOF OF CONCEPT
Instances include:
lending-market/WETH.sol
stableswap/BaseV1-core.sol
stableswap/BaseV1-periphery.sol
TOOLS USED
Manual Analysis
MITIGATION
Hardcode storage variables with their initial value instead of writing it during contract deployment with constructor parameters.
Custom Errors
IMPACT
Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met) while providing the same amount of information, as explained here.
It not only saves gas upon deployment -
~5500
gas saved per custom error instead of a require statement, but it is also cheaper in a function call,22
gas saved per require statement replaced with a custom error.Custom errors are defined using the error statement
PROOF OF CONCEPT
Instances include:
lending-market/WETH.sol
lending-market/GovernorBravoDelegate.sol
lending-market/Comptroller.sol
lending-market/AccountantDelegate.sol
lending-market/AccountantDelegator.sol
lending-market/TreasuryDelegator.sol
lending-market/CNote.sol
stableswap/BaseV1-core.sol
stableswap/BaseV1-periphery.sol
TOOLS USED
Manual Analysis
MITIGATION
Replace require and revert statements with custom errors.
For instance, in
lending-market/GovernorBravoDelegate.sol
:Replace
with
and define the custom error in the contract
Default value initialization
IMPACT
If a variable is not set/initialized, it is assumed to have the default value (0, false, 0x0 etc depending on the data type). Explicitly initializing it with its default value is an anti-pattern and wastes
22
gas per variable initialized.PROOF OF CONCEPT
Instances include:
lending-market/GovernorBravoDelegate.sol
lending-market/Comptroller.sol
BaseV1-core.sol
BaseV1-periphery.sol
TOOLS USED
Manual Analysis
MITIGATION
Remove explicit initialization for default values.
Event emitting of local variable
PROBLEM
When emitting an event, using a local variable instead of a storage variable saves gas.
PROOF OF CONCEPT
Instances include:
lending-market/GovernorBravoDelegate.sol
lending-market/NoteInterest.sol
stableswap/BaseV1-core.sol
TOOLS USED
Manual Analysis
MITIGATION
Emit a local variable or function argument instead of storage variable
Immutable variables save storage
PROBLEM
If a variable is set in the constructor and never modified afterwrds, marking it as
immutable
can save a storage operation -20,000
gas.PROOF OF CONCEPT
Instances include:
lending-market/WETH.sol
TOOLS USED
Manual Analysis
MITIGATION
Mark these variables as
immutable
.Mathematical optimizations
PROBLEM
X += Y costs
22
more gas than X = X + Y.PROOF OF CONCEPT
Instances include:
lending-market/WETH.sol
stableswap/BaseV1-core.sol
TOOLS USED
Manual Analysis
MITIGATION
use
X = X + Y
instead ofX += Y
(same with-
)Modifier instead of duplicate require
PROBLEM
When a
require
statement is use multiple times, it is cheaper to use a modifier instead.PROOF OF CONCEPT
Instances include:
lending-market/Comptroller.sol
stableswap/BaseV1-core.sol
TOOLS USED
Manual Analysis
MITIGATION
Use modifiers for these repeated statements
Prefix increments
IMPACT
Prefix increments are cheaper than postfix increments: it returns the incremented variable instead of returning a temporary variable storing the initial value of the variable. It saves
5
gas per iterationPROOF OF CONCEPT
Instances include:
lending-market/GovernorBravoDelegate.sol
lending-market/Comptroller.sol
stableswap/BaseV1-core.sol
stableswap/BaseV1-periphery.sol
TOOLS USED
Manual Analysis
MITIGATION
change
variable++
to++variable
.Require instead of AND
IMPACT
Require statements including conditions with the
&&
operator can be broken down in multiple require statements to save gas.PROOF OF CONCEPT
Instances include:
lending-market/GovernorBravoDelegate.sol
lending-market/Comptroller.sol
stableswap/BaseV1-core.sol
stableswap/BaseV1-periphery.sol
TOOLS USED
Manual Analysis
MITIGATION
Break down the statements in multiple require statements.
You can also improve gas savings by using custom errors
Return statements
IMPACT
Named returns are the most gas efficient return statements, but there is no gas saving if the named return is unused and a
return
statement is used - costing an extra2,000
gas per function call.PROOF OF CONCEPT
Instances include:
lending-market/GovernorBravoDelegate.sol
stableswap/BaseV1-core.sol
stableswap/BaseV1-periphery.sol
TOOLS USED
Manual Analysis
MITIGATION
Replace the
return
statements as explained, using a local variable with the named return instead.Revert strings length
IMPACT
Revert strings cost more gas to deploy if the string is larger than 32 bytes. Each string exceeding that 32-byte size adds an extra
9,500
gas upon deployment.PROOF OF CONCEPT
Revert strings exceeding 32 bytes include:
lending-market/WETH.sol
lending-market/GovernorBravoDelegate.sol
lending-market/Comptroller.sol
lending-market/AccountantDelegate.sol
lending-market/AccountantDelegator.sol
lending-market/TreasuryDelegator.sol
lending-market/CNote.sol
stableswap/BaseV1-periphery.sol
TOOLS USED
Manual Analysis
MITIGATION
Write the error strings so that they do not exceed 32 bytes. For further gas savings, consider also using custom errors.
Tautologies
PROBLEM
Tautologies should be avoided as they waste gas.
PROOF OF CONCEPT
Instances include:
lending-market/NoteInterest.sol
ir
is a uint256, hence always >= 0.TOOLS USED
Manual Analysis
MITIGATION
Replace
with
Tight Variable Packing
PROBLEM
Solidity contracts have contiguous 32 bytes (256 bits) slots used in storage. By arranging the variables, it is possible to minimize the number of slots used within a contract's storage and therefore reduce deployment costs.
address type variables are each of 20 bytes size (way less than 32 bytes). However, they here take up a whole 32 bytes slot (they are contiguous).
As uint8 and bool type variables are of size 1 byte, there's a slot here that can get saved by moving an address closer to a bool and or a uint8
PROOF OF CONCEPT
Instances include:
stableswap/BaseV1-core.sol
TOOLS USED
Manual Analysis
MITIGATION
Place
factory
beforedecimals
to save one storage slotUnchecked arithmetic
IMPACT
The default "checked" behavior costs more gas when adding/diving/multiplying, because under-the-hood those checks are implemented as a series of opcodes that, prior to performing the actual arithmetic, check for under/overflow and revert if it is detected.
if it can statically be determined there is no possible way for your arithmetic to under/overflow (such as a condition in an if statement), surrounding the arithmetic in an
unchecked
block will save gasPROOF OF CONCEPT
Instances include:
lending-market/WETH.sol
lending-market/GovernorBravoDelegate.sol
lending-market/Comptroller.sol
stableswap/BaseV1-core.sol
stableswap/BaseV1-periphery.sol
TOOLS USED
Manual Analysis
MITIGATION
Place the arithmetic operations in an
unchecked
blockUnnecessary functions
IMPACT
As of Solidity 0.8.0, underflow and overflow checks are default in mathematical operations. Using functions to perform these checks is redundant and wastes gas.
PROOF OF CONCEPT
Instances include:
lending-market/GovernorBravoDelegate.sol
lending-market/NoteInterest.sol
TOOLS USED
Manual Analysis
MITIGATION
Remove these functions
Unnecessary computation
IMPACT
There are several instances where a local variable is used but is not necessary. For instance, when emitting an event that includes a new and an old value, it is cheaper in gas to avoid caching the old value in memory. Instead, emit the event, then save the new value in storage.
PROOF OF CONCEPT
Instances include:
lending-market/GovernorBravoDelegate.sol
scope:
_initiate()
scope:
_setPendingAdmin()
scope:
_acceptAdmin()
lending-market/Comptroller.sol
scope:
_setPriceOracle()
scope:
_setCloseFactor()
scope:
_setLiquidationIncentive()
scope:
_setBorrowCapGuardian()
scope:
_setPauseGuardian()
lending-market/TreasuryDelegate.sol
scope:
queryCantoBalance()
scope:
querynoteBalance()
lending-market/TreasuryDelegator.sol
scope:
_setImplementation
lending-market/NoteInterest.sol
scope:
_setBaseRatePerYear
scope:
_setAdjusterCoefficient
scope:
_setUpdateFrequency
manifest/Proposal-Store.sol
TOOLS USED
Manual Analysis
MITIGATION
Remove the unnecessary local variables:
e.g:
with