-
Notifications
You must be signed in to change notification settings - Fork 163
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: chainlink-like wsteth price feed
- Loading branch information
1 parent
0f13246
commit ae37251
Showing
6 changed files
with
163 additions
and
3 deletions.
There are no files selected for viewing
71 changes: 71 additions & 0 deletions
71
contracts/release/infrastructure/price-feeds/primitives/ChainlinkLikeWstethPriceFeed.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
87 changes: 87 additions & 0 deletions
87
tests/tests/protocols/lido/ChainlinkLikeWstethPriceFeed.t.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters