From 85a5afe31737c99230ee351002577b9e6b3a9e12 Mon Sep 17 00:00:00 2001 From: Sean Casey Date: Wed, 29 Jan 2025 11:57:51 -0400 Subject: [PATCH] feat: smardex usdn native rate price feed (#1352) --- .../ISmarDexUsdnProtocol.sol | 18 +++++ .../SmarDexUsdnNativeRateUsdAggregator.sol | 71 +++++++++++++++++++ .../SmarDexUsdnNativeRateUsdAggregator.t.sol | 62 ++++++++++++++++ tests/utils/Constants.sol | 2 +- tests/utils/core/AssetUniverseUtils.sol | 11 +++ 5 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 contracts/external-interfaces/ISmarDexUsdnProtocol.sol create mode 100644 contracts/release/infrastructure/price-feeds/primitives/SmarDexUsdnNativeRateUsdAggregator.sol create mode 100644 tests/tests/protocols/smar-dex/SmarDexUsdnNativeRateUsdAggregator.t.sol diff --git a/contracts/external-interfaces/ISmarDexUsdnProtocol.sol b/contracts/external-interfaces/ISmarDexUsdnProtocol.sol new file mode 100644 index 000000000..c53249d2a --- /dev/null +++ b/contracts/external-interfaces/ISmarDexUsdnProtocol.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-3.0 + +/* + This file is part of the Enzyme Protocol. + + (c) Enzyme Foundation + + For the full license information, please view the LICENSE + file that was distributed with this source code. +*/ + +pragma solidity >=0.6.0 <0.9.0; + +/// @title ISmarDexUsdnProtocol Interface +/// @author Enzyme Foundation +interface ISmarDexUsdnProtocol { + function usdnPrice(uint128 _wstethInUsdRateWithWeiPrecision) external view returns (uint256 price_); +} diff --git a/contracts/release/infrastructure/price-feeds/primitives/SmarDexUsdnNativeRateUsdAggregator.sol b/contracts/release/infrastructure/price-feeds/primitives/SmarDexUsdnNativeRateUsdAggregator.sol new file mode 100644 index 000000000..9f8424dda --- /dev/null +++ b/contracts/release/infrastructure/price-feeds/primitives/SmarDexUsdnNativeRateUsdAggregator.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-3.0 + +/* + This file is part of the Enzyme Protocol. + + (c) Enzyme Foundation + + 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 {ISmarDexUsdnProtocol} from "../../../../external-interfaces/ISmarDexUsdnProtocol.sol"; +import {PriceFeedHelpersLib} from "../utils/PriceFeedHelpersLib.sol"; +import {RateAggregatorBase} from "./utils/RateAggregatorBase.sol"; +import {RateUsdAggregatorBase} from "./utils/RateUsdAggregatorBase.sol"; + +/// @title SmarDexUsdnNativeRateUsdAggregator Contract +/// @author Enzyme Foundation +/// @notice USD-quoted aggregator for SmarDex USDN using their native rate +contract SmarDexUsdnNativeRateUsdAggregator is RateUsdAggregatorBase { + /// @dev `USDN_RATES_PRECISION`: the precision used in USDN for both wsteth/usd rate input and USDN rate output + uint256 private constant USDN_RATES_PRECISION = 10 ** 18; + + // Immutables: deployer-input + /// @dev `USDN_PROTOCOL`: the main SmarDex USDN protocol contract + ISmarDexUsdnProtocol public immutable USDN_PROTOCOL; + /// @dev `WSTETH_IN_USD_AGGREGATOR_ADDRESS`: the wstETH/USD aggregator to use as input for the USDN rate function + address public immutable WSTETH_IN_USD_AGGREGATOR_ADDRESS; + // Immutables: derived + /// @dev `WSTETH_IN_USD_AGGREGATOR_PRECISION`: the precision of WSTETH_IN_USD_AGGREGATOR_ADDRESS + uint256 private immutable WSTETH_IN_USD_AGGREGATOR_PRECISION; + + constructor(address _usdnProtocolAddress, address _wstethInUsdAggregatorAddress) + RateUsdAggregatorBase(address(0), false) + { + USDN_PROTOCOL = ISmarDexUsdnProtocol(_usdnProtocolAddress); + WSTETH_IN_USD_AGGREGATOR_ADDRESS = _wstethInUsdAggregatorAddress; + + WSTETH_IN_USD_AGGREGATOR_PRECISION = + PriceFeedHelpersLib.parsePrecisionFromChainlinkAggregator(_wstethInUsdAggregatorAddress); + } + + //================================================================================================================== + // Required overrides: RateAggregatorBase + //================================================================================================================== + + /// @inheritdoc RateAggregatorBase + /// @dev Returns the value of 1 unit of USDN: + /// - quoted in USD + /// - with 18-decimals of precision + /// - with the wstETH/USD aggregator's timestamp + function baseRate() public view override returns (uint256 rate_, uint256 ratePrecision_, uint256 timestamp_) { + // Get the wstETH/USD rate + (uint256 wstethInUsdRateInAggregatorPrecision, uint256 wstethInUsdRateTimestamp) = + PriceFeedHelpersLib.parseRateFromChainlinkAggregator(WSTETH_IN_USD_AGGREGATOR_ADDRESS); + + // Convert wstETH/USD rate to USDN's expected precision + uint256 wstethInUsdRateWithWeiPrecision = PriceFeedHelpersLib.convertRatePrecision({ + _rate: wstethInUsdRateInAggregatorPrecision, + _fromPrecision: WSTETH_IN_USD_AGGREGATOR_PRECISION, + _toPrecision: USDN_RATES_PRECISION + }); + + rate_ = USDN_PROTOCOL.usdnPrice({_wstethInUsdRateWithWeiPrecision: uint128(wstethInUsdRateWithWeiPrecision)}); + ratePrecision_ = USDN_RATES_PRECISION; + timestamp_ = wstethInUsdRateTimestamp; + } +} diff --git a/tests/tests/protocols/smar-dex/SmarDexUsdnNativeRateUsdAggregator.t.sol b/tests/tests/protocols/smar-dex/SmarDexUsdnNativeRateUsdAggregator.t.sol new file mode 100644 index 000000000..85e859e42 --- /dev/null +++ b/tests/tests/protocols/smar-dex/SmarDexUsdnNativeRateUsdAggregator.t.sol @@ -0,0 +1,62 @@ +// 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"; + +address constant USDN_PROTOCOL_ADDRESS = 0x656cB8C6d154Aad29d8771384089be5B5141f01a; +// Enzyme's `ChainlinkLikeWstethPriceFeed` +address constant WSTETH_ETH_AGGREGATOR = 0x92829C41115311cA43D5c9f722f0E9e7b9fcd30a; + +contract Test is IntegrationTest { + IChainlinkAggregator usdnAggregator; + address wstethUsdAggregatorAddress; + + function setUp() public override { + vm.createSelectFork("mainnet", ETHEREUM_BLOCK_LATEST); + + // TODO: can replace with live address once it's deployed + // Deploy the wstETH/USD aggregator + wstethUsdAggregatorAddress = __deployWstethInUsdAggregator(); + + // Deploy the USDN aggregator + usdnAggregator = __deployUsdnAggregator(wstethUsdAggregatorAddress); + } + + // DEPLOYMENT HELPERS + + function __deployUsdnAggregator(address _wstethInUsdAggregatorAddress) + private + returns (IChainlinkAggregator usdnAggregator_) + { + bytes memory args = abi.encode(USDN_PROTOCOL_ADDRESS, _wstethInUsdAggregatorAddress); + + return IChainlinkAggregator(deployCode("SmarDexUsdnNativeRateUsdAggregator.sol", args)); + } + + function __deployWstethInUsdAggregator() private returns (address wstethAggregatorAddress_) { + // Convert wstETH from ETH to USD quote using ETH/USD + return deployCode( + "ConvertedQuoteAggregator.sol", + abi.encode(CHAINLINK_AGGREGATOR_DECIMALS_USD, ETHEREUM_ETH_USD_AGGREGATOR, false, WSTETH_ETH_AGGREGATOR) + ); + } + + // TESTS + + function test_decimals_success() public { + assertEq(usdnAggregator.decimals(), CHAINLINK_AGGREGATOR_DECIMALS_USD, "Incorrect decimals"); + } + + function test_latestRoundData_success() public { + (, uint256 wstethUsdTimestamp) = parseRateFromChainlinkAggregator(wstethUsdAggregatorAddress); + (uint256 usdnRate, uint256 usdnTimestamp) = parseRateFromChainlinkAggregator(address(usdnAggregator)); + + // Should be very close to 1, unless the rate starts to depeg + uint256 expectedRate = 1e8; // "1" in USD aggregator precision + uint256 percentTolerance = WEI_ONE_PERCENT; // 1% tolerance + assertApproxEqRel(usdnRate, expectedRate, percentTolerance, "Incorrect rate"); + // Timestamp should be that of the wsteth aggregator + assertEq(usdnTimestamp, wstethUsdTimestamp, "Incorrect timestamp"); + } +} diff --git a/tests/utils/Constants.sol b/tests/utils/Constants.sol index 20bf86528..6981c2dc2 100644 --- a/tests/utils/Constants.sol +++ b/tests/utils/Constants.sol @@ -32,7 +32,7 @@ abstract contract Constants { // expected exchange rates, etc. // `ETHEREUM_BLOCK_LATEST` can be increased as-needed, and should be used in all tests // that should generally continue to pass regardless of block. - uint256 internal constant ETHEREUM_BLOCK_LATEST = 21487350; // Dec 26th, 2024 + uint256 internal constant ETHEREUM_BLOCK_LATEST = 21710000; // Jan 26th, 2025 uint256 internal constant ETHEREUM_BLOCK_TIME_SENSITIVE = 20711624; // Sep 9th, 2024 uint256 internal constant ETHEREUM_BLOCK_TIME_SENSITIVE_ONE_INCH_V5 = 19518890; // March 26th, 2024 uint256 internal constant ETHEREUM_BLOCK_TIME_SENSITIVE_PENDLE = 20100000; // June 15th, 2024 diff --git a/tests/utils/core/AssetUniverseUtils.sol b/tests/utils/core/AssetUniverseUtils.sol index 14cdd01b9..3840849e8 100644 --- a/tests/utils/core/AssetUniverseUtils.sol +++ b/tests/utils/core/AssetUniverseUtils.sol @@ -28,6 +28,17 @@ abstract contract AssetUniverseUtils is CoreUtilsBase { return IChainlinkAggregator(deployCode("UsdEthSimulatedAggregator.sol", abi.encode(_ethUsdAggregatorAddress))); } + function parseRateFromChainlinkAggregator(address _aggregatorAddress) + internal + view + returns (uint256 rate_, uint256 timestamp_) + { + int256 answer; + (, answer,, timestamp_,) = IChainlinkAggregator(_aggregatorAddress).latestRoundData(); + + rate_ = uint256(answer); + } + // ASSET REGISTRATION function addDerivative(