Platform: Code4rena
Start Date: 23/09/2021
Pot Size: $50,000 USDC
Total HM: 5
Participants: 14
Period: 7 days
Judge: ghoulsol
Total Solo HM: 3
Id: 32
League: ETH
Rank: 2/14
Findings: 5
Award: $11,696.01
🌟 Selected for report: 5
🚀 Solo Findings: 2
cmichel
There is no check in UniswapV3Oracle.ethPrice
if the return values indicate stale data. This could lead to stale prices according to the Chainlink documentation:
It also seems to use the deprecated API (latestAnswer
) and a hardcoded decimals of 1e8?
This API is deprecated. Please see API Reference for the latest Price Feed API. Chainlink Docs
Stale prices that do not reflect the current market price anymore could be used which would influence the liquidation pricing.
Add the recommended checks:
( uint80 roundID, int256 price, , uint256 timeStamp, uint80 answeredInRound ) = chainlink.latestRoundData(); require( timeStamp != 0, “ChainlinkOracle::getLatestAnswer: round is not complete” ); require( answeredInRound >= roundID, “ChainlinkOracle::getLatestAnswer: stale data” ); require(price != 0, "Chainlink Malfunction”);
#0 - talegift
2021-10-01T12:53:56Z
Duplicate #55
🌟 Selected for report: cmichel
3964.9416 USDC - $3,964.94
cmichel
The LendingPair.withdrawUniPosition
function allows the user to withdraw their UniswapV3 pool position (NFT) again.
As the Uniswap position acts as collateral in the protocol, a health check is performed afterwards.
However, it does not check the current debt of the caller as it does not accrue
the debt for both tokens first.
In the worst case, in low-activity markets, it could happen that debt has not accrued for a long time and the current debt is significantly higher than the current recorded debt in totalDebtAmount
.
An account with a de-facto negative health ratio if the debt was accrued could still withdraw their collateral NFT instead of having to repay their debt first.
Accrue the debt for both tokens first in LendingPair.withdrawUniPosition
.
🌟 Selected for report: cmichel
3964.9416 USDC - $3,964.94
cmichel
The LendingPair.uniClaimDeposit
function allows the user to "collect fees" and mint new supply shares with the collected amounts.
uniClaimDeposit
does not accrue tokensHowever, the current total supply is not accrue
d in the function.
This means an attacker can:
uniClaimDeposit
totalSupplyAmount
by calling accrue(token0)
and accrue(token1)
afterwards.withdraw
and receive a larger amount of tokens for the newly minted shares due to the increase in totalSupplyAmount
from accrue
(increasing the supply share price _sharesToSupply
).This would only lead to a small protocol loss if uniClaimDeposit
would only collect the fees, however, combined with another flaw, one can steal almost the entire protocol lpRate
each time:
uniClaimDeposit
allows collecting entire liquidity instead of just feesThis has to do with the way liquidity from a Uniswap V3 position (NFT) is withdrawn:
positionManager.decreaseLiquidity
, the position.liquidity
is removed but stored in the position as tokensOwed0/tokensOwed1
. It is not transferred to the userpositionManager.collect(params)
to actually transfer out these tokens, setting tokensOwed0/1
to 0
. (This is correctly done in UniswapV3Helper.removeLiquidity
.)An attacker can perform the following attack:
positionManager.decreaseLiquidity
such that the entire liquidity is removed and stored (but not collected yet) in the position's tokensOwed0/1
fieldsdepositUniPosition
uniClaimDeposit
to mint a huge amount of NFT supply shares. This huge amount will capture the protocol's debt accrual in the next steps.accrue
on both tokens to accrue debt and pay the lpRate
part of it to suppliers, increasing totalSupplyAmount
and thus the value of a supply share.totalSupplyAmount
, the attacker can now withdraw their minted shares again and capture most of the new debt that was accrued, making a profit.Combining these two issues, an attacker could steal most of the accrued lpRate
in a single atomic transaction.
The attacker can repeat this step capturing the supplier interest for each accrual. (The longer the market hasn't been accrued, the bigger the profit per single attack transaction, but in the end, the attacker could perform this attack at every block or when it becomes profitable for the gas costs.)
Providing / removing Uniswap V3 liquidity does not incur fees.
The attacker's profit is the loss of other legitimate suppliers that capture less of the newly accrued debt.
Accrue the debt for both tokens first in LendingPair.uniClaimDeposit
.
It might also be a good idea to disallow collecting the "parked" liquidity in a token (that has been removed but not yet collected) by immediately collecting them when the NFT is deposited in depositUniPosition
. I.e., call _uniCollectFees
in depositUniPosition
to withdraw any outstanding tokens&fees.
Then mint shares with these token amounts.
#0 - talegift
2021-10-03T05:13:45Z
We'll implement the suggested fix.
Suggest lowering severity to 2 as it doesn't allow direct theft of funds and the loss would only occur under specific external conditions - long periods of not accrue interest combined with a low gas price to steal the pending interest.
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
https://docs.code4rena.com/roles/wardens/judging-criteria#estimating-risk-tl-dr
#1 - ghoul-sol
2021-10-12T05:31:47Z
It seems that the attacker can steal interest that is owed to other users but deposits are safe. For that reason I agree with sponsor to make this medium risk.
🌟 Selected for report: cmichel
1321.6472 USDC - $1,321.65
cmichel
The _accrueInterest
function uses a simple interest formula to compute the accrued debt, instead of a compounding formula.
This means the actual borrow rate and interest for suppliers depend on how often updates are made. This difference should be negligible in highly active markets, but it could lead to a lower borrow rate in low-activity markets.
Ensure that the lending pairs is accrued regularly, or switch to a compound interest formula (which has a higher gas cost due to exponentiation, but can be approximated, see Aave).
🌟 Selected for report: cmichel
1321.6472 USDC - $1,321.65
cmichel
The UniswapV3Oracle.tokenPrice
function gets the price by combining the chainlink ETH price with the TWAP prices of the token <> pairToken
and pairToken <> WETH
pools.
It is therefore required that the pairToken <> WETH
pool exists and has sufficient liquidity to be tamper-proof.
When listing lending pairs for tokens that have a WETH pair with low liquidity (at 0.3% fees) the prices can be easily manipulated leading to liquidations or underpriced borrows.
This can happen for tokens that don't use WETH
as their default trading pair, for example, if they prefer a stablecoin, or WBTC
.
Ensure there's enough liquidity on the pairToken <> WETH
Uniswap V3 0.3% pair, either manually or programmatically.
#0 - talegift
2021-10-01T12:55:51Z
We'll only support tokens that have sufficient liquidity on UniV3 in an ETH pair.
52.3013 USDC - $52.30
cmichel
The LendingPair.repayAllETH
function takes a _maxAmount
parameter.
However, this parameter is not necessary as the caller's msg.value
already has the same behavior of a _maxAmount
.
Remove the _maxAmount
parameter to save gas.
Users should use msg.value
as a max amount.