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: 5/147
Findings: 6
Award: $3,033.35
🌟 Selected for report: 2
🚀 Solo Findings: 1
🌟 Selected for report: rbserver
Also found by: GalloDaSballo, Guardian, Lambda, __141345__, cccz, csanuragjain, m9800, zzzitron
When a user has already voted for the active proposal, all subsequent calls to vote
fail. However, it can happen that the user receives new VOTES
tokens after voting. These tokens cannot be used to vote for the current proposal and are effectively frozen until a new proposal is active. Even worse, because VOTES.totalSupply()
is considered in executeProposal()
, it can become much harder (or impossible) to execute a proposal.
Let's consider an extreme example: User A has 1 VOTES
token and votes for the active proposal. The totalSupply()
of VOTES
at that time is 10. Then, user A gets additional 30 VOTES
tokens. However, those cannot be used to vote on the active proposal.
For this proposal, it will be impossible to reach the execution threshold, as the maximum number of votes that it can get is 10, but the totalSupply()
now is 40.
Allow increasing the votes for the active proposal.
#0 - fullyallocated
2022-09-04T02:51:06Z
Duplicate of #275
11.0311 DAI - $11.03
https://github.com/code-423n4/2022-08-olympus/blob/549b96bcf8b97807738572605f6b1e26b33ef411/src/modules/PRICE.sol#L167 https://github.com/code-423n4/2022-08-olympus/blob/549b96bcf8b97807738572605f6b1e26b33ef411/src/modules/PRICE.sol#L173
Chainlink can return negative values for a price feed (for instance temporary for a short time), as asset prices can be negative (e.g., oil in 2020). Currently, this is not handled correctly and will completely break the system: As the value is directly casted to a uint256
, this will lead to huge and therefore completely wrong values.
Note that a reserveEthPrice
of 0 is also not handled explicitly, but will lead to a reversion (which is probably the best thing in such a scenario).
Chainlink temporarily returns a RESERVE/ETH price of -1. After the casting, reserveEthPrice
is type(uint256).max
, meaning that currentPrice
is 0 (whereas the "true value" for a reserveEthPrice
that is equal to +1 (i.e., slightly higher) would be ohmEthPrice * _scaleFactor
).
Revert when the returned price is negative to avoid further calculations with completely wrong values.
#0 - Oighty
2022-09-06T19:57:40Z
Duplicate. See comment on #441.
🌟 Selected for report: Lambda
1905.4132 DAI - $1,905.41
In INSTR.sol
, it is correctly checked that a ChangeExecutor
instruction only occurs at the last position to avoid situations where the other instructions are deemed as invalid.
However, the same problem can occur for MigrateKernel
. For instance, let's say we have a MigrateKernel
followed by a DeactivatePolicy
action. The MigrateKernel
action will change the value of kernel
within the policy. The DeactivatePolicy
action tries to call setActiveStatus
on the policy. However, this has a onlyKernel
modifier and the call will therefore fail when it is done after the value of kernel
was changed.
Perform the same check for MigrateKernel
.
#0 - fullyallocated
2022-09-04T03:31:10Z
Thank you; good catch
To check that an already active policy is not added a second time, isActive()
is called on the policy. However, policy
could be a malicious contract that always returns false
for isActive()
. In such a scenario, it would be possible to activate the policy multiple times for the same Kernel. This would break uniqueness invariants such that _deactivatePolicy()
no longer works. However, it could also be used for a DoS attack: As _reconfigurePolicies
and _migrateKernel
iterate over those lists that now contain duplicates, they could run out of gas if a policy is activated enough times.
Check getPolicyIndex[policy_] != 0
instead of relying on a value of an untrusted contract.
#0 - 0xLienid
2022-09-06T15:05:46Z
@ind-igo a few other submissions also mention problems with over-reliance on policy.isActive (i.e. #368). Might be worth mitigating with the suggested step here or the check on activePolicies[index] like 368 mentions.
#1 - ind-igo
2022-09-08T19:48:41Z
Dupe of #368
#2 - 0xean
2022-09-19T22:21:22Z
I think this is separate from #368 which is about a policy deactivating that isn't already active.
I am a bit skeptical at the impact statement currently, but it does seem like protocol functionality does end up in a bad state with the typical policy lifecycle here. Will award as M unless Sponsor wants to provide some additional reasoning as to a downgrade.
#3 - ind-igo
2022-10-07T16:56:01Z
While the issue is slightly different from #368, the solution is the exact same. The remediation has the new checks to prevent both of these issues.
🌟 Selected for report: rvierdiiev
Also found by: Jeiwan, Lambda, Trust, datapunk, devtooligan, itsmeSTYJ, zzzitron
113.9192 DAI - $113.92
lastBeat
is always increased by the frequency and not set to the curernt timestamp. Therefore, if it was not called for a long time, it can be called multiple times in a row which violates frequency. As the moving average is then always updated, this also leads to wrong values for the moving average (instead of a moving average over N * frequency, it can suddenly be a moving average over just a few seconds).
lastBeat
is currently at timestamp X and we assume the frequency is 100. beat()
is not called for 1000 seconds. At timestamp X + 1001, it can now be called ten times in a row and the moving average is updated every time. If these ten updates happen in the same transaction, the moving average is no longer a moving average, but simply the current price at timestamp X + 1001.
Set lastBeat
to the current timestamp.
#0 - Oighty
2022-09-07T21:03:16Z
See comment on #405.
#1 - 0xean
2022-09-19T13:48:07Z
closing as dupe of #79
🌟 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.4097 DAI - $54.41
Operator.sol
, when reserve
is a token with a fee on transfer, the actual amount that is transferred will be too little (https://github.com/code-423n4/2022-08-olympus/blob/549b96bcf8b97807738572605f6b1e26b33ef411/src/policies/Operator.sol#L330). Fee-on-transfer tokens are explicitly handled in BondBaseTeller.sol
(but with a comment stating that these tokens are not supported, which makes the explicit handling a bit weird IMO. If they were supported, the issue would be medium severity in my opinion.BondBaseCDA
, it is checked that the payoutTokenDecimals
and quoteTokenDecimals
are between 6 and 18. While this should not be a problem in OHM itself (as the reserve token and OHM are between these two values), there are tokens with more or less decimals (Gemini Dollar, YAM v2) and the protocol could not be used with them.TreasuryCustodian.sol
: Revoking approvals for random addresses possible (https://github.com/code-423n4/2022-08-olympus/blob/549b96bcf8b97807738572605f6b1e26b33ef411/src/policies/TreasuryCustodian.sol#L56)PRICE.sol
, it would make sense in my opinion to bound numObservations
(https://github.com/code-423n4/2022-08-olympus/blob/549b96bcf8b97807738572605f6b1e26b33ef411/src/modules/PRICE.sol#L97), as the system can become non-functional when the value is too large (because of out of gas errors when iterating over the observations).Heart
is out of reward tokens, it still tries to transfer them, which will revert (https://github.com/code-423n4/2022-08-olympus/blob/549b96bcf8b97807738572605f6b1e26b33ef411/src/policies/Heart.sol#L112). This means that even people that would call beat()
without a reward (e.g., the OHM team) cannot do so and the reward amount first needs to be set to 0 by the heart admin.