Skip to content

Commit

Permalink
add wstETH price aggregator (#125)
Browse files Browse the repository at this point in the history
* add wstETH price aggregator

* deploy wsteth price on mainnet
  • Loading branch information
thorseldon authored Mar 12, 2024
1 parent 9c657a2 commit 9ce2699
Show file tree
Hide file tree
Showing 12 changed files with 461 additions and 0 deletions.
12 changes: 12 additions & 0 deletions contracts/interfaces/IStETH.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.8.4;

import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";

interface IStETH is IERC20Metadata {
function getPooledEthByShares(uint256 _sharesAmount) external view returns (uint256);

function getSharesByPooledEth(uint256 _pooledEthAmount) external view returns (uint256);

function submit(address _referral) external payable returns (uint256);
}
24 changes: 24 additions & 0 deletions contracts/interfaces/IWstETH.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.8.4;

import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";

/**
* @dev Interface for interacting with WstETH contract
* Note Not a comprehensive interface
*/
interface IWstETH is IERC20Metadata {
function stETH() external returns (address);

function wrap(uint256 _stETHAmount) external returns (uint256);

function unwrap(uint256 _wstETHAmount) external returns (uint256);

function getWstETHByStETH(uint256 _stETHAmount) external view returns (uint256);

function getStETHByWstETH(uint256 _wstETHAmount) external view returns (uint256);

function stEthPerToken() external view returns (uint256);

function tokensPerStEth() external view returns (uint256);
}
1 change: 1 addition & 0 deletions contracts/libraries/helpers/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ library Errors {
string public constant MATH_MULTIPLICATION_OVERFLOW = "200";
string public constant MATH_ADDITION_OVERFLOW = "201";
string public constant MATH_DIVISION_BY_ZERO = "202";
string public constant MATH_NUMBER_OVERFLOW = "203";

//validation & check errors
string public constant VL_INVALID_AMOUNT = "301"; // 'Amount must be greater than 0'
Expand Down
142 changes: 142 additions & 0 deletions contracts/misc/WstETHPriceAggregator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.8.4;

import {AggregatorInterface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorInterface.sol";
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import {AggregatorV2V3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV2V3Interface.sol";

import "../interfaces/IWstETH.sol";

import {Errors} from "../libraries/helpers/Errors.sol";

/**
* @title wstETH price aggregator
* @notice A custom price aggregator that calculates the price for wstETH / ETH
*/
contract WstETHPriceAggregator is AggregatorV2V3Interface {
/// @notice Version of the price feed
uint256 private constant _version = 1;

/// @notice Description of the price feed
string private constant _description = "wstETH / ETH";

/// @notice Chainlink stETH / ETH price feed
address public stETHtoETHPriceAggregator;

/// @notice Number of decimals for the stETH / ETH price feed
uint8 public stETHtoETHPriceAggregatorDecimals;

/// @notice WstETH contract address
address public wstETH;

/// @notice Scale for WstETH contract
int256 private _wstETHScale;

constructor(address stETHtoETHPriceAggregator_, address wstETH_) {
stETHtoETHPriceAggregator = stETHtoETHPriceAggregator_;
stETHtoETHPriceAggregatorDecimals = AggregatorV3Interface(stETHtoETHPriceAggregator_).decimals();
wstETH = wstETH_;

// Note: Safe to convert directly to an int256 because wstETH.decimals == 18
_wstETHScale = int256(10**IWstETH(wstETH).decimals());

require(stETHtoETHPriceAggregatorDecimals == 18, Errors.RC_INVALID_DECIMALS);
}

function signed256(uint256 n) internal pure returns (int256) {
require(n <= uint256(type(int256).max), Errors.MATH_NUMBER_OVERFLOW);
return int256(n);
}

// AggregatorInterface

function latestAnswer() external view override returns (int256) {
int256 stETHPrice = AggregatorInterface(stETHtoETHPriceAggregator).latestAnswer();
int256 scaledPrice = _convertStETHPrice(stETHPrice);
return scaledPrice;
}

function latestTimestamp() external view override returns (uint256) {
return AggregatorInterface(stETHtoETHPriceAggregator).latestTimestamp();
}

function latestRound() external view override returns (uint256) {
return AggregatorInterface(stETHtoETHPriceAggregator).latestRound();
}

function getAnswer(uint256 roundId) external view override returns (int256) {
int256 stETHPrice = AggregatorInterface(stETHtoETHPriceAggregator).getAnswer(roundId);
int256 scaledPrice = _convertStETHPrice(stETHPrice);
return scaledPrice;
}

function getTimestamp(uint256 roundId) external view override returns (uint256) {
return AggregatorInterface(stETHtoETHPriceAggregator).getTimestamp(roundId);
}

// AggregatorV3Interface

function decimals() external view override returns (uint8) {
return stETHtoETHPriceAggregatorDecimals;
}

function description() external pure override returns (string memory) {
return _description;
}

function version() external pure override returns (uint256) {
return _version;
}

function getRoundData(uint80 _roundId)
external
view
override
returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
)
{
(
uint80 roundId_,
int256 stETHPrice,
uint256 startedAt_,
uint256 updatedAt_,
uint80 answeredInRound_
) = AggregatorV3Interface(stETHtoETHPriceAggregator).getRoundData(_roundId);
int256 scaledPrice = _convertStETHPrice(stETHPrice);
return (roundId_, scaledPrice, startedAt_, updatedAt_, answeredInRound_);
}

function latestRoundData()
external
view
override
returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
)
{
(
uint80 roundId_,
int256 stETHPrice,
uint256 startedAt_,
uint256 updatedAt_,
uint80 answeredInRound_
) = AggregatorV3Interface(stETHtoETHPriceAggregator).latestRoundData();
int256 scaledPrice = _convertStETHPrice(stETHPrice);
return (roundId_, scaledPrice, startedAt_, updatedAt_, answeredInRound_);
}

function _convertStETHPrice(int256 stETHPrice) internal view returns (int256) {
uint256 tokensPerStEth = IWstETH(wstETH).tokensPerStEth();
int256 scaledPrice = (stETHPrice * _wstETHScale) / signed256(tokensPerStEth);
return scaledPrice;
}
}
46 changes: 46 additions & 0 deletions contracts/mock/MockStETH.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.8.4;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IStETH} from "../interfaces/IStETH.sol";

contract MockStETH is IStETH, ERC20 {
uint256 public constant RATIO_FACTOR = 10000;

uint256 internal _shareRatio;

constructor() ERC20("Liquid staked Ether 2.0", "stETH") {
// 100% = 1e4
_shareRatio = RATIO_FACTOR;
}

function setShareRatio(uint256 ratio_) public {
_shareRatio = ratio_;
}

function etShareRatio() public view returns (uint256) {
return _shareRatio;
}

function balanceOf(address _account) public view override(IERC20, ERC20) returns (uint256) {
return getPooledEthByShares(super.balanceOf(_account));
}

function getPooledEthByShares(uint256 _sharesAmount) public view override returns (uint256) {
return (_sharesAmount * _shareRatio) / RATIO_FACTOR;
}

function getSharesByPooledEth(uint256 _pooledEthAmount) public view override returns (uint256) {
return (_pooledEthAmount * RATIO_FACTOR) / _shareRatio;
}

function submit(
address /*_referral*/
) public payable override returns (uint256) {
require(msg.value != 0, "ZERO_DEPOSIT");
uint256 sharesAmount = getSharesByPooledEth(msg.value);
_mint(msg.sender, sharesAmount);
return sharesAmount;
}
}
110 changes: 110 additions & 0 deletions contracts/mock/WstETH.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// SPDX-License-Identifier: agpl-3.0
pragma solidity 0.8.4;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IStETH} from "../interfaces/IStETH.sol";

