We audited the Ion-Protocol/ion-protocol repository at commit 5333fe3.
The following files were in scope:
src
├── flash
│ └── handlers
│ ├── base
│ │ └── UniswapFlashloanDirectMintHandler.sol
│ └── WeEthHandler.sol
├── interfaces
│ └── IRedstone.sol
├── libraries
│ └── EtherFiLibrary.sol
├── oracles
│ ├── reserve
│ │ └── WeEthWstEthReserveOracle.sol
│ └── spot
│ └── WeEthWstEthSpotOracle.sol
└── Constants.sol
Additionally, the following files were reviewed for changes between commit 81d5aee and commit 5333fe3.
src
├── flash
│ └── handlers
│ └── base
│ └── IonHandlerBase.sol
├── interfaces
│ └── ProviderInterfaces.sol
└── YieldOracle.sol
Further, the src/IonPool.sol
file was reviewed for changes between commit c4d5479 and commit 5333fe3.
Update: After all fixes were reviewed, a final review was performed on pull request 59 at commit 0f2cdf6 that merged all changes into a single pull request. This pull request has been validated and confirmed that all fixes have been properly addressed and no unintentional issues have been introduced.
The Ion Protocol implements a borrowing and lending platform for liquid staking tokens (LSTs). LSTs are ERC-20 tokens that represent an amount of tokens staked with a blockchain validator and allow stakers to freely use these tokens while still earning rewards.
This portion of the audit reflects changes to the system in order to add the weETH token from EtherFi as a collateral asset. In addition, changes have been made to generalize borrowing for any asset, allowing weETH to collateralize the borrows of wstETH.
The changes implement new code for borrowing wETH from Uniswap, and using it to mint weETH and borrow wstETH against it. Moreover, new oracles have been implemented, utilizing Chainlink and Redstone oracles, to price weETH within the system.
Multiple oracles have been used in the system to protect against price manipulation. The "reserve oracle" is determined by the LSTs on-chain and is the exchange rate between the LST and ETH. When modifying a debt position, account health is determined based on the spot price of the LSTs. These spot prices are retrieved from a variety of sources. Compared to the prior audit, the new source is for the weETH token and the liquid restaking token (LRT), and uses a Redstone oracle.
Internally, the collateralization value of these assets is taken to be the minimum of these oracle prices and the reserve prices. This ensures that no one can open a debt position that is immediately liquidatable. This also reduces the overall attack surface for opening positions as moving the spot price down would devalue collateral and decrease the amount of debt a user can take on, while increasing the spot prices would not allow more borrowing beyond the reserve oracle rate. Spot market price manipulation also cannot result in a user getting liquidated as liquidations are determined only by the reserve oracle rates.
The system contains numerous integrations with external systems for obtaining price data, swapping assets, and using flash loans. These external systems are assumed to operate as documented and the Ion Protocol team should stay vigilant towards any changes made to these systems that could negatively impact the operation of their system. In particular, the following integrations were added:
All external integrations are assumed to work as documented for the purposes of this audit.
The system is dependent on the weETH and eETH contracts behaving as expected. In addition, "black swan" events which may affect the value of eETH and weETH tokens may have negative effects on the protocol, especially when it comes to valuing collateral or liquidations. Consider monitoring the following:
There exists a section of code within the EtherFiLibrary
contract in which computations for converting between eETH, weETH, and ETH incur some rounding error. Additions of 1 resolve some of the rounding errors and, based on testing, appear to allow the contract to function properly. Although this was examined by the auditors, there was not enough time to conclusively determine the source of the rounding error. As such, we recommend reviewing the rounding error in more detail and either documenting the source of the missing unit(s) or implementing robust functions which account for and predict rounding errors. Note that there were no explicit vulnerabilities found regarding these calculations.
UniswapFlashswapDirectMintHandler
The check against maxResultingDebt
in the UniswapFlashswapDirectMintHandler
contract is incorrect and may mislead users. The intent for this handler is to help leverage positions within the wstETH / weETH market. The maxResultingDebt
parameter is intended to limit the borrowed amount for the user, measured in terms of the "base asset".
Here, the base asset should be wstETH. However, the check compares this value to an amount of wETH. Since wETH units are worth slightly less than wstETH, this will have the effect of causing reverts more often for users as the threshold for "max resulting debt" will be lower than they are anticipating. This will result in a poor user experience overall, as well as potential errors in calculations from trying to convert to a reasonable wstETH value.
Consider either changing the docstrings or name of the variable to make it clear that this value represents wETH. Alternatively, consider converting the amountWethToFlashloan
value to an equivalent value of wstETH before the check.
Update: Resolved in pull request #52.
UniswapFlashswapDirectMintHandler
Unusable When Not LeveragingWithin the UniswapFlashswapDirectMintHandler
contract, the _flashswapAndMint
function contains a clause that is unreachable. This is due to the fact that the getEthAmountInForLstAmountOut
function in the EtherFiLibrary
contract always returns a value of 1
or greater. Intuitively, for an amount "out" of 0, the amount "in" should also be 0. However, due to the addition of 1, this is not possible. The UniswapFlashswapDirectMintHandler
contract is designed to handle cases in which the amountLrt
is 0, and when this is the case, we will get an amountWethToFlashloan
of 1.
Not only does this mean that an unnecessary flash-borrow will occur, but the transaction execution will fail as well. During the flashswap callback, the contract will attempt to mint the collateral asset using the value 1. This will eventually revert inside the eETH liquidity pool contract since the division in _sharesForDepositAmount
returns 0. This prevents the contract from functioning under normal circumstances if a user were to use flashswapAndMint
for depositing collateral and borrowing. Note that this is technically not a given and is dependent on the number of shares being less than the total pooled ETH in the eETH contract.
Consider adding an early-escape check within getEthAmountInForLstAmountOut
, which returns 0 when lrtAmount
is 0.
Update: Resolved in pull request #53.
Throughout the codebase, there are several parts that do not have docstrings:
IRedstone.sol
IonHandlerBase.sol
ProviderInterfaces.sol
UniswapFlashswapDirectMintHandler.sol
UniswapFlashswapHandler.sol
WeEthHandler.sol
YieldOracle.sol
.Consider thoroughly documenting all functions (and their parameters) that are part of any contract's public API. Functions implementing sensitive functionality, even if not public
, should be clearly documented as well. When writing docstrings, consider following the Ethereum Natural Specification Format (NatSpec).
Update: Partially resolved in pull request #54. the following instances remain unresolved:
IonHandlerBase.sol
ProviderInterfaces.sol
UniswapFlashswapDirectMintHandler.sol
UniswapFlashswapHandler.sol
YieldOracle.sol
Named return variables are a way to declare variables that are meant to be used within a function's body for the purpose of being returned as the function's output. They are an alternative to explicit in-line return
statements.
The amountIn
return value for the _initiateFlashSwap
function in the UniswapFlashswapDirectMintHandler
contract is being set but not used in the rest of the function or in the _flashswapAndMint
function it returns to.
Consider either using or removing any unused named return variables.
Update: Resolved in pull request #52.
Consider correcting the following typographical errors to improve the readability of the codebase:
UniswapFlashswapDirectMintHandler.sol
, "the a" should be "the".UniswapFlashswapDirectMintHandler.sol
, "eth" should be "mint asset".WeEthWstEthReserveOracle.sol
, "rate between ETH and weETH" should be "rate between wstETH and weETH".Update: Resolved in pull request #54.
Throughout the codebase, there are multiple unused variables:
flashloanInitiated
state variable in the UniswapFlashswapDirectMintHandler
contract.PROTOCOL_FEED
state variable in the WeEthWstEthReserveOracle
contract.addresses
variable in the _flashswapAndMint
function of the UniswapFlashswapDirectMintHandler
contract is declared and initialized, but otherwise unusedTo improve the overall clarity, intentionality, and readability of the codebase, consider removing any unused variables.
Update: Resolved in pull request #54.
Providing a specific security contact (such as an email or ENS name) within a smart contract significantly simplifies the process for individuals to communicate if they identify a vulnerability in the code. This practice is quite beneficial as it permits the code owners to dictate the communication channel for vulnerability disclosure, eliminating the risk of miscommunication or failure to report due to a lack of knowledge on how to do so. In addition, if the contract incorporates third-party libraries and a bug surfaces in these, it becomes easier for the maintainers of those libraries to make contact with the appropriate person about the problem and provide mitigation instructions.
Throughout the codebase, there are contracts that do not have a security contact:
IRedstonePriceFeed
contractIReserveFeed
contractIStEth
interfaceIWstEth
interfaceIStaderStakePoolsManager
interfaceIStaderConfig
interfaceIStaderOracle
interfaceIETHx
interfaceISwEth
interfaceIWeEth
interfaceIEEth
interfaceIEtherFiLiquidityPool
interfaceWeEthHandler
contractConsider adding a NatSpec comment containing a security contact above the contract definitions. Using the @custom:security-contact
convention is recommended as it has been adopted by the OpenZeppelin Wizard and the ethereum-lists.
Update: Partially resolved in pull request #54. The instances pertaining to the interfaces are not fixed.
Liquidation
Contract Incompatible With IonPool
When ilkIndex
Less Than ThreeInside the constructor of the Liquidation
contract, the _maxDiscounts
, _liquidationThresholds
, and _reserveOracles
arguments are implicitly expected to have lengths of at least three. However, the lengths of these arrays are compared to the ilkCount
returned from the input instance of the IonPool
contract. If the instance of the IonPool
contract uses an ilkCount
of less than three, the constructor will revert when attempting to assign the MAX_DISCOUNT_
state variables with an out-of-bounds error for the array.
Consider refactoring the constructor to accept any non-zero number of ilk
indices.
Update: Resolved in pull request #59 at commit 8c4f615.
Within the InterestRate
contract, the reserveFactor
uses 4 decimal places of precision. However, the constructor for the contract incorrectly validates that the input has 27 (RAY) decimals of precision.
Consider updating the validation in the constructor to check for 4 decimals of precision.
Update: Resolved in pull request #58.
The audit covered changes made to previously audited files along with new components to support allowing the weETH token to be used as collateral. Although the changes are minimal, the introduction of new integrations added further complexity to the overall system. Generally, the complexity was added due to the integrations with the new token, eETH/weETH, and generalizing the contracts used for managing positions to accommodate the wstETH token as a borrowable asset. Two medium-severity issues were identified and recommendations to improve the overall quality and health of the codebase have also been made.
Due to the introduction of additional integrations to the system, advancing the test suite and monitoring that the external contracts are behaving as expected would be of great benefit. Specifically, testing for edge cases, contract upgrades to the eETH/weETH contracts, and testing all logical branches of the swap handler code would provide greater certainty of their behaviors. The Ion team was responsive throughout the audit period, in both answering questions and providing supplementary information about the system.
Appendix - Issue Classification
Critical: The issue puts a large number of users’ sensitive assets at risk and is reasonably likely to have a catastrophic impact on the client’s reputation or serious financial implications for the client or users. It often involves, but is not limited to, some form of loss or locking of funds or other core system functionality failures.
High: The issue puts a large number of users’ sensitive assets at risk and is reasonably likely to have a high impact on the client’s reputation or notable financial implications for the client or users. It often involves, but is not limited to, some form of temporary loss or locking of funds or other core system functionality failures for which reasonable mitigations may be available.
Medium: The issue puts a subset of users’ sensitive assets at risk, which would be detrimental to the client’s reputation if exploited or is reasonably likely to have a moderate financial impact on the client and users. Alternatively, the issue results from an implementation that does not adhere to the specified system design but does not result in financial impact.
Low: The issue is relatively small, could not be exploited regularly, or is a risk that the client has identified as having a low impact given their business model.
Notes & Additional Information: This type of issue is not exploitable but still worth resolving to increase the security and quality of the codebase.