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.