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: 7/147
Findings: 6
Award: $2,600.95
π Selected for report: 1
π Solo Findings: 1
π Selected for report: 0x1f8b
1905.4132 DAI - $1,905.41
https://github.com/code-423n4/2022-08-olympus/blob/b5e139d732eb4c07102f149fb9426d356af617aa/src/policies/Governance.sol#L167 https://github.com/code-423n4/2022-08-olympus/blob/b5e139d732eb4c07102f149fb9426d356af617aa/src/policies/Governance.sol#L66
It is possible to overwrite proposals in certain circumstances. The method Governance.submitProposal
doesn't check if the proposalId
(stored in a different contract) exists already as a valid proposal in getProposalMetadata
.
If the project update the kernel module "INSTR
" and reconfigure proposals and call INSTR.store(instructions_);
, the counter may return a proposalId
of an existing proposal and overwrite an existing previous one.
This is due to the fact that the proposals are saved in a mapping of a contract that is not related to the one that returns the counters, and furthermore, they do not check that the record already exists.
uint256 proposalId = INSTR.store(instructions_); getProposalMetadata[proposalId] = ProposalMetadata( title_, msg.sender, block.timestamp, proposalURI_ );
INSTR
contract or ensure that the proposal doesn't exists.#0 - fullyallocated
2022-09-01T21:51:31Z
Agreed with the validity of the circumstance, but it is contingent on us upgrading the contract in an unexpected way. Is the same as saying "if you upgrade a contract incorrectly it can break the dependencies".
#1 - 0xean
2022-09-16T16:34:42Z
Going to downgrade to medium based on some external requirements needing to be in placed to be realized.
Assets not at direct risk, but the function of the protocol or its availability could be impacted, or leak value with a hypothetical attack path with stated assumptions, but external requirements.
Function f the protocol could be impacted and there are external requirements.
π Selected for report: rbserver
Also found by: 0x1f8b, Bahurum, csanuragjain, yixxas
Because when endorsing a proposal, the sender's vote balance is checked, but these votes are not locked, nor are the user's endorsements eliminated when transferred, if a user who endorses 1 vote, then transfers that vote for another user, and this user also endorses, it will make the proposal have a total of 2 votes in endorsement only with one vote.
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 userEndorsementsForProposal[proposalId_][msg.sender] = userVotes; totalEndorsementsForProposal[proposalId_] += userVotes; emit ProposalEndorsed(proposalId_, msg.sender, userVotes); }
With the following contract, 10 contracts will be created and they will reuse the same votes, in order to increase the voting power.
pragma solidity =0.8.15; interface iGov { function endorseProposal(uint256 proposalId_) external; } interface ERC20 { function transfer(address to, uint bal) external returns (bool); } contract payload { function attack(ERC20 token, iGov gov, uint proposalId, uint votes) public { gov.endorseProposal(proposalId); // endorse proposal token.transfer(msg.sender, votes); // return funds back } } contract exploit { function attack(ERC20 token, iGov gov, uint proposalId, uint votes) public { for (uint x; x < 10; ++x) { payload p = new payload(); token.transfer(address(p), votes); // transfer funds to the payload p.attack(token, gov, proposalId, votes); } } }
#0 - csanuragjain
2022-09-02T06:26:23Z
I think this will not be possible as Votes cannot be transferred as mentioned at https://github.com/code-423n4/2022-08-olympus/blob/main/src/modules/VOTES.sol#L45 transferFrom is also not possible for public use since permissioned modifier is used
This is only possible when a malicious policy allows Vote transfer for public
#1 - bahurum
2022-09-02T13:33:33Z
Duplicate of #239
#2 - fullyallocated
2022-09-02T20:44:17Z
Duplicate of #239
#3 - 0x1f8b
2022-09-06T08:08:22Z
I think this will not be possible as Votes cannot be transferred as mentioned at https://github.com/code-423n4/2022-08-olympus/blob/main/src/modules/VOTES.sol#L45 transferFrom is also not possible for public use since permissioned modifier is used
This is only possible when a malicious policy allows Vote transfer for public
Looking at the code in scope, the restriction that the token be VOTES
or ohm
does not exist, if only these tokens are required, or the contract has to be created during deploy with said configuration, or it has to be forced by checking supportsInterface
from EIP165
#4 - 0xean
2022-09-19T17:10:45Z
closing as dupe of #239
π Selected for report: __141345__
https://github.com/code-423n4/2022-08-olympus/blob/b5e139d732eb4c07102f149fb9426d356af617aa/src/policies/Governance.sol#L240 https://github.com/code-423n4/2022-08-olympus/blob/b5e139d732eb4c07102f149fb9426d356af617aa/src/policies/Governance.sol#L265
Governance.vote
and Governance.executeProposal
are vulnerables to front-running attacks.
In Governance
contract, the methods vote
and executeProposal
uses activeProposal
to know which proposal to interact with, instead of using an argument received by the user.
The problem with this pattern is that when the tx arrives in the pool, it could be a different proposal ID, due to an attack or because the normal platform is used, making it vulnerable to a front-running attack.
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(); } 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); }
If the user votes for proposal 1, which is the active one, but his tx arrives due to gas problems when this scenario has changed and now it is proposal 2 the active one, the user will have voted for a different proposal than the one desired.
proposalId
as an argument.#0 - fullyallocated
2022-09-07T23:18:39Z
Duplicate of #273
#1 - 0xean
2022-09-16T16:41:02Z
downgraded to medium, see dupe.
11.0311 DAI - $11.03
https://github.com/code-423n4/2022-08-olympus/blob/2a0b515012b4a40076f6eac487f7816aafb8724a/src/modules/PRICE.sol#L161 https://github.com/code-423n4/2022-08-olympus/blob/2a0b515012b4a40076f6eac487f7816aafb8724a/src/modules/PRICE.sol#L170
In PRICE.getCurrentPrice, we are using latestRoundData
, but there are no validations that the data is not stale.
The current code is:
function getCurrentPrice() public view returns (uint256) { if (!initialized) revert Price_NotInitialized(); // Get prices from feeds uint256 ohmEthPrice; uint256 reserveEthPrice; { (, int256 ohmEthPriceInt, , uint256 updatedAt, ) = _ohmEthPriceFeed.latestRoundData(); // Use a multiple of observation frequency to determine what is too old to use. // Price feeds will not provide an updated answer if the data doesn't change much. // This would be similar to if the feed just stopped updating; therefore, we need a cutoff. if (updatedAt < block.timestamp - 3 * uint256(observationFrequency)) revert Price_BadFeed(address(_ohmEthPriceFeed)); ohmEthPrice = uint256(ohmEthPriceInt); int256 reserveEthPriceInt; (, reserveEthPriceInt, , updatedAt, ) = _reserveEthPriceFeed.latestRoundData(); if (updatedAt < block.timestamp - uint256(observationFrequency)) revert Price_BadFeed(address(_reserveEthPriceFeed)); reserveEthPrice = uint256(reserveEthPriceInt); } // Convert to OHM/RESERVE price uint256 currentPrice = (ohmEthPrice * _scaleFactor) / reserveEthPrice; return currentPrice; }
But is missing the checks to validate the data is stale:
- (, int256 ohmEthPriceInt, , uint256 updatedAt, ) = _ohmEthPriceFeed.latestRoundData(); + (uint80 round, int256 ohmEthPriceInt, , uint256 updatedAt, uint80 answeredInRound) = _ohmEthPriceFeed.latestRoundData(); // Use a multiple of observation frequency to determine what is too old to use. // Price feeds will not provide an updated answer if the data doesn't change much. // This would be similar to if the feed just stopped updating; therefore, we need a cutoff. + if (ohmEthPriceInt <= 0 || answeredInRound < round) + revert Price_BadFeed(address(_ohmEthPriceFeed)); if (updatedAt < block.timestamp - 3 * uint256(observationFrequency)) revert Price_BadFeed(address(_ohmEthPriceFeed)); ohmEthPrice = uint256(ohmEthPriceInt); int256 reserveEthPriceInt; - (, reserveEthPriceInt, , updatedAt, ) = _reserveEthPriceFeed.latestRoundData(); + (uint80 round, reserveEthPriceInt, , updatedAt, uint80 answeredInRound) = _reserveEthPriceFeed.latestRoundData(); + if (reserveEthPriceInt <= 0 || answeredInRound < round) + revert Price_BadFeed(address(_ohmEthPriceFeed)); if (updatedAt < block.timestamp - uint256(observationFrequency)) revert Price_BadFeed(address(_reserveEthPriceFeed)); reserveEthPrice = uint256(reserveEthPriceInt);
Note that an inaccurate price data could quickly lead to a large loss of funds.
#0 - Oighty
2022-09-06T18:56:58Z
Duplicate. See comment on #441.
π 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.3158 DAI - $54.32
The pragma version used are:
pragma solidity >=0.8.0; pragma solidity 0.8.15;
The minimum required version must be 0.8.16; otherwise, contracts will be affected by the following important bug fixes:
abi.encodeCall
in place of fixed bytes arguments.calldatasize()
in all cases.bytes
arrays.Apart from these, there are several minor bug fixes and improvements.
The comment of the ensureValidRole
method of the KernelUtils
contract is either incorrect or the logic does not match the comment, since the character '\0' and '_' are allowed, both of which are not mentioned in the comment.
Recommended change:
if ((char < 0x61 || char > 0x7A) && char != 0x5f && char != 0x00) { - revert InvalidRole(role_); // a-z only + revert InvalidRole(role_); // a-z, _ or \0 only }
Affected source code:
The following methods have a lack checks if the received argument is an address, it's good practice in order to reduce human error to check that the address specified in the constructor or initialize is different than address(0)
.
Affected source code for address(0)
:
Affected source code for array checks:
The method submitProposal
in the Governance
contract doesn't check that the instructions
array is empty. This check will be done in endorseProposal
, so it's not needed to allow an empty proposal.
Recommended change:
if (instructions_.length == 0) { revert CannotEndorseInvalidProposal(); }
The code that contains "open todos" reflects that the development is not finished and that the code can change a posteriori, prior release, with or without audit.
Affected source code:
// TODO Currently allows anyone to revoke any approval EXCEPT activated policies.
// TODO must reorg policy storage to be able to check for deactivated policies.
// TODO Make sure
policy_
is an actual policy and not a random address.
// TODO determine if this should use the last price from the MA or recalculate the current price, ideally last price is ok since it should have been just updated and should include check against secondary?
The Kernel.setActiveStatus
method does not emit an event when setting the isActive
state variable, something that it's very important for dApps and users.
Affected source code:
The RANGE
contract has regenerate
and updatePrices
methods where a user-supplied argument can be passed as zero to the multiply operation.
This will affect range prices, so funds and trades could be affected if the allowed
account decides to call this argument with 0 values.
Reference:
Affected source code:
This is a terrible practice that reduces the readability and auditability of the code.
Affected source code:
OlympusPriceConfig
is PriceConfig.solOlympusInstructions
is INSTR.solOlympusTreasury
is TRSRY.solOlympusMinter
is MINTR.solOlympusPrice
is PRICE.solOlympusRange
is RANGE.solOlympusVotes
is VOTES.solOlympusHeart
is Heart.solIn the method executeProposal
of Governance
contract, an oveflow is raised when there are more NO votes than YES, that's not a good practice an it make worse the user experience, it's better to control this kind of errors in order to show an custom error.
function executeProposal() external { uint256 netVotes = yesVotesForProposal[activeProposal.proposalId] - noVotesForProposal[activeProposal.proposalId];
Affected source code:
π 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
130.1273 DAI - $130.13
It's cheaper to store the length of the array inside a local variable and iterate over it.
Affected source code:
It's compared a boolean value using == true
or == false
, instead of using the boolean value. NOT
opcode, it's cheaper to use EQUAL
or NOTEQUAL
when the value it's false, or just the value without == true
when it's true, because it will use less opcodes inside the VM.
Proof of concept (without optimizations):
pragma solidity 0.8.16; contract TesterA { function testEqual(bool a) public view returns (bool) { return a == true; } } contract TesterB { function testNot(bool a) public view returns (bool) { return a; } }
Gas saving executing: 18 per entry for == true
TesterA.testEqual: 21814 TesterB.testNot: 21796
pragma solidity 0.8.16; contract TesterA { function testEqual(bool a) public view returns (bool) { return a == false; } } contract TesterB { function testNot(bool a) public view returns (bool) { return !a; } }
Gas saving executing: 15 per entry for == false
TesterA.testEqual: 21814 TesterB.testNot: 21799
Affected source code:
Use the value instead of == true
:
Using compound assignment operator for state variables (like State += X
or State -= X
...) it's more expensive than use operator assignment (like State = State + X
or State = State - X
...).
Proof of concept (without optimizations):
pragma solidity 0.8.15; contract TesterA { uint private _a; function testShort() public { _a += 1; } } contract TesterB { uint private _a; function testLong() public { _a = _a + 1; } }
Gas saving executing: 13 per entry
TesterA.testShort: 43507 TesterB.testLong: 43494
Affected source code:
Shifting one to the right will calculate a division by two.
he SHR
opcode only requires 3 gas, compared to the DIV
opcode's consumption of 5. Additionally, shifting is used to get around Solidity's division operation's division-by-0 prohibition.
Proof of concept (without optimizations):
pragma solidity 0.8.15; contract TesterA { function testDiv(uint a) public returns (uint) { return a / 2; } } contract TesterB { function testShift(uint a) public returns (uint) { return a >> 1; } }
Gas saving executing: 172 per entry
TesterA.testDiv: 21965 TesterB.testShift: 21793
Affected source code:
++i
costs less gas compared to i++
or i += 1
++i
costs less gas compared to i++
or i += 1
for unsigned integer, as pre-increment is cheaper (about 5 gas per iteration). This statement is true even with the optimizer enabled.
i++
increments i
and returns the initial value of i
. Which means:
uint i = 1; i++; // == 1 but i == 2
But ++i
returns the actual incremented value:
uint i = 1; ++i; // == 2 and i == 2 too, so no need for a temporary variable
In the first case, the compiler has to create a temporary variable (when used) for returning 1
instead of 2
I suggest using ++i
instead of i++
to increment the value of an uint variable. Same thing for --i
and i--
Keep in mind that this change can only be made when we are not interested in the value returned by the operation, since the result is different, you only have to apply it when you only want to increase a counter.
Affected source code:
If a variable is not set/initialized, the default value is assumed (0, false
, 0x0 ... depending on the data type). You are simply wasting gas if you directly initialize it with its default value.
Proof of concept (without optimizations):
pragma solidity 0.8.15; contract TesterA { function testInit() public view returns (uint) { uint a = 0; return a; } } contract TesterB { function testNoInit() public view returns (uint) { uint a; return a; } }
Gas saving executing: 8 per entry
TesterA.testInit: 21392 TesterB.testNoInit: 21384
Affected source code:
bool
to uint256
can save gasBecause each write operation requires an additional SLOAD
to read the slot's contents, replace the bits occupied by the boolean, and then write back, booleans
are more expensive than uint256
or any other type that uses a complete word. This cannot be turned off because it is the compiler's defense against pointer aliasing and contract upgrades.
Reference:
Also, this is applicable to integer types different than uint256
or int56
.
Affected source code for booleans
:
Affected source code for integers
:
KEYCODE
It is possible to optimize the KEYCODE
method from the Module
contract as shown below, or send the keycode to the base constructor to use the immutable in the base contract, as shown below.
abstract contract Module is KernelAdapter { KeyCode private immutable _KEYCODE; constructor(Kernel kernel_, string memory keycode) KernelAdapter(kernel_) { _KEYCODE = toKeycode("MINTR"); } function KEYCODE() public pure virtual returns (Keycode) { return _KEYCODE; } ... } contract OlympusTreasury is Module, ReentrancyGuard { ... constructor(Kernel kernel_) Module(kernel_, "PRICE") {} ... }
MINTR.KEYCODE
Recommended changes:
+ KeyCode private immutable _KEYCODE = toKeycode("MINTR"); function KEYCODE() public pure override returns (Keycode) { + return _KEYCODE; - return toKeycode("MINTR"); }
Affected source code:
INSTR.KEYCODE
Recommended changes:
+ KeyCode private immutable _KEYCODE = toKeycode("INSTR"); function KEYCODE() public pure override returns (Keycode) { + return _KEYCODE; - return toKeycode("INSTR"); }
Affected source code:
VOTES.KEYCODE
Recommended changes:
+ KeyCode private immutable _KEYCODE = toKeycode("VOTES"); function KEYCODE() public pure override returns (Keycode) { + return _KEYCODE; - return toKeycode("VOTES"); }
Affected source code:
PRICE.KEYCODE
Recommended changes:
+ KeyCode private immutable _KEYCODE = toKeycode("PRICE"); function KEYCODE() public pure override returns (Keycode) { + return _KEYCODE; - return toKeycode("PRICE"); }
Affected source code:
TRSRY.KEYCODE
Recommended changes:
+ KeyCode private immutable _KEYCODE = toKeycode("TRSRY"); function KEYCODE() public pure override returns (Keycode) { + return _KEYCODE; - return toKeycode("TRSRY"); }
Affected source code:
RANGE.KEYCODE
Recommended changes:
+ KeyCode private immutable _KEYCODE = toKeycode("RANGE"); function KEYCODE() public pure override returns (Keycode) { + return _KEYCODE; - return toKeycode("RANGE"); }
Affected source code:
requestPermissions
OlympusPriceConfig.requestPermissions
Recommended changes:
function requestPermissions() external view override returns (Permissions[] memory permissions) { permissions = new Permissions[](3); + Keycode priceCode = PRICE.KEYCODE(); + permissions[0] = Permissions(priceCode, PRICE.initialize.selector); + permissions[1] = Permissions(priceCode, PRICE.changeMovingAverageDuration.selector); + permissions[2] = Permissions(priceCode, PRICE.changeObservationFrequency.selector); - permissions[0] = Permissions(PRICE.KEYCODE(), PRICE.initialize.selector); - permissions[1] = Permissions(PRICE.KEYCODE(), PRICE.changeMovingAverageDuration.selector); - permissions[2] = Permissions(PRICE.KEYCODE(), PRICE.changeObservationFrequency.selector); }
Affected source code:
VoterRegistration.requestPermissions
Recommended changes:
function requestPermissions() external view override returns (Permissions[] memory permissions) { permissions = new Permissions[](2); + Keycode votesCode = VOTES.KEYCODE(); + permissions[0] = Permissions(votesCode, VOTES.mintTo.selector); + permissions[1] = Permissions(votesCode, VOTES.burnFrom.selector); - permissions[0] = Permissions(VOTES.KEYCODE(), VOTES.mintTo.selector); - permissions[1] = Permissions(VOTES.KEYCODE(), VOTES.burnFrom.selector); }
Affected source code:
configureDependencies
using immutable
OlympusPriceConfig.configureDependencies
Recommended changes:
+ KeyCode private immutable PRICE_CODE = toKeycode("PRICE"); function configureDependencies() external override returns (Keycode[] memory dependencies) { dependencies = new Keycode[](1); - dependencies[0] = toKeycode("PRICE"); + dependencies[0] = PRICE_CODE; PRICE = OlympusPrice(getModuleAddress(dependencies[0])); }
Affected source code:
TreasuryCustodian.configureDependencies
Recommended changes:
+ KeyCode private immutable PRICE_CODE = toKeycode("TRSRY"); function configureDependencies() external override returns (Keycode[] memory dependencies) { dependencies = new Keycode[](1); + dependencies[0] = PRICE_CODE; - dependencies[0] = toKeycode("TRSRY"); TRSRY = OlympusTreasury(getModuleAddress(dependencies[0])); }
Affected source code:
BondCallback.configureDependencies
+ KeyCode private immutable dep0 = toKeycode("TRSRY"); + KeyCode private immutable dep1 = toKeycode("MINTR"); function configureDependencies() external override returns (Keycode[] memory dependencies) { dependencies = new Keycode[](2); - dependencies[0] = toKeycode("TRSRY"); - dependencies[1] = toKeycode("MINTR"); + dependencies[0] = dep0; + dependencies[1] = dep1; - TRSRY = OlympusTreasury(getModuleAddress(dependencies[0])); - MINTR = OlympusMinter(getModuleAddress(dependencies[1])); + TRSRY = OlympusTreasury(getModuleAddress(dep0)); + MINTR = OlympusMinter(getModuleAddress(dep1)); // Approve MINTR for burning OHM (called here so that it is re-approved on updates) ohm.safeApprove(address(MINTR), type(uint256).max); }
Affected source code:
Governance.configureDependencies
+ KeyCode private immutable dep0 = toKeycode("INSTR"); + KeyCode private immutable dep1 = toKeycode("VOTES"); function configureDependencies() external override returns (Keycode[] memory dependencies) { dependencies = new Keycode[](2); - dependencies[0] = toKeycode("INSTR"); - dependencies[1] = toKeycode("VOTES"); + dependencies[0] = dep0; + dependencies[1] = dep1; - INSTR = OlympusInstructions(getModuleAddress(dependencies[0])); - VOTES = OlympusVotes(getModuleAddress(dependencies[1])); + INSTR = OlympusInstructions(getModuleAddress(dep0)); + VOTES = OlympusVotes(getModuleAddress(dep1)); }
Affected source code:
Operator.configureDependencies
+ KeyCode private immutable dep0 = toKeycode("PRICE"); + KeyCode private immutable dep1 = toKeycode("RANGE"); + KeyCode private immutable dep2 = toKeycode("TRSRY"); + KeyCode private immutable dep3 = toKeycode("MINTR"); function configureDependencies() external override returns (Keycode[] memory dependencies) { dependencies = new Keycode[](4); - dependencies[0] = toKeycode("PRICE"); - dependencies[1] = toKeycode("RANGE"); - dependencies[2] = toKeycode("TRSRY"); - dependencies[3] = toKeycode("MINTR"); + dependencies[0] = dep0; + dependencies[1] = dep1; + dependencies[2] = dep2; + dependencies[3] = dep3; - PRICE = OlympusPrice(getModuleAddress(dependencies[0])); - RANGE = OlympusRange(getModuleAddress(dependencies[1])); - TRSRY = OlympusTreasury(getModuleAddress(dependencies[2])); - MINTR = OlympusMinter(getModuleAddress(dependencies[3])); + PRICE = OlympusPrice(getModuleAddress(dep0)); + RANGE = OlympusRange(getModuleAddress(dep1)); + TRSRY = OlympusTreasury(getModuleAddress(dep2)); + MINTR = OlympusMinter(getModuleAddress(dep3)); /// Approve MINTR for burning OHM (called here so that it is re-approved on updates) ohm.safeApprove(address(MINTR), type(uint256).max); }
Affected source code:
It is possible to avoid storage accesses by taking advantage of memory variables that have the same value, as shown below.
Recommended changes:
constructor( Kernel kernel_, AggregatorV2V3Interface ohmEthPriceFeed_, AggregatorV2V3Interface reserveEthPriceFeed_, uint48 observationFrequency_, uint48 movingAverageDuration_ ) Module(kernel_) { /// @dev Moving Average Duration should be divisible by Observation Frequency to get a whole number of observations if (movingAverageDuration_ == 0 || movingAverageDuration_ % observationFrequency_ != 0) revert Price_InvalidParams(); // Set price feeds, decimals, and scale factor _ohmEthPriceFeed = ohmEthPriceFeed_; - uint8 ohmEthDecimals = _ohmEthPriceFeed.decimals(); + uint8 ohmEthDecimals = ohmEthPriceFeed_.decimals(); _reserveEthPriceFeed = reserveEthPriceFeed_; - uint8 reserveEthDecimals = _reserveEthPriceFeed.decimals(); + uint8 reserveEthDecimals = reserveEthPriceFeed_.decimals();
Affected source code:
inline
instead of a methodThe following methods can be moved to inline calls without greatly affecting readability, this will increase the performance of the contract.
Recommended changes:
function beat() external nonReentrant { ... // Issue reward to sender - _issueReward(msg.sender); + rewardToken.safeTransfer(msg.sender, reward); + emit RewardIssued(msg.sender, reward); emit Beat(block.timestamp); } - function _issueReward(address to_) internal { - rewardToken.safeTransfer(to_, reward); - emit RewardIssued(to_, reward); - }
Affected source code:
immutable
It's possible to avoid storage access a save gas using immutable
keyword for the following variables:
It's also better to remove the initial values, because they will be set during the constructor.
Affected source code:
The number of public methods increase the bytecode of the contract, in addition to the possible attack vectors, reducing the number to the minimum necessary for the contract to work normally is a good practice for both security and gas savings.
It can be more efficient to change constants that shouldn't be made public
to private or internal
to stay away from pointless getter functions.
Affected source code:
Governance.submitProposal
It is possible to reduce the condition of the submitProposal
method in the following way, since it is not necessary to multiply in both places.
function submitProposal( Instruction[] calldata instructions_, bytes32 title_, string memory proposalURI_ ) external { - if (VOTES.balanceOf(msg.sender) * 10000 < VOTES.totalSupply() * SUBMISSION_REQUIREMENT) + if (VOTES.balanceOf(msg.sender) * SUBMISSION_REQUIREMENT < VOTES.totalSupply()) revert NotEnoughVotesToPropose();
Affected source code:
Governance.activateProposal
function activateProposal(uint256 proposalId_) external { ... if ( - (totalEndorsementsForProposal[proposalId_] * 100) < - VOTES.totalSupply() * ENDORSEMENT_THRESHOLD + (totalEndorsementsForProposal[proposalId_] * 50) < + VOTES.totalSupply() ) { revert NotEnoughEndorsementsToActivateProposal(); } ... }
Affected source code:
Governance.executeProposal
- if (netVotes * 100 < VOTES.totalSupply() * EXECUTION_THRESHOLD) { + if (netVotes * 3 < VOTES.totalSupply()) { revert NotEnoughVotesToExecute(); }
Affected source code:
Operator._activate
It's possible to avoid the duplicate operation of 10**(oracleDecimals * 2)
like following:
- uint256 invCushionPrice = 10**(oracleDecimals * 2) / range.cushion.low.price; - uint256 invWallPrice = 10**(oracleDecimals * 2) / range.wall.low.price; + uint256 invCushionPrice = 10**(oracleDecimals * 2); + uint256 invWallPrice = invCushionPrice / range.wall.low.price; + invCushionPrice /= range.cushion.low.price;
Affected source code:
calldata
instead of memory
The method propose
is external
, and the arguments are defined as memory
instead of as calldata
.
By marking the function as external
it is possible to use calldata
in the arguments shown below and save significant gas.
Recommended change:
function submitProposal( Instruction[] calldata instructions_, bytes32 title_, - string memory proposalURI_ + string calldata proposalURI_ ) external { ... }
Affected source code:
It is recommended to always perform input or parameter checks first, from cheapest to most expensive. Avoiding making external calls or unnecessary costly tasks for the cases to be verified.
Governance.endorseProposal
Recommended change:
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); }
Affected source code:
Governance.endorseProposal
Recommended change:
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); }
Affected source code:
Governance
storageIt is possible to save all duplicate keys between yesVotesForProposal
and noVotesForProposal
if they both share the same key and use a structure as a value.
Recommended change:
+ struct ActivatedProposal { + uint256 yes; + uint256 no; + } + /// @notice Return the total of votes for a proposal id used in calculating net votes. + mapping(uint256 => YesNo) public votesForProposal; - /// @notice Return the total yes votes for a proposal id used in calculating net votes. - mapping(uint256 => uint256) public yesVotesForProposal; - /// @notice Return the total no votes for a proposal id used in calculating net votes. - mapping(uint256 => uint256) public noVotesForProposal;
Affected source code:
delete
optimizationUse delete
instead of set to default value (false
or 0
).
5 gas could be saved per entry in the following affected lines:
Affected source code:
Operator.bondPurchase
It's possible to optimize the following method using an else
instruction:
function bondPurchase(uint256 id_, uint256 amountOut_) external onlyWhileActive onlyRole("operator_reporter") { if (id_ == RANGE.market(true)) { _updateCapacity(true, amountOut_); _checkCushion(true); } + else if (id_ == RANGE.market(false)) { - if (id_ == RANGE.market(false)) { _updateCapacity(false, amountOut_); _checkCushion(false); } }
Affected source code:
Operator
refactoring some methodsIt is possible to remove the bool high_
argument and the required conditionals in the following methods. It's best to create two methods, one for high and one for low to reduce push arguments and conditional checking, because all the logic is different.
Affected source code:
_activate
in Operator.sol:363_regenerate
in Operator.sol:699Operator.setRegenParams
and Operator._regenerate
It is possible to avoid repeated accesses to storage as well as possible human errors such as those posted in the issues about the lastRegen
field if instead of manually resetting each field in the struct, the entire struct is reset. This will save a considerable amount of gas and reduce human error.
function setRegenParams( uint32 wait_, uint32 threshold_, uint32 observe_ ) external onlyRole("operator_policy") { /// Confirm regen parameters are within allowed values if (wait_ < 1 hours || threshold_ > observe_ || observe_ == 0) revert Operator_InvalidParams(); /// Set regen params _config.regenWait = wait_; _config.regenThreshold = threshold_; _config.regenObserve = observe_; - /// Re-initialize regen structs with new values (except for last regen) + /// Re-initialize regen structs with new values + Regen memory empty = Regen({ + count: uint32(0), + lastRegen: uint48(block.timestamp), + nextObservation: uint32(0), + observations: new bool[](observe_) + }); + _status.high = empty; - _status.high.count = 0; - _status.high.nextObservation = 0; - _status.high.observations = new bool[](observe_); + _status.low = empty; - _status.low.count = 0; - _status.low.nextObservation = 0; - _status.low.observations = new bool[](observe_); emit RegenParamsChanged(wait_, threshold_, observe_); } ... function _regenerate(bool high_) internal { /// Deactivate cushion if active on the side being regenerated _deactivate(high_); + Regen memory empty = Regen({ + count: uint32(0), + lastRegen: uint48(block.timestamp), + nextObservation: uint32(0), + observations: new bool[](_config.regenObserve) + }); if (high_) { /// Reset the regeneration data for the side + _status.high = empty; - _status.high.count = uint32(0); - _status.high.observations = new bool[](_config.regenObserve); - _status.high.nextObservation = uint32(0); - _status.high.lastRegen = uint48(block.timestamp); /// Calculate capacity uint256 capacity = fullCapacity(true); /// Regenerate the side with the capacity RANGE.regenerate(true, capacity); } else { /// Reset the regeneration data for the side + _status.low = empty; - _status.low.count = uint32(0); - _status.low.observations = new bool[](_config.regenObserve); - _status.low.nextObservation = uint32(0); - _status.low.lastRegen = uint48(block.timestamp); /// Calculate capacity uint256 capacity = fullCapacity(false); /// Regenerate the side with the capacity RANGE.regenerate(false, capacity); } }
Affected source code: