Platform: Code4rena
Start Date: 09/12/2021
Pot Size: $50,000 USDC
Total HM: 19
Participants: 21
Period: 7 days
Judge: 0xean
Total Solo HM: 14
Id: 61
League: ETH
Rank: 8/21
Findings: 3
Award: $2,211.16
π Selected for report: 4
π Solo Findings: 0
1267.5392 USDC - $1,267.54
hyh
Full withdrawal and moving funds between strategies can lead to wrong accounting if the corresponding market has tight liquidity, which can be the case at least for AaveYield
. That is, as the whole amount is required to be moved at once from Aave, both withdrawAll
and switchStrategy
will incorrectly account for partial withdrawal as if it was full whenever the corresponding underlying yield pool had liquidity issues.
withdrawAll
will delete user entry, locking the user funds in the strategy: user will get partial withdrawal and have the corresponding accounting entry removed, while the remaining actual funds will be frozen within the system.
switchStrategy
will subtract full number of shares for the _amount
requested from the old strategy, while adding lesser partial number of shares for _tokensReceived
to the new one with the same effect of freezing user's funds within the system.
SavingsAccount.withdrawAll https://github.com/code-423n4/2021-12-sublime/blob/main/contracts/SavingsAccount/SavingsAccount.sol#L286
SavingsAccount.switchStrategy: https://github.com/code-423n4/2021-12-sublime/blob/main/contracts/SavingsAccount/SavingsAccount.sol#L152
When full withdrawal or strategy switch is performed it is one withdraw via unlockTokens
without checking the amount received.
In the same time the withdraw can fail for example for the strategy switch if old strategy is having liquidity issues at the moment, i.e. Aave market is currently have utilization rate too high to withdraw the amount requested given current size of the lending pool.
Aave unlockTokens
return is correctly not matched with amount requested:
https://github.com/code-423n4/2021-12-sublime/blob/main/contracts/yield/AaveYield.sol#L217
But, for example, withdrawAll
ignores the fact that some funds can remain in the strategy and deletes the use entry after one withdraw attempt:
https://github.com/code-423n4/2021-12-sublime/blob/main/contracts/SavingsAccount/SavingsAccount.sol#L294
https://github.com/code-423n4/2021-12-sublime/blob/main/contracts/SavingsAccount/SavingsAccount.sol#L312
switchStrategy
removes the old entry completely:
https://github.com/code-423n4/2021-12-sublime/blob/main/contracts/SavingsAccount/SavingsAccount.sol#L181
For both withdrawAll
and switchStrategy
the immediate fix is to account for tokens received in both cases, which are _amount
after unlockTokens
for withdrawAll
and _tokensReceived
for switchStrategy
.
More general handling of the liquidity issues ideally to be addressed architecturally, given the potential issues with liquidity availability any strategy withdrawals can be done as follows:
#0 - ritik99
2021-12-23T10:01:52Z
The above issue requires making a few assumptions - (i) the underlying yield protocol does not have sufficient reserves to facilitate the withdrawal of a single user, (ii) the user attempts to withdraw all their assets during such times of insufficient reserves.
We agree that the above could be a possibility, but would be unlikely. The underlying yield protocols undergo an interest rate spike during high utilization ratios to bring reserves back to normal levels, and some revert if they cannot withdraw the necessary amount (for eg, Compound). During live deployment, only those strategies that work expectedly would be onboarded, while others wouldn't (for eg, Aave as a strategy wouldn't be integrated until their wrappers for aTokens are ready for use). Hence we suggest reducing severity to (2) medium-risk
#1 - ritik99
2021-12-27T05:59:22Z
also similar to #144
#2 - 0xean
2022-01-21T00:43:42Z
While I understand the argument regarding this being an unlikely scenario, I don't believe that is a sufficient reason to downgrade the issue give the impact to a user and the lost funds.
2 β Med: Assets not at direct risk, but the function of the protocol or its availability could be impacted, or leak value with a hypothetical attack path with stated assumptions, but external requirements.
In this scenario - Assets are at a direct risk.
380.2618 USDC - $380.26
hyh
Funds that are acquired from a liquidator and should be sent to a lender are left with the contract instead. The funds aren't lost, but after the fact mitigation will require manual accounting and fund transfer for each CreditLine.liquidate usage.
ETH sent to CreditLine.liquidate by an external liquidator when autoLiquidation
is enabled remain with the contract and aren't transferred to the lender:
https://github.com/code-423n4/2021-12-sublime/blob/main/contracts/CreditLine/CreditLine.sol#L1015
Add transfer to a lender for ETH case:
Now:
if (_borrowAsset == address(0)) { uint256 _returnETH = msg.value.sub(_borrowTokens, 'Insufficient ETH to liquidate'); if (_returnETH != 0) { (bool success, ) = msg.sender.call{value: _returnETH}(''); require(success, 'Transfer fail'); } }
To be:
if (_borrowAsset == address(0)) { uint256 _returnETH = msg.value.sub(_borrowTokens, 'Insufficient ETH to liquidate'); (bool success, ) = _lender.call{value: _borrowTokens}(''); require(success, 'liquidate: Transfer failed'); if (_returnETH != 0) { (success, ) = msg.sender.call{value: _returnETH}(''); require(success, 'liquidate: Return transfer failed'); } }
π Selected for report: hyh
281.6754 USDC - $281.68
hyh
If SavingsAccount implementation be switched to another contract while system is live in production, i.e. have deposited funds and ongoing loans, the core functionality will become broken as there is no user balances transfer mechanics implemented.
There is no mechanics to transfer the contents of user account ledger, the balanceInShares and allowance mappings of SavingsAccount that hold user data, so updating SavingsAccount when the system is in production will break it.
Both CreditLine and Pool have the ability to switch SavingsAccount to another implementation:
CreditLine.updateSavingsAccount https://github.com/code-423n4/2021-12-sublime/blob/main/contracts/CreditLine/CreditLine.sol#L317
PoolFactory.updateSavingsAccount https://github.com/code-423n4/2021-12-sublime/blob/main/contracts/Pool/PoolFactory.sol#L575
There is a SavingsAccount.withdrawAll mechanics, but it exists for a particular account owner. In a case of emergecy the probability that all users will be able to timely run withdrawAll is quite low.
For example, if a bug be found in current SavingsAccount implementation, the updateSavingsAccount functions will not be useful as the system will not be functional after the update as user balances information will be lost.
There are at least two options, restrict current logic to pre-prod stages or implement ledger transfer mechanics to expand the ability of SavingsAccount implementation update to production:
Restric current logic: add a mechanics to switch SavingsAccount update off on the system's release (i.e. have it for alpha/beta stages, then switch it off on release), by adding the corresponding variable, say isProduction
, with onlyOwner access, that will be switched on when the system go live publicly, and require isProduction == 0
for the SavingsAccount setter functions.
Implement ledger transfer mechanics, i.e. add a sender and receiver functions to SavingsAccount which read&send and receive&store correspondingly the current contents of balanceInShares and allowance mappings. This way the aggregated funds are still to be transferred manually, while the mappings of whose money it is will be copied directly.
#0 - ritik99
2021-12-27T05:05:17Z
We follow a proxy pattern that allows us to update logic without losing data. Upgrading will only be necessary if there are major bugs in the contract. Also, since we can update the logic, it allows us to add in mechanics to load data at a later time as well. Hence we suggest lowering the severity to (1) Low-risk
#1 - 0xean
2022-01-21T16:54:31Z
downgrading based on proxy pattern that can maintain state
π Selected for report: hyh
281.6754 USDC - $281.68
hyh
CreditLine can be liquidated at non-market price at certain attainable set of conditions, leading to irreversible damage for the borrower, lender, or both, depending on the current collateral ratio and the manipulated price.
Suppose Oracle price becomes stale due to Oracle implementation related reasons, the CreditLine is active and big enough, autoLiquidation
is true.
An attacker can move Uniswap pool used as a backup price source in priceOracle.getLatestPrice and liquidate the CreditLine, obtaining collateral at a discounted price.
For example, if CreditLine's idealCollateralRatio
is 100%, current ratio is 110%, Uniswap pool is manipulated to indicate the price 20% lower than the current market price, so the collateral ratio be 110% * 0.8 = 88% and liquidate will go through, sending the collateral to the attacker. The attacker pays and the lender gets _borrowTokensToLiquidate
amount of the borrow tokens, which is 88% of the principal (we omit liquidator's reward and interest for illustration simplicity here), leading to 100 - 88 = 12% loss for the lender, and 110 - 100 = 10% loss for the borrower in the terms of the principal amount.
The attack is economically viable as long as CreditLine collateral is big enough compared with the current size of Uniswap pool, which can happen with growing usage of the system.
When autoLiquidation is false a malicious lender can perform the same attack, which is less likely, but still possible.
CreditLine.liquidate allows performing the liquidation in one step: https://github.com/code-423n4/2021-12-sublime/blob/main/contracts/CreditLine/CreditLine.sol#L996
The collateral ratio condition is checked only once and the liquidation proceeds immediately, so any price manipulation leads directly to CreditLine exploit: it can be done within one transaction with no risk for an attacker and allowing flash loan usage.
A delay between intention and action can be a simple and effective way to significantly reduce attractiveness of manipulation attacks by worsening their risk-reward profile: market has to be skewed for some time / twice, which posses much bigger risk for an attacker and substantially reduces the attractiveness of flash loans usage.
The same request-wait-liquidate approach is used in the Pool contract, and it looks to be suitable for CreditLine: Pool.requestMarginCall https://github.com/code-423n4/2021-12-sublime/blob/main/contracts/Pool/Pool.sol#L656 Pool.liquidateForLender https://github.com/code-423n4/2021-12-sublime/blob/main/contracts/Pool/Pool.sol#L864
#0 - ritik99
2022-01-07T23:15:14Z
This was a design choice. The current architecture for credit lines provides enough room for flexibility to allow for users with different kinds of risk appetites. For reg, those that do not wish to be influenced by short-term movements in collateral prices could set the autoLiquidate argument as true, giving the lender full freedom in choosing when the liquidation takes place. Additionally, as mentioned in the contest readme the oracles are assumed to be reliable
#1 - 0xean
2022-01-21T16:52:17Z
Downgrading to low risk as the manipulation of oracle's is not unique the this contract suite and is inherent in their use.