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: 49/147
Findings: 4
Award: $189.06
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: rbserver
Also found by: GalloDaSballo, Guardian, Lambda, __141345__, cccz, csanuragjain, m9800, zzzitron
91.1353 DAI - $91.14
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#L295
Firstly, since the VOTES
module token is non-transferrable, it is not necessary to "lock" VOTES
in the Governance contract -- although this might change if the VOTES
module were to be upgraded to a transferrable token.
If the locking logic is kept, there should be added checks in the vote
function to prevent users from accidentally voting with 0 votes or with potential votes locked up in a past proposal. If a user were to accidentally vote without reclaiming their past locked votes, they wouldn’t be able to exercise their full voting power – as once a nonzero value is recorded in the userVotesForProposal
, the user can not vote again for that proposal, even if they reclaim more votes.
Additionally, there is no check to prevent user's from calling the vote
function while they have 0 VOTES
in their wallet.
User A has 5 VOTES
tokens.
User A votes on proposal 1.
User A's 5 VOTES
tokens are locked in the governance contract.
User A receives 5 more VOTES
tokens.
Proposal 1 reaches the execution threshold and is executed.
Proposal 2 is submitted and activated for voting.
User A votes on proposal 2.
User A's votes for proposal 2 are recorded as just 5. But User A had 10 total tokens, with 5 of them unclaimed from the governance contract.
User A realizes they didn't exercise their full voting power, but they cannot vote again -- even after reclaiming their locked votes.
There are a few ways this might be resolved:
VOTES
is not currently a transferrable token.vote
OR transfer their locked amount to count towards the current proposal automatically in the votes
function. vote
when they have no VOTES
tokens in their wallet.VOTES
tokens that they voted with to the governance contract each time.#0 - bahurum
2022-09-02T13:00:48Z
Second paragraph of impact section is a duplicate of #382
#1 - fullyallocated
2022-09-04T02:45:52Z
There is a whole host of issues related to this topic so I will group them all under here.
Warden brings up a good point that the VOTES are already transfer locked so we don't need to lock them in the contract here. This brings up additional issues like #376 so we will likely remove this and consider a new model for the remediation.
#2 - 0xean
2022-09-19T15:27:50Z
closing as dupe of #275
11.0311 DAI - $11.03
https://github.com/code-423n4/2022-08-olympus/blob/b5e139d732eb4c07102f149fb9426d356af617aa/src/modules/PRICE.sol#L161 https://github.com/code-423n4/2022-08-olympus/blob/b5e139d732eb4c07102f149fb9426d356af617aa/src/modules/PRICE.sol#L170
Extra validation checks should be added on the result from Chainlink to ensure non-stale data. The price from the data feed influences the moving average and consequently range data so it is imperative the data is up to date and correct. If a price less than 0 was returned for ohmEthPriceInt
or reserveEthPriceInt
and then converted to a uint
, the resulting price would be massive.
Remix
https://docs.chain.link/docs/price-feeds-api-reference/#latestrounddata-1 Add the following checks:
require(answer > 0) // answer is int256 require(updatedAt != 0) require(answeredInRound >= roundId)
#0 - Oighty
2022-09-06T18:53:36Z
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.3129 DAI - $54.31
Heart policy can allow for Operator update, just as it allows for update of the reward token and reward amount. Otherwise, if the Operator is deactivated then the Heart is still calling the stale Operator. (Or should Operator be a module, that way upgrades happen smoothly because they are already supported by logic in the kernel)
BondCallback.sol::160 priorBalances[token] = token.balanceOf(address(this)); could be set to priorBalances[token] = 0; as the whole balance has been transferred in line 159.
Oftentimes bonds are negative ROI, but users still use them as opportunities to bypass slippage and do an “OTC” deal of sorts with the treasury. An OHM-OHM bond cannot be used as an “OTC” deal since you are bonding OHM for OHM, but there may be other reasons why a user might take a negative ROI OHM bond. However, in BondCallback.sol::120 the callback function will revert if the outputAmount_ is less than the inputAmount_. Therefore it is not possible to take a negative ROI OHM-OHM bond, which may be unexpected behavior.
PRICE.sol::59 decimals should follow constant variable naming conventions
PriceConfig.sol::54-54 The comment implies that a smaller duration should not clear the data in the current window and require re-initialization. However, changeMovingAverageDuration in PRICE.sol clears the current data no matter if the MA duration is smaller or larger.
PRICE.sol::240, 266 movingAverageDuration_ and observationFrequency_ should be checked that they differ from the previous values. No need to perform reset if the values are the same.
PRICE.sol::126 typo numbe
🌟 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
32.5836 DAI - $32.58
Redundant boolean checks: Governance.sol::223,306
BondCallback.sol::160 The remaining balance will always be 0, therefore the priorBalances can simply be set to 0
Governance.sol::193 The previousEndorsement
variable is only used once, the following line can simply read from userEndorsementsForProposal without declaring a stack variable.
INSTR.sol::48 this check can be moved up to the second line of the function, to avoid spending extra gas in the case of a revert.
TRSRY.sol::122 in the setDebt
function, instead of storing oldDebt
and using an if to conditionally handle the debt change, Simply subtract the oldDebt
from the totalDebt
and add the new debt amount to the totalDebt
.
PRICE.sol::177 no need to declare a currentPrice
variable only to return it on the next line, instead just return the computed value directly.
Operator.sol::299 Rather than transferring ohm from the msg.sender
to the operator
contract and then burning it from the operator
contract, the MINTR
can burn the ohm directly from the msg.sender
.
Variables should not be initialized to defaults (uint256 default is 0): Kernel.sol::397 => for (uint256 i = 0; i < reqLength; ) { utils/KernelUtils.sol::43 => for (uint256 i = 0; i < 5; ) { utils/KernelUtils.sol::58 => for (uint256 i = 0; i < 32; ) {
Length of array should be computed outside of for-loop: Kernel.sol::300 => getPolicyIndex[policy_] = activePolicies.length - 1; Kernel.sol::304 => uint256 depLength = dependencies.length; Kernel.sol::310 => getDependentIndex[keycode][policy_] = moduleDependents[keycode].length - 1; Kernel.sol::334 => Policy lastPolicy = activePolicies[activePolicies.length - 1]; Kernel.sol::352 => uint256 keycodeLen = allKeycodes.length; Kernel.sol::361 => uint256 policiesLen = activePolicies.length; Kernel.sol::380 => uint256 depLength = dependents.length; Kernel.sol::396 => uint256 reqLength = requests_.length; Kernel.sol::411 => uint256 depcLength = dependencies.length; Kernel.sol::418 => Policy lastPolicy = dependents[dependents.length - 1]; modules/INSTR.sol::43 => uint256 length = instructions_.length; modules/INSTR.sol::48 => if (length == 0) revert INSTR_InstructionsCannotBeEmpty(); modules/INSTR.sol::50 => for (uint256 i; i < length; ) { modules/INSTR.sol::61 => } else if (instruction.action == Actions.ChangeExecutor && i != length - 1) { modules/PRICE.sol::201 => /// @param startObservations_ - Array of observations to initialize the moving average with. Must be of length numObservations. modules/PRICE.sol::212 => uint256 numObs = observations.length; modules/PRICE.sol::215 => if (startObservations_.length != numObs || lastObservationTime_ > uint48(block.timestamp)) policies/BondCallback.sol::155 => uint256 len = tokens_.length; policies/Governance.sol::188 => if (instructions.length == 0) { policies/Governance.sol::278 => for (uint256 step; step < instructions.length; ) { policies/PriceConfig.sol::41 => /// @param startObservations_ Array of observations to initialize the moving average with. Must be of length numObservations. policies/TreasuryCustodian.sol::58 => uint256 len = tokens_.length;
!= is more efficient than > 0 for uint comparisons: policies/Governance.sol::247 => if (userVotesForProposal[activeProposal.proposalId][msg.sender] > 0) { test/modules/PRICE.t.sol::101 => change = int256(uint256(keccak256(abi.encodePacked(nonce, i)))) % int256(1000); test/modules/PRICE.t.sol::128 => change = int256(uint256(keccak256(abi.encodePacked(nonce, i)))) % int256(1000); test/policies/PriceConfig.t.sol::124 => change = int256(uint256(keccak256(abi.encodePacked(nonce, i)))) % int256(1000);
Switching from division/multiplication to right-shift/left-shift can save gas: policies/Operator.sol::372 => int8 scaleAdjustment = int8(ohmDecimals) - int8(reserveDecimals) + (priceDecimals / 2); policies/Operator.sol::419 => uint256 invCushionPrice = 10**(oracleDecimals * 2) / range.cushion.low.price; policies/Operator.sol::420 => uint256 invWallPrice = 10**(oracleDecimals * 2) / range.wall.low.price; policies/Operator.sol::427 => int8 scaleAdjustment = int8(reserveDecimals) - int8(ohmDecimals) + (priceDecimals / 2); policies/Operator.sol::786 => ) * (FACTOR_SCALE + RANGE.spread(true) * 2)) /