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

Oracle will return 0 when price is correct

Summary

According to the docs on LibWstethEthOracle the price returned will be either:

  • the average price(or minimum) when price comparison is < MAX_DIFFERENCE

  • otherwise the Chainlink price.

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

  • The problem is whenever the LibOracleHelpers.getPercentDifference(chainlinkPrice, uniswapPrice) >= MAX_DIFFERENCE, the value returned will be 0.

// 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);
}

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 exchangerate

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.

  1. 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 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 // 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

  • Return the Chainlink price when the difference is greater than 1% on getWstethEthPrice function:

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.

Updates

Lead Judging Commences

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

wstETH:ETH price max difference

Support

FAQs

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