-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add wstETH price aggregator * deploy wsteth price on mainnet
- Loading branch information
1 parent
9c657a2
commit 9ce2699
Showing
12 changed files
with
461 additions
and
0 deletions.
There are no files selected for viewing
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,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); | ||
} |
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,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); | ||
} |
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
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; | ||
} | ||
} |
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,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; | ||
} | ||
} |
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,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); | ||
} | ||
} |
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
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
Oops, something went wrong.