/**
* @title StETH token wrapper with static balances.
* @dev It's an ERC20 token that represents the account's share of the total
* supply of stETH tokens. WstETH token's balance only changes on transfers,
* unlike StETH that is also changed when oracles report staking rewards and
* penalties. It's a "power user" token for DeFi protocols which don't
* support rebasable tokens.
*
* The contract is also a trustless wrapper that accepts stETH tokens and mints
* wstETH in return. Then the user unwraps, the contract burns user's wstETH
* and sends user locked stETH in return.
*
* The contract provides the staking shortcut: user can send ETH with regular
* transfer and get wstETH in return. The contract will send ETH to Lido submit
* method, staking it and wrapping the received stETH.
*
*/
contract WstETH is ERC20 {
IStETH public stETH;

/**
* @param _stETH address of the StETH token to wrap
*/
constructor(IStETH _stETH) ERC20("Wrapped liquid staked Ether 2.0", "wstETH") {
stETH = _stETH;
}

/**
* @notice Exchanges stETH to wstETH
* @param _stETHAmount amount of stETH to wrap in exchange for wstETH
* @dev Requirements:
* - `_stETHAmount` must be non-zero
* - msg.sender must approve at least `_stETHAmount` stETH to this
* contract.
* - msg.sender must have at least `_stETHAmount` of stETH.
* User should first approve _stETHAmount to the WstETH contract
* @return Amount of wstETH user receives after wrap
*/
function wrap(uint256 _stETHAmount) external returns (uint256) {
require(_stETHAmount > 0, "wstETH: can't wrap zero stETH");
uint256 wstETHAmount = stETH.getSharesByPooledEth(_stETHAmount);
_mint(msg.sender, wstETHAmount);
stETH.transferFrom(msg.sender, address(this), _stETHAmount);
return wstETHAmount;
}

/**
* @notice Exchanges wstETH to stETH
* @param _wstETHAmount amount of wstETH to uwrap in exchange for stETH
* @dev Requirements:
* - `_wstETHAmount` must be non-zero
* - msg.sender must have at least `_wstETHAmount` wstETH.
* @return Amount of stETH user receives after unwrap
*/
function unwrap(uint256 _wstETHAmount) external returns (uint256) {
require(_wstETHAmount > 0, "wstETH: zero amount unwrap not allowed");
uint256 stETHAmount = stETH.getPooledEthByShares(_wstETHAmount);
_burn(msg.sender, _wstETHAmount);
stETH.transfer(msg.sender, stETHAmount);
return stETHAmount;
}

/**
* @notice Shortcut to stake ETH and auto-wrap returned stETH
*/
receive() external payable {
uint256 shares = stETH.submit{value: msg.value}(address(0));
_mint(msg.sender, shares);
}

/**
* @notice Get amount of wstETH for a given amount of stETH
* @param _stETHAmount amount of stETH
* @return Amount of wstETH for a given stETH amount
*/
function getWstETHByStETH(uint256 _stETHAmount) external view returns (uint256) {
return stETH.getSharesByPooledEth(_stETHAmount);
}

/**
* @notice Get amount of stETH for a given amount of wstETH
* @param _wstETHAmount amount of wstETH
* @return Amount of stETH for a given wstETH amount
*/
function getStETHByWstETH(uint256 _wstETHAmount) external view returns (uint256) {
return stETH.getPooledEthByShares(_wstETHAmount);
}

/**
* @notice Get amount of stETH for a one wstETH
* @return Amount of stETH for 1 wstETH
*/
function stEthPerToken() external view returns (uint256) {
return stETH.getPooledEthByShares(1 ether);
}

/**
* @notice Get amount of wstETH for a one stETH
* @return Amount of wstETH for a 1 stETH
*/
function tokensPerStEth() external view returns (uint256) {
return stETH.getSharesByPooledEth(1 ether);
}
}
8 changes: 8 additions & 0 deletions deployments/deployed-contracts-goerli.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,5 +168,13 @@
},
"UniswapV3DebtSwapAdapter": {
"address": "0x06a41eC387810a0dC9FE3042180D4617207C6E27"
},
"WstETHPriceAggregator": {
"address": "0xa7e842d5e85284c0da225FC75cc44d2E21736376",
"deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6"
},
"rateStrategyWSTETH240130": {
"address": "0xDfd5815010E599c4487cA808844A213A7160552f",
"deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6"
}
}
8 changes: 8 additions & 0 deletions deployments/deployed-contracts-main.json
Original file line number Diff line number Diff line change
Expand Up @@ -174,5 +174,13 @@
"rateStrategyUSDT240120": {
"address": "0x3C5cDf47fe2Eaf3e40d1a66148Cf4F72adf9446A",
"deployer": "0x868964fa49a6fd6e116FE82c8f4165904406f479"
},
"WstETHPriceAggregator": {
"address": "0xe73F027Aa66d8626076751b70688CBcBD708aFd9",
"deployer": "0x868964fa49a6fd6e116FE82c8f4165904406f479"
},
"rateStrategyWstETH240223": {
"address": "0x6F8A38Bf0331Cc691AB6dD8146aDeB5B846430B2",
"deployer": "0x868964fa49a6fd6e116FE82c8f4165904406f479"
}
}
6 changes: 6 additions & 0 deletions helpers/contracts-deployments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import {
MockerERC721Wrapper,
ChainlinkAggregatorHelperFactory,
UniswapV3DebtSwapAdapterFactory,
WstETHPriceAggregatorFactory,
} from "../types";
import {
withSaveAndVerify,
Expand Down Expand Up @@ -681,3 +682,8 @@ export const deployUniswapV3DebtSwapAdapter = async (verify?: boolean) => {
await rawInsertContractAddressInDb(eContractid.UniswapV3DebtSwapAdapterImpl, adapterImpl.address);
return withSaveAndVerify(adapterImpl, eContractid.UniswapV3DebtSwapAdapter, [], verify);
};

export const deployWstETHPriceAggregator = async (stETHAgg: string, wstETH: string, verify?: boolean) => {
const aggregator = await new WstETHPriceAggregatorFactory(await getDeploySigner()).deploy(stETHAgg, wstETH);
return withSaveAndVerify(aggregator, eContractid.WstETHPriceAggregator, [stETHAgg, wstETH], verify);
};
Loading

0 comments on commit 9ce2699

Please sign in to comment.