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:
1) 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.
2) 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.
Reset beat after toggling the Heart from inactive to active.
@@ -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;
}
Handle missed beats within 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
+ }
+
Lines of code
https://github.com/code-423n4/2022-08-olympus/blob/main/src/policies/Heart.sol#L92-L109
Vulnerability details
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, thelastBeat
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:1) 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.
2) Rewards tokens can be drained.
beat()
can be called repeatedly thereby draining the reward token balance of Heart. The total loss is capped atreward * timeElapsedSinceLastBeat / frequency()
which could be significant, especially in the case of toggling the beat to active after an extended inactive period.Proof of concept:
Recommendation:
beat()
. This may include shutting down the walls -- updateCapacity(0), filling in missed observations,updating lastBeat, or other mitigative actions.