Closed code423n4 closed 1 year ago
The observations arrays use a circular buffer to track the last regenObserve
price observations. Initializing the observation arrays to all false
requires the system to making regenThreshold
positive observations before regenerating a side. If initialized to some true values, it could happen more quickly.
It is expected that regenerations will occur during "stable" price period (price is between the cushions/walls). They may be superfluous, but it ensures that the sides have the proper capacity in the event of large market movements.
Based on this, I don't see an issue with the design. Can you elaborate on how this negatively impacts the protocol?
Well, if you look for the design pitfalls, the main one, as it's mentioned in the description, is that number of the moves is quite irrelevant metric for the market movement. You know, for example, that market decline oftentimes happens in a quick and brutal manner (i.e. number of moves is small, size of moves is big). In the same time market tend to rise slowly. To count the number of moves of a given direction looks to be a pointless activity in this regard: marker can do a quick big drop or climb for a long time, but gaining not so much as a result.
I.e. the relevant metric is the overall size of the total move, not the number of the moves in the direction.
Here, on one hand the count is measured, on the other hand it's measured wrongly as array initialization does mess up the frequency. Overall impact for the project is that walls rebuilding will be performed without real alignment to the market movements as moves are counted, instead of being accumulated, and the rebuilding itself be done too frequently vs the stated logic without any need to do so, just per glitch in the implementation.
I disagree that the array initialization messes up the count. Counts are kept separately for the high and low sides. We initialize the count to 0 on initialization and the observation arrays are initialized to all false. Therefore, the system must observe a minimum of regenThreshold
positive observations on a given side to be regenerated on that side. If the array was initialized with some true
values and the count was non-zero, this number would be less and the configuration of regenThreshold
would be inaccurate.
As for whether the design could be improved by using a different type of metric, that may be the case, but it would rely on additional external data that is not available via a suitable oracle.
As for the up-down-up-down scenario not counting one direction of movement, they will be reflected if the previous value in that slot of the circular buffer is the opposite value. E.g. if there is a true
value in slot 2 and then the buffer circles around again, if the observation is negative, it will switch the value to false
and reduce the count.
this comes down to a design question at this point and cannot be awarded inside of the M criteria as the protocol is function as expected per the sponsor. Downgrading to QA. Closing as dupe of #500 the wardens QA report.
Lines of code
https://github.com/code-423n4/2022-08-olympus/blob/2a0b515012b4a40076f6eac487f7816aafb8724a/src/policies/Operator.sol#L663-L694
Vulnerability details
Counter accumulation logic favors counter increase over decrease as both are conditioned on the earliest, i.e. the most distant from the current, entry situation instead of the previous one, while initially these most distant entries are all false, being not initialized. This way the counters will be allowed to be increased, but not allowed to be decreased, capturing one side of the market movements only.
This way their rationale of controlling (not in exact way as moves can be drastically different, while here it's a direction only logic) market situation and rebuilding when it's needed only isn't met as counter logic is currently biased to the counter growth side.
The impact is system being regenerated when it's not needed over and over again as each _regenerate() will reinit the observations array, leading to the repeat of the counter increase favoring and the accumulation that doesn't correspond to the cumulative market movement.
Proof of Concept
In _addObservation()
_status.x.count--
happens only ifregen.observations[regen.nextObservation] == true
both forx
=low
andhigh
cases:https://github.com/code-423n4/2022-08-olympus/blob/2a0b515012b4a40076f6eac487f7816aafb8724a/src/policies/Operator.sol#L663-L694
But
regen.nextObservation
is next observation, and all such observations will initially come from the uninitialized_status.x.observations
array, which elements will be initially false. This way all down-moves for low case and up-moves for high case will be ignored counter-wise asif (regen.observations[regen.nextObservation])
condition will not be satisfied until the_config.regenObserve
of ticks be passed.This implies that, for example, in a 'up-down-up-down-...' sideways market situation all ups will be counted for the low case and all downs will be counted for the high case, i.e. both counters will be equal to accumulated one side only and most likely exceed the regeneration threshold, while as the market didn't really moved the regeneration will be excessive.
_addObservation() is a part of operate() logic which will be called via heartbeats:
https://github.com/code-423n4/2022-08-olympus/blob/2a0b515012b4a40076f6eac487f7816aafb8724a/src/policies/Operator.sol#L194-L201
There regeneration logic is triggered when enough time passed and counter is bigger than
config_.regenThreshold
:https://github.com/code-423n4/2022-08-olympus/blob/2a0b515012b4a40076f6eac487f7816aafb8724a/src/policies/Operator.sol#L208-L220
Recommended Mitigation Steps
The mechanics isn't exact, but the design can be updated to look at the most recent entry, so sideways market won't generate the regeneration, i.e. for the low case as an example: