UniswapV3 TWAP oracle suffers from "infinite price" manipulation problem. If the price is driven above (or below) last tick possesing liquidity, it's free to manipulate the price further, up to max or min tick. Depending on the liquidity depth and distribution, leading to significant changes in 30 minutes TWAP period requires inflated price for just a few blocks and may be relatively cheap. There are 2 additional factors increasing feasibility of this attack:
The manipulation of UniswapV3 on Layer 2 (L2) networks is notably inexpensive and easy due to the absence of flash bots and the reliance on a centralized sequencer that does not engage in Miner Extractable Value (MEV) practices. That means that the bad actor can increase price to "infinity" for few blocks, and impact the price over the accepted limit by Wiselending for 30 minutes - TWAP window duration. It is worth to mention that the attack is relatively inexpensive for the attacker - they require upfront capital to drive the price beyond last tick (can be done via highly leveraged short term loan) and block stuff few blocks, then swap back and repay the loan. Because block stuffing is cheap on L2, the attack costs may be smaller than 30 minutes of inability to trade on Wiselending, which may be critical in case of abrupt market conditions.
There are some big validator farms that hold big power, like LIDO having roughly 1/3 of all stake, meaning that on average they may propose 4 consequtive blocks every 20 minutes due to having 33% of ALL stake. In this situation, the situation might be actually as cheap and severe as described above for L2.
Impact
On-demand prolonged system DoS leading to extracting value from positions that can't be executed on time.
Proof of Concept
To illustrate how the attack impacts the system, I'll go through the code that uses oracle pricing. Whenever price is checked in WiseOracleHub.getTokensInETH(), it calls latestResolver() which verifies allowed difference:
/**
* @dev Returns ETH value of a given token
* amount in order of 1E18 decimal precision.
*/
function getTokensInETH(
address _tokenAddress,
uint256 _tokenAmount
)
public
view
returns (uint256)
{
if (_tokenAddress == WETH_ADDRESS) {
return _tokenAmount;
}
uint8 tokenDecimals = _tokenDecimals[
_tokenAddress
];
return _decimalsETH < tokenDecimals
? _tokenAmount
* latestResolver(_tokenAddress)
/ 10 ** decimals(_tokenAddress)
/ 10 ** (tokenDecimals - _decimalsETH)
: _tokenAmount
* 10 ** (_decimalsETH - tokenDecimals)
* latestResolver(_tokenAddress)
/ 10 ** decimals(_tokenAddress);
}
function latestResolver(
address _tokenAddress
)
public
view
returns (uint256)
{
if (chainLinkIsDead(_tokenAddress) == true) {
revert OracleIsDead();
}
return _validateAnswer( // @audit Chainlink vs Uniswap price difference is done here
_tokenAddress
);
}
Resolver checks if the price is valid, Chainlink oracle is working and difference between UniswapV3 and Chainlink is acceptable in _validateAnswer() function:
function _validateAnswer(
address _tokenAddress
)
internal
view
returns (uint256)
{
// [...]
if (fetchTwapValue > 0) {
uint256 relativeDifference = _getRelativeDifference( // @audit it's smaller price devided by the bigger
answer,
fetchTwapValue
);
_compareDifference(
relativeDifference
);
}
At the end, the difference is compared in _compareDifference() to check if the two prices are not too distant from each other:
function _compareDifference(
uint256 _relativeDifference
)
internal
view
{
if (_relativeDifference > ALLOWED_DIFFERENCE) { // @audit initial value 10250, common for all tokens
revert OraclesDeviate();
}
}
It's worth mentioning that the allowed price diference (expressed in percentage) is common for all token prices, which might be dangerous for low liquidity assets.
So, to summarize, if the prices deviate by too much, it will revert with OraclesDeviate error. It will actually revert until the outlier block values are past TWAP period, which is hardcoded to 30 minutes in the code.
Tools Used
Manual Review
Recommended Mitigation Steps
While we understand the preventive measures of Wiselending protecting against black swan event on Chainlink oracles, we believe that the Wiselending checks should be more dynamic and adjusted on the underlying asset basis. Allow for more dynamic price deviation percentage per pool / chain, because of ease of TWAP manipulation on L2.
Lines of code
https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/WiseOracleHub/WiseOracleHub.sol#L143-L168 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/WiseOracleHub/OracleHelper.sol#L163-L171
Vulnerability details
UniswapV3 TWAP oracle suffers from "infinite price" manipulation problem. If the price is driven above (or below) last tick possesing liquidity, it's free to manipulate the price further, up to max or min tick. Depending on the liquidity depth and distribution, leading to significant changes in 30 minutes TWAP period requires inflated price for just a few blocks and may be relatively cheap. There are 2 additional factors increasing feasibility of this attack:
The manipulation of UniswapV3 on Layer 2 (L2) networks is notably inexpensive and easy due to the absence of flash bots and the reliance on a centralized sequencer that does not engage in Miner Extractable Value (MEV) practices. That means that the bad actor can increase price to "infinity" for few blocks, and impact the price over the accepted limit by Wiselending for 30 minutes - TWAP window duration. It is worth to mention that the attack is relatively inexpensive for the attacker - they require upfront capital to drive the price beyond last tick (can be done via highly leveraged short term loan) and block stuff few blocks, then swap back and repay the loan. Because block stuffing is cheap on L2, the attack costs may be smaller than 30 minutes of inability to trade on Wiselending, which may be critical in case of abrupt market conditions.
There are some big validator farms that hold big power, like LIDO having roughly 1/3 of all stake, meaning that on average they may propose 4 consequtive blocks every 20 minutes due to having 33% of ALL stake. In this situation, the situation might be actually as cheap and severe as described above for L2.
Impact
On-demand prolonged system DoS leading to extracting value from positions that can't be executed on time.
Proof of Concept
To illustrate how the attack impacts the system, I'll go through the code that uses oracle pricing. Whenever price is checked in
WiseOracleHub.getTokensInETH()
, it callslatestResolver()
which verifies allowed difference:Resolver checks if the price is valid, Chainlink oracle is working and difference between UniswapV3 and Chainlink is acceptable in
_validateAnswer()
function:At the end, the difference is compared in
_compareDifference()
to check if the two prices are not too distant from each other:It's worth mentioning that the allowed price diference (expressed in percentage) is common for all token prices, which might be dangerous for low liquidity assets.
So, to summarize, if the prices deviate by too much, it will revert with
OraclesDeviate
error. It will actually revert until the outlier block values are past TWAP period, which is hardcoded to 30 minutes in the code.Tools Used
Manual Review
Recommended Mitigation Steps
While we understand the preventive measures of Wiselending protecting against black swan event on Chainlink oracles, we believe that the Wiselending checks should be more dynamic and adjusted on the underlying asset basis. Allow for more dynamic price deviation percentage per pool / chain, because of ease of TWAP manipulation on L2.
Assessed type
DoS