If the reserve token is fee-to-transfer token and the user is buying ohm, the Operator::swap will incorrectly assume the amountIn_ value is transferred, which fails to consider the fees. If the fee is rounded up, the attacker can purchase ohm without giving any assets to the treasury. It may not be profitable for the attacker, but it may cause devaluing of the ohm. However, the loss will be limited to the capacity.
// Operator::swap
// if(tokenIn_ == reserve) : buying ohm
329 /// Transfer reserves to treasury
330 reserve.safeTransferFrom(msg.sender, address(TRSRY), amountIn_);
L01 BondCallback: incorrect accounting if quoteToken is rebase token
If the quoteToken is rebase token, the priorBalances may change due to rebasing or airdrop. It may result to an incorrect accounting. However, whether it is exploitable depends on the Bond market's logic.
With the current logic, it just checks whether the balance is increased more than the inputAmount_, so it is harder to exploit, compare to the alternative logic of using the difference in balances as the input amount. However, it also introduces the possibility of paying the users less than they deserve.
// Callback::callback
113 // Check that quoteTokens were transferred prior to the call
114 if (quoteToken.balanceOf(address(this)) < priorBalances[quoteToken] + inputAmount_)
115 revert Callback_TokensNotReceived();
The movingAverageDuration and observationFrequency are uint48. So movingAverageDuration / observationFrequency may overflow when casted to uint32. In the below snippet, line 281, the array will be set based on the uint256 value, but the numObservations is casted down to uint32. It may result in different numObservations and the length of observations array.
However, given the large numbers, the attempt to set such a large number as the parameters will likely to fail with "out of gas" error, since the length of the array observations is ridiculously large in this case. Yet, it is probably safe to set some upper limit for the numObservations or use safeCast.
// modules/PRICE::constructor
97 numObservations = uint32(movingAverageDuration_ / observationFrequency_);
// modules/PRICE::changeObservationFrequency
280 // Store blank observations array of new size
281 observations = new uint256[](newObservations);
282
283 // Set initialized to false and update state variables
284 initialized = false;
285 lastObservationTime = 0;
286 _movingAverage = 0;
287 nextObsIndex = 0;
288 observationFrequency = observationFrequency_;
289 numObservations = uint32(newObservations);
In the Operator::_activate decimal values were casted to int8 and uint8 back and forth. Since there is no check, those values can potentially overflow/underflow. However, if it happens the exponent in the line 376 will like to revert due to too large numbers. Besides, if the price decimals are that big, this may not be the biggest problem to face.
If the operator is not set, the callback function will revert so, it is crucial to set the operator before any operation. However, it was not set in the constructor, but should be set separately by calling setOperator.
L05 Operator: missing check for configParmas[0] (cushionFactor) in the constructor
The Operator::constructor does not check the condition of the cushionFactor. Below is the condition for the cushionFactor checked in the Operator::setCushionFactor.
// Operator::setCushionFactor
516 function setCushionFactor(uint32 cushionFactor_) external onlyRole("operator_policy") {
517 /// Confirm factor is within allowed values
518 if (cushionFactor_ > 10000 || cushionFactor_ < 100) revert Operator_InvalidParams();
519
520 /// Set factor
521 _config.cushionFactor = cushionFactor_;
522
523 emit CushionFactorChanged(cushionFactor_);
524 }
L06 Kernel: misplaced zero address check for changeKernel
Currently, the check for the Kernel to be a contract (also not to be the zero address), is in the current Kernel implementation. However, no modules and policies have the logic to ensure this as they inherit from KernelAdapter, which will just set the new kernel without a question. This will work well as long as the new Kernel has the similar logic to check the next Kernel's integrity. However, if the logic is forgotten, there is no other safe guard to ensure that the next kernel is not a zero address and is a contract.
Since Kernel is absolutely needed for this system's functionality, there is no possible case that the Kernel should be the zero address. Therefore, it is probably safe to add the checking logic to the KernelAdapter, so every module and policy will check for the next Kernel. It costs more gas since the check is done multiple times, but still arguably it is worth the cost, since Kernel is core part of the system and it will not updated very often.
The Governance's logic will break if the INSTR module is upgraded to a new contract without having the same instructions data, since the proposalId's the Governance is using are bound to the INSTR module.
L09 BondCallback, Operator: upon module's upgrade, the token approval should be revoked
The BondCallback and Operator approves ohm to the MINTR module in the configureDependencies. However, there is no logic to revoke those approvals (e.i. approve to zero). In the case of the MINTR has some bugs, it may be desirable to be able to revoke the approvals.
If the _issueReward reverts, for example, because the token balance is too low, the beat will as well revert, due to the safeTransfer. One might consider not to revert even in the case the _issueReward reverts.
L12 PRICE: stale price
There is no indicator whether the price information is up-to-date. If the price information is not properly updated, the other contracts will keep using the data resulting in incorrect prices for swap.
Olympus DAO QA Report
Summary
numObservations
constructor
changeKernel
executor
andadmin
FullMath
_issueReward
fails the heart beat will revertLow
L00 Operator: incorrect accounting for fee-on-transfer reserve token
If the reserve token is fee-to-transfer token and the user is buying ohm, the
Operator::swap
will incorrectly assume theamountIn_
value is transferred, which fails to consider the fees. If the fee is rounded up, the attacker can purchase ohm without giving any assets to the treasury. It may not be profitable for the attacker, but it may cause devaluing of the ohm. However, the loss will be limited to the capacity.L01 BondCallback: incorrect accounting if quoteToken is rebase token
If the quoteToken is rebase token, the priorBalances may change due to rebasing or airdrop. It may result to an incorrect accounting. However, whether it is exploitable depends on the Bond market's logic. With the current logic, it just checks whether the balance is increased more than the
inputAmount_
, so it is harder to exploit, compare to the alternative logic of using the difference in balances as the input amount. However, it also introduces the possibility of paying the users less than they deserve.L02 PRICE: unsafe cast for
numObservations
The
movingAverageDuration
andobservationFrequency
are uint48. SomovingAverageDuration / observationFrequency
may overflow when casted to uint32. In the below snippet, line 281, the array will be set based on the uint256 value, but thenumObservations
is casted down to uint32. It may result in differentnumObservations
and the length ofobservations
array. However, given the large numbers, the attempt to set such a large number as the parameters will likely to fail with "out of gas" error, since the length of the arrayobservations
is ridiculously large in this case. Yet, it is probably safe to set some upper limit for thenumObservations
or use safeCast.L03 Operator: unsafe cast for decimals
In the
Operator::_activate
decimal values were casted toint8
anduint8
back and forth. Since there is no check, those values can potentially overflow/underflow. However, if it happens the exponent in the line 376 will like to revert due to too large numbers. Besides, if the price decimals are that big, this may not be the biggest problem to face.L04 BondCallback: operator is not set
constructor
If the
operator
is not set, thecallback
function will revert so, it is crucial to set theoperator
before any operation. However, it was not set in theconstructor
, but should be set separately by callingsetOperator
.L05 Operator: missing check for configParmas[0] (cushionFactor) in the constructor
The
Operator::constructor
does not check the condition of thecushionFactor
. Below is the condition for thecushionFactor
checked in theOperator::setCushionFactor
.L06 Kernel: misplaced zero address check for
changeKernel
Currently, the check for the
Kernel
to be a contract (also not to be the zero address), is in the currentKernel
implementation. However, no modules and policies have the logic to ensure this as they inherit fromKernelAdapter
, which will just set the new kernel without a question. This will work well as long as the new Kernel has the similar logic to check the next Kernel's integrity. However, if the logic is forgotten, there is no other safe guard to ensure that the next kernel is not a zero address and is a contract. SinceKernel
is absolutely needed for this system's functionality, there is no possible case that the Kernel should be the zero address. Therefore, it is probably safe to add the checking logic to theKernelAdapter
, so every module and policy will check for the next Kernel. It costs more gas since the check is done multiple times, but still arguably it is worth the cost, since Kernel is core part of the system and it will not updated very often.L07 Kernel: missing zero address check for
executor
andadmin
The
executor
andadmin
are not checked for the zero address when set by theKernel::executeAction
.L08 INSTR, Governance: upon module's upgrade, all instruction data should be carried over to the new modules
The
Governance
's logic will break if theINSTR
module is upgraded to a new contract without having the same instructions data, since theproposalId
's theGovernance
is using are bound to theINSTR
module.L09 BondCallback, Operator: upon module's upgrade, the token approval should be revoked
The
BondCallback
andOperator
approves ohm to theMINTR
module in theconfigureDependencies
. However, there is no logic to revoke those approvals (e.i. approve to zero). In the case of theMINTR
has some bugs, it may be desirable to be able to revoke the approvals.L10 RANGE, PRICE: unused import of
FullMath
The modules
RANGE
andPRICE
importsFullMath
, but it is not used.L11 Heart: if the issueReward fails the heart beat will revert
If the
_issueReward
reverts, for example, because the token balance is too low, thebeat
will as well revert, due to thesafeTransfer
. One might consider not to revert even in the case the_issueReward
reverts.L12 PRICE: stale price
There is no indicator whether the price information is up-to-date. If the price information is not properly updated, the other contracts will keep using the data resulting in incorrect prices for swap.