Skip to content

Commit

Permalink
feat: chainlink-like wsteth price feed
Browse files Browse the repository at this point in the history
  • Loading branch information
SeanJCasey authored Aug 22, 2024
1 parent 0f13246 commit ae37251
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: GPL-3.0

/*
This file is part of the Enzyme Protocol.
(c) Enzyme Foundation <foundation@enzyme.finance>
For the full license information, please view the LICENSE
file that was distributed with this source code.
*/

pragma solidity 0.8.19;

import {IChainlinkAggregator} from "../../../../external-interfaces/IChainlinkAggregator.sol";
import {ILidoSteth} from "../../../../external-interfaces/ILidoSteth.sol";

/// @title ChainlinkLikeWstethPriceFeed Contract
/// @author Enzyme Foundation <security@enzyme.finance>
/// @notice Price feed for Lido wrapped stETH (wstETH), wrapped in a Chainlink-like interface and quoted in ETH
/// @dev Relies on a stETH/ETH feed with the Chainlink aggregator interface for the intermediary conversion step
contract ChainlinkLikeWstethPriceFeed is IChainlinkAggregator {
uint8 private constant CHAINLINK_AGGREGATOR_ETH_QUOTE_DECIMALS = 18;
uint256 private constant STETH_UNIT = 10 ** 18;
uint256 private constant WSTETH_UNIT = 10 ** 18;

ILidoSteth private immutable STETH;
IChainlinkAggregator private immutable STETH_ETH_CHAINLINK_AGGREGATOR;

error ChainlinkLikeWstethPriceFeed__NegativeAnswer();

constructor(ILidoSteth _steth, IChainlinkAggregator _stethEthChainlinkAggregator) {
STETH = _steth;
STETH_ETH_CHAINLINK_AGGREGATOR = _stethEthChainlinkAggregator;
}

/// @notice The number of precision decimals in the aggregator answer
/// @return decimals_ The number of decimals
function decimals() external pure override returns (uint8 decimals_) {
return CHAINLINK_AGGREGATOR_ETH_QUOTE_DECIMALS;
}

/// @notice Returns Chainlink-like latest round data for the wstETH/ETH pair
/// @return roundId_ Unused
/// @return answer_ The price of wstETH quoted in ETH
/// @return startedAt_ The `startedAt_` value returned by the Chainlink stETH/ETH aggregator `latestRoundData()`
/// @return updatedAt_ The `updatedAt_` value returned by the Chainlink stETH/ETH aggregator `latestRoundData()`
/// @return answeredInRound_ Unused
/// @dev Does not pass through round-related values, to avoid misinterpretation
function latestRoundData()
external
view
override
returns (uint80, int256 answer_, uint256 startedAt_, uint256 updatedAt_, uint80)
{
// steth-per-wsteth rate
uint256 stethPerWsteth = STETH.getPooledEthByShares({_sharesAmount: WSTETH_UNIT});

// eth-per-steth rate
int256 ethPerStethAnswer;
(, ethPerStethAnswer, startedAt_, updatedAt_,) = STETH_ETH_CHAINLINK_AGGREGATOR.latestRoundData();

if (ethPerStethAnswer < 0) {
revert ChainlinkLikeWstethPriceFeed__NegativeAnswer();
}

// eth-per-wsteth rate
answer_ = int256(uint256(ethPerStethAnswer) * stethPerWsteth / STETH_UNIT);

return (uint80(0), answer_, startedAt_, updatedAt_, uint80(0));
}
}
2 changes: 1 addition & 1 deletion tests/bases/IntegrationTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ abstract contract IntegrationTest is CoreUtils {
}

