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: 50/147
Findings: 2
Award: $168.24
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: rvierdiiev
Also found by: Jeiwan, Lambda, Trust, datapunk, devtooligan, itsmeSTYJ, zzzitron
113.9192 DAI - $113.92
https://github.com/code-423n4/2022-08-olympus/blob/main/src/policies/Heart.sol#L92-L109
While there is a check for too quick of a heart beat, there is no check for a heart beat where an extended amount of time has passed since the last beat. This condition could result from technical difficulties preventing the keeper from calling beat()
, an admin toggling the Heart beat back on without resetting the heart beat, or other unforeseen conditions.
Furthermore, when beat()
is called, the lastBeat
is not updated to the current block timestamp.
Impact:
As a result of this, beat()
may be called multiple times until the lastBeat catches up to the current block timestamp. This has two significant impacts:
TWAP observations will be duplicated. If the amount of time since lastBeat exceeds the movingAverageDuration then all of the observations will be set to the current price. This negates the time-weighted effect of the TWAP. In a volatile market, this could mean the sale of reserve wall at significantly below market, or the purchase of the OHM wall significantly above market.
Rewards tokens can be drained. beat()
can be called repeatedly thereby draining the reward token balance of Heart. The total loss is capped at reward * timeElapsedSinceLastBeat / frequency()
which could be significant, especially in the case of toggling the beat to active after an extended inactive period.
Proof of concept:
function testCallBeatRepeatedly() public { uint256 startBalance = rewardToken.balanceOf(address(this)); console2.log("starting balance attacker", startBalance / 1e18); uint256 startBalanceHeart = rewardToken.balanceOf(address(heart)); console2.log("starting balance heart", startBalanceHeart / 1e18); while (rewardToken.balanceOf(address(heart)) >= 1e18) { heart.beat(); } uint256 endBalance = rewardToken.balanceOf(address(this)); console2.log("ending balance attacker", endBalance / 1e18); uint256 endBalanceHeart = rewardToken.balanceOf(address(heart)); console2.log("ending balance heart", endBalanceHeart / 1e18); }
[PASS] testCallBeatRepeatedly() (gas: 14614379) Logs: starting balance attacker 0 starting balance heart 1000 ending balance attacker 1000 ending balance heart 0 Test result: ok. 1 passed; 0 failed; finished in 26.34ms
Recommendation:
@@ -133,7 +137,11 @@ contract OlympusHeart is IHeart, Policy, ReentrancyGuard { /// @inheritdoc IHeart function toggleBeat() external onlyRole("heart_admin") { - active = !active; + bool priorState = active; + if (!priorState) { + resetBeat(); + } + active = !priorState; }
beat()
. This may include shutting down the walls -- updateCapacity(0), filling in missed observations,updating lastBeat, or other mitigative actions.@@ -93,6 +93,11 @@ contract OlympusHeart is IHeart, Policy, ReentrancyGuard { if (!active) revert Heart_BeatStopped(); if (block.timestamp < lastBeat + frequency()) revert Heart_OutOfCycle(); + uint MAXIMUM_BEAT_LAG = frequency() * 3; // this can be set elsewhere + if (block.timestamp - lastBeat > MAXIMUM_BEAT_LAG) { + // handle missed beats + } +
#0 - Oighty
2022-09-07T21:25:27Z
Agree with calling resetBeat()
on a toggleBeat()
call. Other issue has been commented on in #405 and #79, with a solution proposed in the latter.
#1 - 0xean
2022-09-19T13:31:35Z
closing as duplicate 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.3166 DAI - $54.32
Operator.sol
Severity: Low
Using a struct would greatly increase the readability and reduce the chance of a mistake during deployment.
diff --git a/src/policies/Operator.sol b/src/policies/Operator.sol index 7573526..1b70536 100644 --- a/src/policies/Operator.sol +++ b/src/policies/Operator.sol @@ -88,14 +88,26 @@ contract Operator is IOperator, Policy, ReentrancyGuard { /// Constants uint32 public constant FACTOR_SCALE = 1e4; + struct ConfigParams { + uint32 cushionFactor; + uint32 cushionDuration; + uint32 cushionDebtBuffer; + uint32 cushionDepositInterval; + uint32 reserveFactor; + uint32 regenWait; + uint32 regenThreshold; + uint32 regenObserve; + } + /* ========== CONSTRUCTOR ========== */ constructor( Kernel kernel_, IBondAuctioneer auctioneer_, IBondCallback callback_, ERC20[2] memory tokens_, // [ohm, reserve] - uint32[8] memory configParams // [cushionFactor, cushionDuration, cushionDebtBuffer, cushionDepositInterval, reserveFactor, regenWait, regenThreshold, regenObserve] + ConfigParams memory configParams ) Policy(kernel_) {
Severity: Low
Through the use of constants, certain limits and other values important to the protocol can be better managed and reduce chance of setting something incorrectly.
Here is an example of constants being used along with using a struct for ConfigParams
- if (configParams[1] > uint256(7 days) || configParams[1] < uint256(1 days)) + if (configParams.cushionDuration > MAX_CUSHION_DURATION || configParams.cushionDuration < MIN_CUSHION_DURATION ) revert Operator_InvalidParams(); - if (configParams[2] < uint32(10_000)) revert Operator_InvalidParams(); + if (configParams.cushionDebtBuffer < MIN_DEBT_BUFFER) revert Operator_InvalidParams(); - if (configParams[3] < uint32(1 hours) || configParams[3] > configParams[1]) + if (configParams.cushionDepositInterval < MIN_CUSHION_DEPOSIT_INTERVAL || configParams.cushionDepositInterval > MAX_CUSHION_DEPOSIT_INTERVAL) revert Operator_InvalidParams();
Severity: Low
It is confusing to read something like:
_regenerate(false)
For better readability and to prevent possible mistakes in the future, use constants instead.
bool constant HIGH = true; bool constant LOW = false; _regenerate(LOW)
Severity: Gas optimization
Instead of calling PRICE.decimals()
, use a local constant for decimals. Both the decimals in PRICE.sol and the decimals when calling PRICE.sol as an external contract can be imported from a constants.sol
file thereby keeping the gas savings from the constant and also retaining only a single source of truth for the constant values.