Platform: Code4rena
Start Date: 25/08/2022
Pot Size: $75,000 USDC
Total HM: 35
Participants: 147
Period: 7 days
Judge: 0xean
Total Solo HM: 15
Id: 156
League: ETH
Rank: 63/147
Findings: 2
Award: $90.30
๐ Selected for report: 0
๐ Solo Findings: 0
๐ Selected for report: zzzitron
Also found by: 0x040, 0x1f8b, 0x52, 0x85102, 0xDjango, 0xNazgul, 0xNineDec, 0xSky, 0xSmartContract, 0xkatana, 8olidity, Aymen0909, Bahurum, BipinSah, Bnke0x0, CRYP70, CertoraInc, Ch_301, Chandr, Chom, CodingNameKiki, Deivitto, DimSon, Diraco, ElKu, EthLedger, Funen, GalloDaSballo, Guardian, IllIllI, JansenC, Jeiwan, Lambda, LeoS, Margaret, MasterCookie, PPrieditis, PaludoX0, Picodes, PwnPatrol, RaymondFam, ReyAdmirado, Rohan16, Rolezn, Ruhum, Sm4rty, StevenL, The_GUILD, TomJ, Tomo, Trust, Waze, __141345__, ajtra, ak1, apostle0x01, aviggiano, bin2chen, bobirichman, brgltd, c3phas, cRat1st0s, carlitox477, cccz, ch13fd357r0y3r, cloudjunky, cryptphi, csanuragjain, d3e4, datapunk, delfin454000, devtooligan, dipp, djxploit, durianSausage, eierina, enckrish, erictee, fatherOfBlocks, gogo, grGred, hansfriese, hyh, ignacio, indijanc, itsmeSTYJ, ladboy233, lukris02, martin, medikko, mics, natzuu, ne0n, nxrblsrpr, okkothejawa, oyc_109, p_crypt0, pfapostol, prasantgupta52, rajatbeladiya, rbserver, reassor, ret2basic, robee, rokinot, rvierdiiev, shenwilly, sikorico, sorrynotsorry, tnevler, tonisives, w0Lfrum, yixxas
54.313 DAI - $54.31
[L01] A floating pragma is set. [L02] A different pragma is set.
[NC01] Use of block timestamp
The current pragma Solidity directive is >=0.8.0" for some contracts. It is recommended to specify a fixed compiler version to ensure that the bytecode produced does not vary between builds. This is especially important if you rely on bytecode-level verification of the code.
Lock the pragma.
IBondCallback.sol#L2 IHeart.sol#L2 IOperator.sol#L2
The current pragma Solidity directive is "0.8.15" but there is others contracts that is using a different pragma version ">=0.8.0". It's cleaner to use the same versions.
INSTR.sol#L2 MINTR.sol#L2 TRSRY.sol#L2 RANGE.sol#L2 PRICE.sol#L2 VOTES.sol#L2 Kernel.sol#L2 KernelUtils.sol#L2 TreasuryCustodian.sol#L2 Operator.sol#L2 BondCallback.sol#L2 Heart.sol#L2 Governance.sol#L2 PriceConfig.sol#L2 VoterRegistration.sol#L2 IHeart.sol#L2 IOperator.sol#L2 IBondCallback.sol#L2
Block timestamps have historically been used for a variety of applications, such as entropy for random numbers (see the Entropy Illusion for further details), locking funds for periods of time, and various state-changing conditional statements that are time-dependent. Miners have the ability to adjust timestamps slightly, which can prove to be dangerous if block timestamps are used incorrectly in smart contracts.
Block timestamps should not be used for entropy or generating random numbersโi.e., they should not be the deciding factor (either directly or through some derivation) for winning a game or changing an important state.
Time-sensitive logic is sometimes required; e.g., for unlocking contracts (time-locking), completing an ICO after a few weeks, or enforcing expiry dates. It is sometimes recommended to use block.number and an average block time to estimate times; with a 10 second block time, 1 week equates to approximately, 60480 blocks. Thus, specifying a block number at which to change a contract state can be more secure, as miners are unable to easily manipulate the block number.
Heart.sol#63 Heart.sol#94 Heart.sol#131 Governance.sol#L171 Governance.sol#L212 Governance.sol#L227 Governance.sol#L231 Governance.sol#L272 PRICE.sol#L143 PRICE.sol#L146 PRICE.sol#L165 PRICE.sol#L171 PRICE.sol#L215 RANGE.sol#L85 RANGE.sol#L92 RANGE.sol#L136 RANGE.sol#L138 RANGE.sol#L148 RANGE.sol#L150 RANGE.sol#L191 RANGE.sol#L200 RANGE.sol#L207 RANGE.sol#L231 RANGE.sol#L233 Operator.sol#L210 Operator.sol#L216 Operator.sol#L404 Operator.sol#L456 Operator.sol#L708 Operator.sol#L720
๐ Selected for report: pfapostol
Also found by: 0x040, 0x1f8b, 0x85102, 0xDjango, 0xNazgul, 0xNineDec, 0xSmartContract, 0xkatana, Amithuddar, Aymen0909, Bnke0x0, CertoraInc, Chandr, CodingNameKiki, Deivitto, Dionysus, Diraco, ElKu, Fitraldys, Funen, GalloDaSballo, Guardian, IllIllI, JC, JansenC, Jeiwan, LeoS, Metatron, Noah3o6, RaymondFam, ReyAdmirado, Rohan16, RoiEvenHaim, Rolezn, Ruhum, Saintcode_, Shishigami, Sm4rty, SooYa, StevenL, Tagir2003, The_GUILD, TomJ, Tomo, Waze, __141345__, ajtra, apostle0x01, aviggiano, bobirichman, brgltd, c3phas, cRat1st0s, carlitox477, cccz, ch0bu, chrisdior4, d3e4, delfin454000, djxploit, durianSausage, erictee, exolorkistis, fatherOfBlocks, gogo, grGred, hyh, ignacio, jag, karanctf, kris, ladboy233, lukris02, m_Rassska, martin, medikko, natzuu, ne0n, newfork01, oyc_109, peiw, rbserver, ret2basic, robee, rokinot, rvierdiiev, sikorico, simon135, tnevler, zishansami
35.995 DAI - $35.99
[G01] Post-increment/decrement cost more gas then pre-increment/decrement [G02] Array length should not be looked up in every loop of a for-loop [G03] Operatos <= or >= cost more gas than operators < or > [G04] != 0 is cheaper than > 0 [G05] Variable1 = Variable1 + (-) Variable2 is cheaper in gas cost than variable1 += (-=) variable2. [G06] Using private rather than public for constants [G07] Don't compare boolean expressions to boolean literals [G08] Usage of uints/ints smaller than 32 Bytes (256 bits) incurs overhead [G09] Initialize variables with default values are not needed [G10] Using bools for storage incurs overhead [G11] Multiplication/division by two should use bit shifting [G12] Calldata vs Memory [G13] Use a more recent version of solidity [G14] Using storage instead of memory for structs/arrays [G15] Tight variable packing [G16] Move variable declaration before is going to be used [G17] Refactoring code [G18] Use unchecked when it's not possible to overflow [G19] Internal functions only called once can be inlined to save gas [G20] Remove unused functions
++I (--I) cost less gas than I++ (I--) especially in a loop.
contract TestPost { function testPost() public { uint256 i; i++; } } contract TestPre { function testPre() public { uint256 i; ++i; } }
KernelUtils.sol#L49 KernelUtils.sol#L64 Operator.sol#L488 Operator.sol#L670 Operator.sol#L686
Storage array length checks incur an extra Gwarmaccess (100 gas) per loop. Store the array length in a variable and use it in the for loop helps to save gas.
contract TestForLength { function testArrayLength() public { uint256[] memory array = new uint256[](10); for(uint256 i; i < array.length; ){ ++i; } } } contract TestForCachLength { function testArrayLength() public { uint256[] memory array = new uint256[](10); uint256 arrayLen = array.length; for(uint256 i; i < arrayLen; ){ ++i; } } }
Change all <= / >= operators for < / > and remember to increse / decrese in consecuence to maintain the logic (example, a <= b for a < b + 1)
contract TestMaxEqual { function testMaxEqual() public { uint256 i = 1; if (i >= 1){ i++; } } } contract TestMax { function TestMax() public { uint256 i = 1; if (i > 0){ i++; } } }
Operator.sol#L210 Operator.sol#L211 Operator.sol#L216 Operator.sol#L217 Operator.sol#L486 Operator.sol#L667 Operator.sol#L683
Replace all > 0 for != 0
TRSRY.sol#L96 TRSRY.sol#L97 TRSRY.sol#L115 TRSRY.sol#L116 TRSRY.sol#L131 TRSRY.sol#L132 PRICE.sol#L136 PRICE.sol#L138 PRICE.sol#L222 VOTES.sol#L56 VOTES.sol#L58 BondCallback.sol#L143 BondCallback.sol#L144 Heart.sol#L103 Governance.sol#L194 Governance.sol#L198 Governance.sol#L252 Governance.sol#L254
If needed, the value can be read from the verified contract source code. Savings are due to the compiler not having to create non-payable getter functions for deployment calldata, and not adding another entry to the method ID table.
RANGE.sol#L65 PRICE.sol#L59 Operator.sol#L89 Governance.sol#L121 Governance.sol#L124 Governance.sol#L127 Governance.sol#L130 Governance.sol#L133 Governance.sol#L137
if (<x> == true) => if (<x>), if (<x> == false) => if (!<x>)
Governance.sol#L223 Governance.sol#L306
When using elements that are smaller than 32 bytes, your contract's gas usage may be higher. This is because the EVM operates on 32 bytes at a time. Therefore, if the element is smaller than that, the EVM must use more operations in order to reduce the size of the element from 32 bytes to the desired size. Use a larger size then downcast where needed
RANGE.sol#L45 PRICE.sol#L44 PRICE.sol#L47 PRICE.sol#L50 PRICE.sol#L53 PRICE.sol#L56 PRICE.sol#L59 PRICE.sol#L84 PRICE.sol#L87 PRICE.sol#L127 PRICE.sol#L161 PRICE.sol#L185 Operator.sol#L83 Operator.sol#L86 Operator.sol#L89 Operator.sol#L371 Operator.sol#L372 Operator.sol#L375 Operator.sol#L418 Operator.sol#L426 Operator.sol#L427 Operator.sol#L430 Operator.sol#L485 Operator.sol#L665 IOperator.sol#L13 IOperator.sol#L14 IOperator.sol#L15 IOperator.sol#L16 IOperator.sol#L17 IOperator.sol#L18 IOperator.sol#L19 IOperator.sol#L20 IOperator.sol#L31 IOperator.sol#L32 IOperator.sol#L33
If a variable is not set/initialized, it is assumed to have the default value (0 for uint, false for bool, address(0) for address๏ฟฝ). Explicitly initializing it with its default value is an anti-pattern and wastes gas.
Kernel.sol#L397 KernelUtils.sol#L43 KernelUtils.sol#L58
Assuming than uint's less than 256 are updated to uint256. Operator.sol#L127 Operator.sol#L129 Operator.sol#403 Operator.sol#455
Use uint256(1) and uint256(2) for true/false to avoid a Gwarmaccess (100 gas), and to avoid Gsset (20000 gas) when changing from 'false' to 'true', after having been 'true' in the past
Kernel.sol#L113 Kernel.sol#L181 Kernel.sol#L194 Kernel.sol#L197 RANGE.sol#L44 PRICE.sol#L62 Operator.sol#L63 Operator.sol#L66 Operator.sol#L735 BondCallback.sol#L24 Heart.sol#L33 Governance.sol#L105 Governance.sol#L117 IOperator.sol#L34
<x> * 2 is equivalent to <x> << 1 and <x> / 2 is the same as <x> >> 1. The MUL and DIV opcodes cost 5 gas, whereas SHL and SHR only cost 3 gas
contract TestDiv2 { function TestDivBy2 () public returns (uint256){ uint256 i = 4; i = i / 2; return i; } } contract TestDivShift { function TestDivByShift () public returns (uint256){ uint256 i = 4; i = i >> 1; return i; } }
Operator.sol#L372 Operator.sol#L419 Operator.sol#L420 Operator.sol#L427
Use calldata instead of memory in a function parameter when you are only to read the data can save gas by storing it in calldata
PRICE.sol#L205 TreasuryCustodian.sol#L53 BondCallback.sol#L152 PriceConfig.sol#L45
Use a solidity version of at least 0.8.16 to have more efficient code for checked addition and subtraction.
Kernel.sol#L2 KernelUtils.sol#L2 TRSRY.sol#L2 MINTR.sol#L2 RANGE.sol#L2 PRICE.sol#L2 VOTES.sol#L2 INSTR.sol#L2 TreasuryCustodian.sol#L2 Operator.sol#L2 BondCallback.sol#L2 Heart.sol#L2 PriceConfig.sol#L2 Governance.sol#L2 VoterRegistration.sol#L2 IBondCallback.sol#L2 IHeart.sol#L2 IOperator.sol#L2
When retrieving data from a memory location, assigning the data to a memory variable causes all fields of the struct/array to be read from memory, resulting in a Gcoldsload (2100 gas) for each field of the struct/array. When reading fields from new memory variables, they cause an extra MLOAD instead of a cheap stack read. Rather than declaring a variable with the memory keyword, it is much cheaper to declare a variable with the storage keyword and cache all fields that need to be read again in a stack variable, because the fields actually read will only result in a Gcoldsload. The only case where the entire struct/array is read into a memory variable is when the entire struct/array is returned by a function, passed to a function that needs memory, or when the array/struct is read from another store array/struct.
Kernel.sol#L379 Kernel.sol#L398 RANGE.sol#L80 Operator.sol#L96 Operator.sol#L97 BondCallback.sol#L179 Governance.sol#L206
Reordering the variables's declaration it's possible to save some slots. Apply the following changes.
/// Operator variables, defined in the interface on the external getter functions Status internal _status; Config internal _config; /// @notice Whether the Operator has been initialized - bool public initialized; /// @notice Whether the Operator is active - bool public active; /// Modules OlympusPrice internal PRICE; OlympusRange internal RANGE; OlympusTreasury internal TRSRY; OlympusMinter internal MINTR; /// External contracts /// @notice Auctioneer contract used for cushion bond market deployments IBondAuctioneer public auctioneer; /// @notice Callback contract used for cushion bond market payouts IBondCallback public callback; /// Tokens /// @notice OHM token contract ERC20 public immutable ohm; - uint8 public immutable ohmDecimals; /// @notice Reserve token contract ERC20 public immutable reserve; uint8 public immutable reserveDecimals; + uint8 public immutable ohmDecimals; + bool public initialized; + bool public active; /// Constants uint32 public constant FACTOR_SCALE = 1e4;
It's important to declare the variable before it's use and after to the if/require conditions becase of it's possible to save gas when the if's/require conditions are true and the execution doesn't follow.
function endorseProposal(uint256 proposalId_) external { - uint256 userVotes = VOTES.balanceOf(msg.sender); if (proposalId_ == 0) { revert CannotEndorseNullProposal(); } Instruction[] memory instructions = INSTR.getInstructions(proposalId_); if (instructions.length == 0) { revert CannotEndorseInvalidProposal(); } // undo any previous endorsement the user made on these instructions uint256 previousEndorsement = userEndorsementsForProposal[proposalId_][msg.sender]; totalEndorsementsForProposal[proposalId_] -= previousEndorsement; // reapply user endorsements with most up-to-date votes + uint256 userVotes = VOTES.balanceOf(msg.sender); userEndorsementsForProposal[proposalId_][msg.sender] = userVotes; totalEndorsementsForProposal[proposalId_] += userVotes; emit ProposalEndorsed(proposalId_, msg.sender, userVotes); }
function vote(bool for_) external { - uint256 userVotes = VOTES.balanceOf(msg.sender); if (activeProposal.proposalId == 0) { revert NoActiveProposalDetected(); } if (userVotesForProposal[activeProposal.proposalId][msg.sender] > 0) { revert UserAlreadyVoted(); } + uint256 userVotes = VOTES.balanceOf(msg.sender); if (for_) { yesVotesForProposal[activeProposal.proposalId] += userVotes; } else { noVotesForProposal[activeProposal.proposalId] += userVotes; } userVotesForProposal[activeProposal.proposalId][msg.sender] = userVotes; VOTES.transferFrom(msg.sender, address(this), userVotes); emit WalletVoted(activeProposal.proposalId, msg.sender, for_, userVotes); }
Governance.sol#L180-L201 Governance.sol#L240-L262
In the following case it's possible to save gas checking the common condition once instead of twice.
-if ( - uint48(block.timestamp) >= RANGE.lastActive(true) + uint48(config_.regenWait) && - _status.high.count >= config_.regenThreshold -) { - _regenerate(true); -} -if ( - uint48(block.timestamp) >= RANGE.lastActive(false) + uint48(config_.regenWait) && - _status.low.count >= config_.regenThreshold -) { - _regenerate(false); -} +if (uint48(block.timestamp) >= RANGE.lastActive(true) + uint48(config_.regenWait)) +{ + if (_status.high.count >= config_.regenThreshold) + { + _regenerate(true); + } + if (_status.low.count >= config_.regenThreshold) + { + _regenerate(false); + } +}
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.
For all for-loops in the code it is possible to change as the following example.
for (uint256 i;i < X;){ -- code -- unchecked { ++i; } }
contract TestWithoutUnchecked { function Test() public { for(uint256 i; i < 10; ){ ++i; } } } contract TestWitUnchecked { function Test() public { for(uint256 i; i < 10; ){ unchecked { ++i; } } } }
Not inlining costs 20 to 40 gas because of two extra JUMP instructions and additional stack operations needed for function calls.
Remove the following functions in Kernel.sol (getModuleAddress) to save gas due to it's internal and it's not used inside the contract.