DeFiHardhat
35,000 USDC
View results
Submission Details
Severity: low
Valid

LibWstethEthOracle is broken due to integration with Uniswap and wrong exchange rate

Summary

The integration with Uniswap breaks the functionality of LibWstethEthOracle. For the following reasons:

Vulnerability Details

if (lookback > type(uint32).max) return 0;
uint256 uniswapPrice = LibUniswapOracle.getTwap(
lookback == 0 ? LibUniswapOracle.FIFTEEN_MINUTES :
uint32(lookback),
WSTETH_ETH_UNIV3_01_POOL, C.WSTETH, C.WETH, ONE
);
// Check if the uniswapPrice oracle fails.
if (uniswapPrice == 0) return 0;
if (LibOracleHelpers.getPercentDifference(chainlinkPrice, uniswapPrice) < MAX_DIFFERENCE) {
wstethEthPrice = chainlinkPrice.add(uniswapPrice).div(AVERAGE_DENOMINATOR);
if (wstethEthPrice > stethPerWsteth) wstethEthPrice = stethPerWsteth;
wstethEthPrice = wstethEthPrice.div(PRECISION_DENOMINATOR);
}
  • Due to the logic to fetch the average price, the wrong exchange rate stETH:wsETH, and the missing else for the case that MAX_DIFFERENCE is >= 1 the function will return 0. Already reported here in my other finding: "Oracle will return 0 when price is correct".

  • Fetching the price from a low liquidity pool from Uniswap doesn't make the protocol safer or the price fair for users/protocol. It shouldn't be used.

  • STETH/ETH Price feed has a heartbeat of 24 hours. When not returning zero, oracle has a high probability of returning an outdated price due to heartbeat + pool with low liquidity(not reliable with the current market price).

Impact

  • Oracle will return 0 and when not returning zero:

  • Oracle will return the wrong price due to 24-hour heartbeat(outdated price) + low liquidity pool.

PoC

  • Update the MockWsteth.sol so it can return the uniswap pool price:

pragma solidity =0.7.6;
import "./MockToken.sol";
import {IWsteth} from "contracts/libraries/Oracle/LibWstethEthOracle.sol";
import "contracts/libraries/Oracle/LibUniswapOracle.sol";
import "contracts/C.sol";
/**
* @author Brendan
* @title Mock WStEth
**/
contract MockWsteth is MockToken {
address internal constant WSTETH_ETH_UNIV3_01_POOL = 0x109830a1AAaD605BbF02a9dFA7B0B92EC2FB7dAa; // 0.01% pool
uint128 constant ONE = 1e18;
uint256 _stEthPerToken;
constructor() MockToken("Wrapped Staked Ether", "WSTETH") {
_stEthPerToken = 1e18;
}
function setStEthPerToken(uint256 __stEthPerToken) external {
_stEthPerToken = __stEthPerToken;
}
function stEthPerToken() external view returns (uint256) {
return _stEthPerToken;
}
function wstethEthUniswapPoolPrice() public view returns (uint256 uniswapPrice) {
uniswapPrice = LibUniswapOracle.getTwap(
LibUniswapOracle.FIFTEEN_MINUTES,
WSTETH_ETH_UNIV3_01_POOL, C.WSTETH, C.WETH, ONE
);
}
}

Add the following test on WstethOracle.test.js:

testIfRpcSet('wStEth Oracle with Forking', function () {
it("Return zero if protocol is launched today", async function () {
try {
await network.provider.request({
method: "hardhat_reset",
params: [
{
forking: {
jsonRpcUrl: process.env.FORKING_RPC,
blockNumber: 19610305 // today's block. Apr 8, 2024
},
},
],
});
} catch (error) {
console.log('forking error in WstethOracle');
console.log(error);
return
}
const UsdOracle = await ethers.getContractFactory('UsdOracle');
const usdOracle = await UsdOracle.deploy();
await usdOracle.deployed();
// price contracts
const wsteth = await ethers.getContractAt('MockWsteth', WSTETH);
const chainlinkAggregator = await ethers.getContractAt('MockChainlinkAggregator', STETH_ETH_CHAINLINK_PRICE_AGGREGATOR);
// interpersonate so we can use the auxiliar function of the contract to get the pool price
await impersonateWsteth();
// print prices
const wstethEthUniswapPool = await wsteth.wstethEthUniswapPoolPrice();
console.log('wstethEthUniswapPool', wstethEthUniswapPool);
const ethPerSteth = (await chainlinkAggregator.latestRoundData()).answer;
console.log('STETH_ETH chainlink price', ethPerSteth);
// difference is greater than 1% and oracle will return zero
expect(await usdOracle.getWstethUsdPrice()).to.be.equal(0);
})
})

Then run: npx hardhat test test/WstethOracle.test.js --network hardhat

Output - price difference > 1%, oracle returns 0:

wStEth Oracle with Forking
wstethEthUniswapPool BigNumber { value: "1161128678185570980" }
STETH_ETH chainlink price BigNumber { value: "998125002771300500" }
✓ Return zero if protocol is launched today

Tools Used

Manual Review & Hardhat

Recommendations

  • Remove completely the Uniswap integration

  • As the price feed for stETH/ETH has a heartbeat of 24 hours:

  • Add a 2nd price feed from chainlink stETH/USD: https://data.chain.link/feeds/ethereum/mainnet/steth-usd, it contains a heartbeat of 1 hour and at the same time it is another source of liquidity/price for stETH.

  • Add timeout for both oracles based on their heartbeats(STETH/ETH - 24 hours, STETH/USD - 1hour)

-(Optional) Recommended a fallback of the last healthy price as used on Liquity protocol: https://github.com/liquity/dev/blob/main/packages/contracts/contracts/PriceFeed.sol

References

https://docs.lido.fi/guides/lido-tokens-integration-guide/#sttokens-steth-and-wsteth

https://github.com/code-423n4/2023-11-kelp-findings/issues/609

Updates

Lead Judging Commences

giovannidisiena Lead Judge
about 1 year ago
giovannidisiena Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

wstETH:ETH price calculation

holydevoti0n Submitter
about 1 year ago
giovannidisiena Lead Judge
about 1 year ago
holydevoti0n Submitter
about 1 year ago
giovannidisiena Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

wstETH:ETH price calculation

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.