Use a solidity version of at least 0.8.0 to get overflow protection without SafeMath
Use a solidity version of at least 0.8.2 to get compiler automatic inlining
Use a solidity version of at least 0.8.3 to get better struct packing and cheaper multiple storage reads
Use a solidity version of at least 0.8.4 to get custom errors, which are cheaper at deployment than revert()/require() strings
Use a solidity version of at least 0.8.10 to have external calls skip contract existence checks if the external call has a return value
Use a solidity version of at least 0.8.2 to get compiler automatic inlining
Use a solidity version of at least 0.8.3 to get better struct packing and cheaper multiple storage reads
Use a solidity version of at least 0.8.4 to get custom errors, which are cheaper at deployment than revert()/require() strings
Use a solidity version of at least 0.8.10 to have external calls skip contract existence checks if the external call has a return value
These unused state variables are wasting a lot of gas. The compiler automatically creates non-payable getter functions which is expensive and each variable, since it is given a value, costs a ton of gas to initialize. Variables initialized to zero cost G~sreset~ (2900 gas) and variables initialized to non-zero cost G~sset~ (20000 gas)
// ========== Not used ==========
//boost
address public boostPayment = address(0);
uint256 public maximumBoostPayment = 0;
uint256 public boostRate = 10000;
uint256 public nextMaximumBoostPayment = 0;
uint256 public nextBoostRate = 10000;
uint256 public constant denominator = 10000;
// ==============================
Changing from immutable to just private wastes a lot of gas
One of the changes made after branching this file was to remove the immutable keyword. Doing this changes the variable from a deployment-time replacement to a state variable that gets assigned, costing G~sset~ (20000 gas). The variable's value does not change after initialization, so it should remain immutable
Multiple address mappings can be combined into a single mapping of an address to a struct, where appropriate
Saves a storage slot for the mapping as well as a non-payable getter for the mapping. Depending on the circumstances and sizes of types, can avoid a G~sset~ (20000 gas). Reads and subsequent writes are also cheaper
mapping(address => uint256) public boughtAmounts;
/// Whether an account has claimed tokens
/// NOTE: can reset boughtAmounts after a claim to optimize gas
/// but we need to persist boughtAmounts
mapping(address => bool) public hasClaimed;
mapping(address => uint256) public additionalTokensEarned;
mapping(address => uint256) public lastAdditionalTokenAmount;
State variables can be packed into fewer storage slots
If variables occupying the same slot are both written the same function or by the constructor, avoids a separate G~sset~ (20000 gas). Reads of the variables are also cheaper
IERC20 public citadel; /// token to distribute (in vested xCitadel form)
Variable ordering with one fewer slots:
uint256(32):citadelPriceInAsset, uint256(32):minCitadelPriceInAsset, uint256(32):maxCitadelPriceInAsset, uint256(32):assetDecimalsNormalizationValue, user-defined(20):citadel, bool(1):citadelPriceFlag, user-defined(20):xCitadel, user-defined(20):asset, address(20):citadelPriceInAssetOracle, address(20):saleRecipient, user-defined(*):funding
Pre-calculate repeatedly-checked offsets
Reading from storage is expensive, so it saves gas when only one variable has to be read versus multiple. If there is a calculation which requires multiple storage reads, the calculation should be optimized to pre-calculate as much as possible, and store the intermediate result in storage. Replace the saleDuration variable with a private pre-calculated saleEnd timestamp, and reference that rather than checking both saleStart and saleDuration in multiple places. The duration can be calculated by subtracting the start from the end in a view function. Making this change saves a G~coldsload~ (2100 gas) for each call to buy() and saleEnded()
State variables should be cached in stack variables rather than re-reading them from storage
The instances below point to the second+ access of a state variable within a function. With each of these individually, caching will replace a G~warmaccess~ (100 gas) with a much cheaper stack read.
Less obvious optimizations flagged include having local storage variables of mappings within state variable mappings or mappings within state variable structs, having local storage variables of structs within mappings, or having local caches of state variable contracts/addresses.
Checking of equality to account is cheaper than looking up the gac role via static call, so that check should be on the left of the condition to shortcut the logic
.length should not be looked up in every loop of a for-loop
Even memory arrays incur the overhead of bit tests and bit shifts to calculate the array length. Storage array length checks incur an extra G~warmaccess~ (100 gas) PER-LOOP.
Gas Optimizations
Use a more recent version of solidity
Use a solidity version of at least 0.8.0 to get overflow protection without
SafeMath
Use a solidity version of at least 0.8.2 to get compiler automatic inlining Use a solidity version of at least 0.8.3 to get better struct packing and cheaper multiple storage reads Use a solidity version of at least 0.8.4 to get custom errors, which are cheaper at deployment thanrevert()/require()
strings Use a solidity version of at least 0.8.10 to have external calls skip contract existence checks if the external call has a return valueUse a more recent version of solidity
Use a solidity version of at least 0.8.2 to get compiler automatic inlining Use a solidity version of at least 0.8.3 to get better struct packing and cheaper multiple storage reads Use a solidity version of at least 0.8.4 to get custom errors, which are cheaper at deployment than
revert()/require()
strings Use a solidity version of at least 0.8.10 to have external calls skip contract existence checks if the external call has a return valueUnused state variables should be removed
These unused state variables are wasting a lot of gas. The compiler automatically creates non-payable getter functions which is expensive and each variable, since it is given a value, costs a ton of gas to initialize. Variables initialized to zero cost G~sreset~ (2900 gas) and variables initialized to non-zero cost G~sset~ (20000 gas)
Changing from
immutable
to justprivate
wastes a lot of gasOne of the changes made after branching this file was to remove the
immutable
keyword. Doing this changes the variable from a deployment-time replacement to a state variable that gets assigned, costing G~sset~ (20000 gas). The variable's value does not change after initialization, so it should remain immutableMultiple
address
mappings can be combined into a singlemapping
of anaddress
to astruct
, where appropriateSaves a storage slot for the mapping as well as a non-payable getter for the mapping. Depending on the circumstances and sizes of types, can avoid a G~sset~ (20000 gas). Reads and subsequent writes are also cheaper
State variables can be packed into fewer storage slots
If variables occupying the same slot are both written the same function or by the constructor, avoids a separate G~sset~ (20000 gas). Reads of the variables are also cheaper
File: external/StakedCitadelLocker.sol (line 63)
Variable ordering with two fewer slots: address:rewardTokens, mapping(32):rewardData, mapping(32):rewardDistributors, mapping(32):userRewardPerTokenPaid, mapping(32):rewards, uint256(32):lockedSupply, uint256(32):boostedSupply, user-defined:epochs, mapping(32):balances, mapping(32):userLocks, uint256(32):maximumBoostPayment, uint256(32):boostRate, uint256(32):nextMaximumBoostPayment, uint256(32):nextBoostRate, uint256(32):minimumStake, uint256(32):maximumStake, uint256(32):kickRewardPerEpoch, uint256(32):kickRewardEpochDelay, string(32):_name, string(32):_symbol, user-defined(20):stakingToken, bool(1):isShutdown, uint8(1):_decimals, address(20):boostPayment, address(20):stakingProxy
File: src/Funding.sol (line 32)
Variable ordering with one fewer slots: uint256(32):citadelPriceInAsset, uint256(32):minCitadelPriceInAsset, uint256(32):maxCitadelPriceInAsset, uint256(32):assetDecimalsNormalizationValue, user-defined(20):citadel, bool(1):citadelPriceFlag, user-defined(20):xCitadel, user-defined(20):asset, address(20):citadelPriceInAssetOracle, address(20):saleRecipient, user-defined(*):funding
Pre-calculate repeatedly-checked offsets
Reading from storage is expensive, so it saves gas when only one variable has to be read versus multiple. If there is a calculation which requires multiple storage reads, the calculation should be optimized to pre-calculate as much as possible, and store the intermediate result in storage. Replace the
saleDuration
variable with aprivate
pre-calculatedsaleEnd
timestamp, and reference that rather than checking bothsaleStart
andsaleDuration
in multiple places. The duration can be calculated by subtracting the start from the end in aview
function. Making this change saves a G~coldsload~ (2100 gas) for each call tobuy()
andsaleEnded()
File: src/KnightingRound.sol (lines 168-171)
File: src/KnightingRound.sol (lines 259-261)
State variables should be cached in stack variables rather than re-reading them from storage
The instances below point to the second+ access of a state variable within a function. With each of these individually, caching will replace a G~warmaccess~ (100 gas) with a much cheaper stack read. Less obvious optimizations flagged include having local storage variables of mappings within state variable mappings or mappings within state variable structs, having local storage variables of structs within mappings, or having local caches of state variable contracts/addresses.
Add
unchecked {}
for subtractions where the operands cannot underflow because of a previousrequire()
require(a <= b); x = b - a
=>require(a <= b); unchecked { x = b - a }
Cheaper checks should be done first
Checking of equality to
account
is cheaper than looking up thegac
role via static call, so that check should be on the left of the condition to shortcut the logic.length
should not be looked up in every loop of afor
-loopEven memory arrays incur the overhead of bit tests and bit shifts to calculate the array length. Storage array length checks incur an extra G~warmaccess~ (100 gas) PER-LOOP.
++i
/i++
should beunchecked{++i}
/unchecked{++i}
when it is not possible for them to overflow, as is the case when used infor
- andwhile
-loops