Summary
According to the docs on LibWstethEthOracle
the price returned will be either:
Reference from the docs at LibWstethEtheOracle
* It then computes a wstETH:ETH price by taking the minimum of (3) and either the average of (1) and (2)
* if (1) and (2) are within `MAX_DIFFERENCE` from each other or (1).
The assumption above is correct, because:
-
The Chainlink STETH-ETH is the most reliable source for the STETH
price.
-
The Uniswap Pool for WSTETH-ETH has low liquidity(only 30m currently - https://etherscan.io/address/0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0) and higher volatility, therefore when the range of price between Chainlink & Uniswap is greater than 1%, Chainlink price should be used.
The problem is that even though the docs is correct, the implementation and the tests aren't.
Vulnerability Details
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);
}
Another point to consider is stETH is a rebased token so the exchange rate between wstETH-stETH is not 1:1. https://data.chain.link/feeds/arbitrum/mainnet/wsteth-steth%20exchangerate
Impact
Oracle will return 0, even though the price price from Chainlink is valid.
All contracts that rely on the Oracle price will be affected.
PoC
Run the PoC below. This PoC contains the block number from today's date(8th April 2024). This will show that if those contracts were deployed today, the Oracle price would be always 0. Try to set different days in the past and you will notice that this will be the result most of the time.
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;
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 onWstethOracle.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
},
},
],
});
} 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();
const wsteth = await ethers.getContractAt('MockWsteth', WSTETH);
const chainlinkAggregator = await ethers.getContractAt('MockChainlinkAggregator', STETH_ETH_CHAINLINK_PRICE_AGGREGATOR);
await impersonateWsteth();
const wstethEthUniswapPool = await wsteth.wstethEthUniswapPoolPrice();
console.log('wstethEthUniswapPool', wstethEthUniswapPool);
const ethPerSteth = (await chainlinkAggregator.latestRoundData()).answer;
console.log('STETH_ETH chainlink price', ethPerSteth);
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
if (LibOracleHelpers.getPercentDifference(chainlinkPrice, uniswapPrice) < MAX_DIFFERENCE) {
wstethEthPrice = chainlinkPrice.add(uniswapPrice).div(AVERAGE_DENOMINATOR);
if (wstethEthPrice > stethPerWsteth) wstethEthPrice = stethPerWsteth;
wstethEthPrice = wstethEthPrice.div(PRECISION_DENOMINATOR);
+ } else {
+ wstethEthPrice = chainlinkPrice.div(PRECISION_DENOMINATOR);
+ }
Also, consider returning the Chainlink price when uniswapPrice
is 0.
(Improve readability) Change the variable name WSTETH_ETH_CHAINLINK_PRICE_AGGREGATOR
to STETH_ETH_CHAINLINK_PRICE_AGGREGATOR
.