SmartVaultManagerV5
implementation contract allows unauthorized initializationSmartVaultManagerV5
contract utilizes Initializable
contract from OpenZeppelin(OZ) library, but doesn't disable the initializer function, to prevent unauthorized initialization of the implementation contract (deployed behind a proxy).
SmartVaultManagerV5
uses the "transparent upgradeable proxy" pattern, and each version of the contract (implementation) is intended to be deployed behind a proxy.
The initialize
function, expected to be called (by the deploy script in this case) after each contract upgrade. But during deploy it will be called only for the proxy contract( and not implementation).
The documentation for Initializable
OZ contract clearly states that initialization functionality should be manually disabled for implementation contracts. But in SmartVaultManagerV5
it's not implemented.
Even though there is no way to exploit this flaw in the current version of SmartVaultManager (because the initializer function is empty), it poses serious risks for any further versions of this contract, which might add logic to the initializer function, but omit safeguarding it from malicious actors.
Also, there is no reason to ignore strict recommendations from the OZ documentation.
The following test case demonstrates how a unauthorized user can initialize the implementation contract for SmartVaultManagerV5
Run with npx hardhat test
Use _disableInitializers
function in the constructor to prevent initialization of the implementation contract.
LiquidationPool
if there was no fee distributionDuring fee distribution, LiqudationPoolManager
approves LiqudationPool
to distribute EUROs
tokens among all stakers. But LiquidationPool
will not spend this allowance, if there is no TST
token staked.
LiquidationPoolManager
holds EUROs
tokens which is distributed between stakers and the Protocol.
LiquidationPoolManager.distributeFees
function called by LiquidationPool
during fee distribution:
contracts/LiquidationPoolManager.sol
In this function, LiquidationPoolManager
approves LiquidationPool
to spend EUROs
, if there is some EUROs
collected.
LiquidationPool.distributeFees
function uses this allowance to transfer EUROs
to stakers:
As we can see, EUROs
will be transferred only if there is some TST
tokens staked. That means, if tstTotal == 0
, the allowance will not be spent, and LiquidationPool
will be able to spend EUROs
held by LiquidationPoolManager
even after fee distribution function completed.
This issue by itself doesn't harm the Protocol, but it might be exploited though vulnerability chaining in another function (I've reported another vulnerability in distributeAssets
function which is an ideal candidate for this scenario).
So, there is no reasons to leave this gap in the logic.
Consider the following options to resolve the issue:
If getTstTotal
function can be made public
, it can be utilized in LiquidationPoolManager.distributeFees
to prevent approval and call to LiquidationPool.distributeFees
, if tstTotal == 0
LiquidationPool.distributeFees
can return a bool
, to signal whether fee distribution actually happened, so LiquidationPoolManager
can reset approval if needed
Reset approval each time (NOTE: this is less desirable method because some tokens may revert on zero approvals):
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.