function setUpMainnetEnvironment(uint256 _forkBlock) internal {
vm.createSelectFork("mainnet", _forkBlock);
vm.createSelectFork({urlOrAlias: "mainnet", blockNumber: _forkBlock});

v4ReleaseContracts = getV4MainnetReleaseContracts();

Expand Down
2 changes: 2 additions & 0 deletions tests/interfaces/external/ILidoSteth.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@
pragma solidity >=0.6.0 <0.9.0;

interface ILidoSteth {
function getPooledEthByShares(uint256 _sharesAmount) external view returns (uint256 ethAmount_);

function submit(address _referral) external payable;
}
87 changes: 87 additions & 0 deletions tests/tests/protocols/lido/ChainlinkLikeWstethPriceFeed.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.19;

import {IntegrationTest} from "tests/bases/IntegrationTest.sol";
import {IChainlinkAggregator} from "tests/interfaces/external/IChainlinkAggregator.sol";
import {IERC20} from "tests/interfaces/external/IERC20.sol";
import {ILidoSteth} from "tests/interfaces/external/ILidoSteth.sol";

contract ChainlinkLikeWstethPriceFeedTest is IntegrationTest {
IChainlinkAggregator wstethAggregator;
IChainlinkAggregator originalStethEthAggregator = IChainlinkAggregator(ETHEREUM_STETH_ETH_AGGREGATOR);

function setUp() public override {
vm.createSelectFork("mainnet", ETHEREUM_BLOCK_LATEST);

wstethAggregator = __deployWstethAggregator();
}

// DEPLOYMENT HELPERS

function __deployWstethAggregator() private returns (IChainlinkAggregator wstethAggregator_) {
bytes memory args = abi.encode(ETHEREUM_STETH, ETHEREUM_STETH_ETH_AGGREGATOR);

address addr = deployCode("ChainlinkLikeWstethPriceFeed.sol", args);

return IChainlinkAggregator(addr);
}

// TESTS

function test_decimals_success() public {
assertEq(wstethAggregator.decimals(), CHAINLINK_AGGREGATOR_DECIMALS_ETH, "Incorrect decimals");
}

function test_latestRoundData_successWithForkData() public {
// Query return data of stETH/ETH aggregator and the simulated wstETH/ETH aggregator
(,, uint256 originalStartedAt, uint256 originalUpdatedAt,) = originalStethEthAggregator.latestRoundData();
(
uint80 wstethRoundId,
int256 wstethAnswer,
uint256 wstethStartedAt,
uint256 wstethUpdatedAt,
uint80 wstethAnsweredInRound
) = wstethAggregator.latestRoundData();

// startedAt and updatedAt should be passed-through as-is
assertEq(wstethStartedAt, originalStartedAt, "Incorrect startedAt");
assertEq(wstethUpdatedAt, originalUpdatedAt, "Incorrect updatedAt");

// Round values should be empty
assertEq(wstethRoundId, 0, "Non-zero roundId");
assertEq(wstethAnsweredInRound, 0, "Non-zero roundData");

// Rate: 1.17 ETH/wstETH, on June 24th, 2024
// https://www.coingecko.com/en/coins/wrapped-steth
uint256 expectedWstethEthRate = 1.17e18;
uint256 halfPercent = WEI_ONE_PERCENT / 2;
assertApproxEqRel(uint256(wstethAnswer), expectedWstethEthRate, halfPercent, "Incorrect rate");
}

function test_latestRoundData_successWithAlteredRates() public {
// Mock return values of stETH and wstETH sources to be:
// - eth-per-steth rate is 5e18
// - steth-per-wsteth rate is 2e18
// Expected eth-per-wsteth rate: 10e18
uint256 ethPerStethRate = 5e18;
uint256 stethPerWstethRate = 2e18;
uint256 expectedEthPerWstethRate = 10e18;

// Mock call on the Chainlink aggregator
vm.mockCall({
callee: address(originalStethEthAggregator),
data: abi.encodeWithSelector(IChainlinkAggregator.latestRoundData.selector),
returnData: abi.encode(1, ethPerStethRate, 345, 456, 2)
});

// Mock call in stETH
vm.mockCall({
callee: ETHEREUM_STETH,
data: abi.encodeWithSelector(ILidoSteth.getPooledEthByShares.selector, assetUnit(IERC20(ETHEREUM_WSTETH))),
returnData: abi.encode(stethPerWstethRate)
});

(, int256 wstethAnswer,,,) = wstethAggregator.latestRoundData();
assertEq(uint256(wstethAnswer), expectedEthPerWstethRate, "Incorrect rate");
}
}
2 changes: 1 addition & 1 deletion tests/tests/protocols/lido/WstethPriceFeed.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ abstract contract WstethPriceFeedTestBase is IntegrationTest {
_valueInterpreter: IValueInterpreter(getValueInterpreterAddressForVersion(version)),
_tokenAddress: ETHEREUM_STETH,
_skipIfRegistered: true,
_aggregatorAddress: ETHEREUM_STEH_ETH_AGGREGATOR,
_aggregatorAddress: ETHEREUM_STETH_ETH_AGGREGATOR,
_rateAsset: IChainlinkPriceFeedMixinProd.RateAsset.ETH
});
addDerivative({
Expand Down
2 changes: 1 addition & 1 deletion tests/utils/Constants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ abstract contract Constants {
address internal constant ETHEREUM_DAI_ETH_AGGREGATOR = 0x773616E4d11A78F511299002da57A0a94577F1f4;
address internal constant ETHEREUM_ETH_USD_AGGREGATOR = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419;
address internal constant ETHEREUM_MLN_ETH_AGGREGATOR = 0xDaeA8386611A157B08829ED4997A8A62B557014C;
address internal constant ETHEREUM_STEH_ETH_AGGREGATOR = 0x86392dC19c0b719886221c78AB11eb8Cf5c52812;
address internal constant ETHEREUM_STETH_ETH_AGGREGATOR = 0x86392dC19c0b719886221c78AB11eb8Cf5c52812;
address internal constant ETHEREUM_USDC_ETH_AGGREGATOR = 0x986b5E1e1755e3C2440e960477f25201B0a8bbD4;
address internal constant ETHEREUM_USDT_ETH_AGGREGATOR = 0xEe9F2375b4bdF6387aa8265dD4FB8F16512A1d46;
address internal constant ETHEREUM_WEETH_ETH_AGGREGATOR = 0x5c9C449BbC9a6075A2c061dF312a35fd1E05fF22;
Expand Down

0 comments on commit ae37251

Please sign in to comment.