Open code423n4 opened 2 years ago
Warden created this issue as a placeholder, because their submission was too large for the contest form. They then emailed their md file to our team. I've updated this issue with their md file content.
Note: my original copy/paste did not capture all of the warden's content. I've updated this issue to now contain the entirety of the original submission. I've also notified the sponsor. Contest has not yet moved to judging.
Vulnerability details:
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
-loopsrequire()
/revert()
strings longer than 32 bytes cost extra gasNot using the named return variables when a function returns, wastes deployment gas
Remove unused local variable
Using
bool
s for storage incurs overheadhttps://github.com/OpenZeppelin/openzeppelin-contracts/blob/58f635312aa21f947cae5f8578638a85aa2519f5/contracts/security/ReentrancyGuard.sol#L23-L27
Using
> 0
costs more gas than!= 0
when used on auint
in arequire()
statementIt costs more gas to initialize variables to zero than to let the default of zero be applied
internal
functions only called once can be inlined to save gasUsing
calldata
instead ofmemory
for read-only arguments inexternal
functions saves gas++i
costs less gas than++i
, especially when it's used infor
-loops (--i
/i--
too)Usage of
uints
/ints
smaller than 32 bytes (256 bits) incurs overheadhttps://docs.soliditylang.org/en/v0.8.11/internals/layout_in_storage.html Use a larger size then downcast where needed
Expressions for constant values such as a call to
keccak256()
, should useimmutable
rather thanconstant
See this issue for a detail description of the issue
Using
private
rather thanpublic
for constants, saves gasIf needed, the value can be read from the verified contract source code
Don't compare boolean expressions to boolean literals
if ( == true)
=>if ()
,if ( == false)
=>if (!)
Duplicated
require()
/revert()
checks should be refactored to a modifier or functionMultiplication/division by two should use bit shifting
* 2
is equivalent to<< 1
and/ 2
is the same as>> 1
Stack variable used as a cheaper cache for a state variable is only used once
If the variable is only accessed once, it's cheaper to use the state variable directly that one time
require()
orrevert()
statements that check input arguments should be at the top of the functionChecks that involve constants should come before checks that involve state variables
Superfluous event fields
block.timestamp
andblock.number
are added to event information by default so adding them manually wastes gasUse custom errors rather than
revert()
/require()
strings to save deployment gasFunctions guaranteed to revert when called by normal users can be marked
payable
If a function modifier such as
onlyOwner
is used, the function will revert if a normal user tries to pay the function. Marking the function aspayable
will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided.