Add stETH wrap/unwrap support [TKR-377] (#476)
* Update MixinLido to support stETH wrapping/unwrapping * Update LidoSampler and asset-swapper * Re-use token address constants in LIDO_INFO_BY_CHAIN * Update CHANGELOG.json * Add stETH <-> wstETH to TokenAdjacencyGraph * Change lido gas schedule code style * Move allowance approval inside the wrap branch * Refactor LidoSampler to reduce its bytecode size
This commit is contained in:
parent
cf8fc0ff8e
commit
db76da58d7
@ -5,6 +5,10 @@
|
||||
{
|
||||
"note": "Splits BridgeAdapter up by chain",
|
||||
"pr": 487
|
||||
},
|
||||
{
|
||||
"note": "Add stETH wrap/unwrap support",
|
||||
"pr": 476
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -26,7 +26,7 @@ import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol";
|
||||
|
||||
|
||||
/// @dev Minimal interface for minting StETH
|
||||
interface ILido {
|
||||
interface IStETH {
|
||||
/// @dev Adds eth to the pool
|
||||
/// @param _referral optional address for referrals
|
||||
/// @return StETH Amount of shares generated
|
||||
@ -37,6 +37,33 @@ interface ILido {
|
||||
function getPooledEthByShares(uint256 _sharesAmount) external view returns (uint256);
|
||||
}
|
||||
|
||||
/// @dev Minimal interface for wrapping/unwrapping stETH.
|
||||
interface IWstETH {
|
||||
|
||||
/**
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
|
||||
|
||||
contract MixinLido {
|
||||
using LibERC20TokenV06 for IERC20TokenV06;
|
||||
@ -59,12 +86,43 @@ contract MixinLido {
|
||||
internal
|
||||
returns (uint256 boughtAmount)
|
||||
{
|
||||
(ILido lido) = abi.decode(bridgeData, (ILido));
|
||||
if (address(sellToken) == address(WETH) && address(buyToken) == address(lido)) {
|
||||
if (address(sellToken) == address(WETH)) {
|
||||
return _tradeStETH(buyToken, sellAmount, bridgeData);
|
||||
}
|
||||
|
||||
return _tradeWstETH(sellToken, buyToken, sellAmount, bridgeData);
|
||||
}
|
||||
|
||||
function _tradeStETH(
|
||||
IERC20TokenV06 buyToken,
|
||||
uint256 sellAmount,
|
||||
bytes memory bridgeData
|
||||
) private returns (uint256 boughtAmount) {
|
||||
(IStETH stETH) = abi.decode(bridgeData, (IStETH));
|
||||
if (address(buyToken) == address(stETH)) {
|
||||
WETH.withdraw(sellAmount);
|
||||
boughtAmount = lido.getPooledEthByShares(lido.submit{ value: sellAmount}(address(0)));
|
||||
} else {
|
||||
revert("MixinLido/UNSUPPORTED_TOKEN_PAIR");
|
||||
return stETH.getPooledEthByShares(stETH.submit{ value: sellAmount}(address(0)));
|
||||
}
|
||||
|
||||
revert("MixinLido/UNSUPPORTED_TOKEN_PAIR");
|
||||
}
|
||||
|
||||
function _tradeWstETH(
|
||||
IERC20TokenV06 sellToken,
|
||||
IERC20TokenV06 buyToken,
|
||||
uint256 sellAmount,
|
||||
bytes memory bridgeData
|
||||
|
||||
) private returns(uint256 boughtAmount){
|
||||
(IEtherTokenV06 stETH, IWstETH wstETH) = abi.decode(bridgeData, (IEtherTokenV06, IWstETH));
|
||||
if (address(sellToken) == address(stETH) && address(buyToken) == address(wstETH) ) {
|
||||
sellToken.approveIfBelow(address(wstETH), sellAmount);
|
||||
return wstETH.wrap(sellAmount);
|
||||
}
|
||||
if (address(sellToken) == address(wstETH) && address(buyToken) == address(stETH) ) {
|
||||
return wstETH.unwrap(sellAmount);
|
||||
}
|
||||
|
||||
revert("MixinLido/UNSUPPORTED_TOKEN_PAIR");
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,10 @@
|
||||
{
|
||||
"version": "16.61.0",
|
||||
"changes": [
|
||||
{
|
||||
"note": "Add stETH wrap/unwrap support",
|
||||
"pr": 476
|
||||
},
|
||||
{
|
||||
"note": "Offboard/clean up Oasis, CoFix, and legacy Kyber",
|
||||
"pr": 482
|
||||
|
@ -22,10 +22,18 @@ pragma experimental ABIEncoderV2;
|
||||
|
||||
import "./SamplerUtils.sol";
|
||||
|
||||
|
||||
interface IWstETH {
|
||||
function getWstETHByStETH(uint256 _stETHAmount) external view returns (uint256);
|
||||
function getStETHByWstETH(uint256 _wstETHAmount) external view returns (uint256);
|
||||
}
|
||||
|
||||
|
||||
contract LidoSampler is SamplerUtils {
|
||||
struct LidoInfo {
|
||||
address stEthToken;
|
||||
address wethToken;
|
||||
address wstEthToken;
|
||||
}
|
||||
|
||||
/// @dev Sample sell quotes from Lido
|
||||
@ -42,20 +50,17 @@ contract LidoSampler is SamplerUtils {
|
||||
uint256[] memory takerTokenAmounts
|
||||
)
|
||||
public
|
||||
pure
|
||||
view
|
||||
returns (uint256[] memory)
|
||||
{
|
||||
_assertValidPair(makerToken, takerToken);
|
||||
|
||||
if (takerToken != lidoInfo.wethToken || makerToken != address(lidoInfo.stEthToken)) {
|
||||
// Return 0 values if not selling WETH for stETH
|
||||
uint256 numSamples = takerTokenAmounts.length;
|
||||
uint256[] memory makerTokenAmounts = new uint256[](numSamples);
|
||||
return makerTokenAmounts;
|
||||
if (takerToken == lidoInfo.wethToken && makerToken == address(lidoInfo.stEthToken)) {
|
||||
// Minting stETH is always 1:1 therefore we can just return the same amounts back.
|
||||
return takerTokenAmounts;
|
||||
}
|
||||
|
||||
// Minting stETH is always 1:1 therefore we can just return the same amounts back
|
||||
return takerTokenAmounts;
|
||||
return _sampleSellsForWrapped(lidoInfo, takerToken, makerToken, takerTokenAmounts);
|
||||
}
|
||||
|
||||
/// @dev Sample buy quotes from Lido.
|
||||
@ -72,20 +77,43 @@ contract LidoSampler is SamplerUtils {
|
||||
uint256[] memory makerTokenAmounts
|
||||
)
|
||||
public
|
||||
pure
|
||||
view
|
||||
returns (uint256[] memory)
|
||||
{
|
||||
_assertValidPair(makerToken, takerToken);
|
||||
|
||||
if (takerToken != lidoInfo.wethToken || makerToken != address(lidoInfo.stEthToken)) {
|
||||
// Return 0 values if not buying stETH for WETH
|
||||
uint256 numSamples = makerTokenAmounts.length;
|
||||
uint256[] memory takerTokenAmounts = new uint256[](numSamples);
|
||||
return takerTokenAmounts;
|
||||
if (takerToken == lidoInfo.wethToken && makerToken == address(lidoInfo.stEthToken)) {
|
||||
// Minting stETH is always 1:1 therefore we can just return the same amounts back.
|
||||
return makerTokenAmounts;
|
||||
}
|
||||
|
||||
// Minting stETH is always 1:1 therefore we can just return the same amounts back
|
||||
return makerTokenAmounts;
|
||||
// Swap out `makerToken` and `takerToken` and re-use `_sampleSellsForWrapped`.
|
||||
return _sampleSellsForWrapped(lidoInfo, makerToken, takerToken, makerTokenAmounts);
|
||||
}
|
||||
|
||||
function _sampleSellsForWrapped(
|
||||
LidoInfo memory lidoInfo,
|
||||
address takerToken,
|
||||
address makerToken,
|
||||
uint256[] memory takerTokenAmounts
|
||||
) private view returns (uint256[] memory) {
|
||||
IWstETH wstETH = IWstETH(lidoInfo.wstEthToken);
|
||||
uint256 numSamples = takerTokenAmounts.length;
|
||||
uint256[] memory makerTokenAmounts = new uint256[](numSamples);
|
||||
|
||||
if (takerToken == lidoInfo.stEthToken && makerToken == lidoInfo.wstEthToken) {
|
||||
for (uint256 i = 0; i < numSamples; i++) {
|
||||
makerTokenAmounts[i] = wstETH.getWstETHByStETH(takerTokenAmounts[i]);
|
||||
}
|
||||
return makerTokenAmounts;
|
||||
}
|
||||
|
||||
if (takerToken == lidoInfo.wstEthToken && makerToken == lidoInfo.stEthToken) {
|
||||
for (uint256 i = 0; i < numSamples; i++) {
|
||||
makerTokenAmounts[i] = wstETH.getStETHByWstETH(takerTokenAmounts[i]);
|
||||
}
|
||||
return makerTokenAmounts;
|
||||
}
|
||||
|
||||
// Returns 0 values.
|
||||
return makerTokenAmounts;
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
GeistFillData,
|
||||
GetMarketOrdersOpts,
|
||||
isFinalUniswapV3FillData,
|
||||
LidoFillData,
|
||||
LidoInfo,
|
||||
LiquidityProviderFillData,
|
||||
LiquidityProviderRegistry,
|
||||
@ -448,6 +449,7 @@ export const MAINNET_TOKENS = {
|
||||
sEUR: '0xd71ecff9342a5ced620049e616c5035f1db98620',
|
||||
sETH: '0x5e74c9036fb86bd7ecdcb084a0673efc32ea31cb',
|
||||
stETH: '0xae7ab96520de3a18e5e111b5eaab095312d7fe84',
|
||||
wstETH: '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0',
|
||||
LINK: '0x514910771af9ca656af840dff83e8264ecf986ca',
|
||||
MANA: '0x0f5d2fb29fb7d3cfee444a200298f468908cc942',
|
||||
KNC: '0xdefa4e8a7bcba345f687a2f1456f5edd9ce97202',
|
||||
@ -931,6 +933,10 @@ export const DEFAULT_TOKEN_ADJACENCY_GRAPH_BY_CHAIN_ID = valueByChainId<TokenAdj
|
||||
builder
|
||||
.add(MAINNET_TOKENS.OHMV2, MAINNET_TOKENS.BTRFLY)
|
||||
.add(MAINNET_TOKENS.BTRFLY, MAINNET_TOKENS.OHMV2);
|
||||
// Lido
|
||||
builder
|
||||
.add(MAINNET_TOKENS.stETH, MAINNET_TOKENS.wstETH)
|
||||
.add(MAINNET_TOKENS.wstETH, MAINNET_TOKENS.stETH);
|
||||
})
|
||||
// Build
|
||||
.build(),
|
||||
@ -2125,11 +2131,13 @@ export const BEETHOVEN_X_VAULT_ADDRESS_BY_CHAIN = valueByChainId<string>(
|
||||
export const LIDO_INFO_BY_CHAIN = valueByChainId<LidoInfo>(
|
||||
{
|
||||
[ChainId.Mainnet]: {
|
||||
stEthToken: '0xae7ab96520de3a18e5e111b5eaab095312d7fe84',
|
||||
stEthToken: MAINNET_TOKENS.stETH,
|
||||
wstEthToken: MAINNET_TOKENS.wstETH,
|
||||
wethToken: MAINNET_TOKENS.WETH,
|
||||
},
|
||||
},
|
||||
{
|
||||
wstEthToken: NULL_ADDRESS,
|
||||
stEthToken: NULL_ADDRESS,
|
||||
wethToken: NULL_ADDRESS,
|
||||
},
|
||||
@ -2524,7 +2532,18 @@ export const DEFAULT_GAS_SCHEDULE: Required<FeeSchedule> = {
|
||||
|
||||
return gas;
|
||||
},
|
||||
[ERC20BridgeSource.Lido]: () => 226e3,
|
||||
[ERC20BridgeSource.Lido]: (fillData?: FillData) => {
|
||||
const lidoFillData = fillData as LidoFillData;
|
||||
const wethAddress = NATIVE_FEE_TOKEN_BY_CHAIN_ID[ChainId.Mainnet];
|
||||
// WETH -> stETH
|
||||
if (lidoFillData.takerToken === wethAddress) {
|
||||
return 226e3;
|
||||
} else if (lidoFillData.takerToken === lidoFillData.stEthTokenAddress) {
|
||||
return 120e3;
|
||||
} else {
|
||||
return 95e3;
|
||||
}
|
||||
},
|
||||
[ERC20BridgeSource.AaveV2]: (fillData?: FillData) => {
|
||||
const aaveFillData = fillData as AaveV2FillData;
|
||||
// NOTE: The Aave deposit method is more expensive than the withdraw
|
||||
|
@ -351,7 +351,7 @@ export function createBridgeDataForBridgeOrder(order: OptimizedMarketBridgeOrder
|
||||
break;
|
||||
case ERC20BridgeSource.Lido:
|
||||
const lidoFillData = (order as OptimizedMarketBridgeOrder<LidoFillData>).fillData;
|
||||
bridgeData = encoder.encode([lidoFillData.stEthTokenAddress]);
|
||||
bridgeData = encoder.encode([lidoFillData.stEthTokenAddress, lidoFillData.wstEthTokenAddress]);
|
||||
break;
|
||||
case ERC20BridgeSource.AaveV2:
|
||||
const aaveFillData = (order as OptimizedMarketBridgeOrder<AaveV2FillData>).fillData;
|
||||
@ -568,7 +568,7 @@ export const BRIDGE_ENCODERS: {
|
||||
{ name: 'path', type: 'bytes' },
|
||||
]),
|
||||
[ERC20BridgeSource.KyberDmm]: AbiEncoder.create('(address,address[],address[])'),
|
||||
[ERC20BridgeSource.Lido]: AbiEncoder.create('(address)'),
|
||||
[ERC20BridgeSource.Lido]: AbiEncoder.create('(address,address)'),
|
||||
[ERC20BridgeSource.AaveV2]: AbiEncoder.create('(address,address)'),
|
||||
[ERC20BridgeSource.Compound]: AbiEncoder.create('(address)'),
|
||||
[ERC20BridgeSource.Geist]: AbiEncoder.create('(address,address)'),
|
||||
|
@ -1106,8 +1106,10 @@ export class SamplerOperations {
|
||||
return new SamplerContractOperation({
|
||||
source: ERC20BridgeSource.Lido,
|
||||
fillData: {
|
||||
makerToken,
|
||||
takerToken,
|
||||
stEthTokenAddress: lidoInfo.stEthToken,
|
||||
wstEthTokenAddress: lidoInfo.wstEthToken,
|
||||
},
|
||||
contract: this._samplerContract,
|
||||
function: this._samplerContract.sampleSellsFromLido,
|
||||
@ -1124,8 +1126,10 @@ export class SamplerOperations {
|
||||
return new SamplerContractOperation({
|
||||
source: ERC20BridgeSource.Lido,
|
||||
fillData: {
|
||||
makerToken,
|
||||
takerToken,
|
||||
stEthTokenAddress: lidoInfo.stEthToken,
|
||||
wstEthTokenAddress: lidoInfo.wstEthToken,
|
||||
},
|
||||
contract: this._samplerContract,
|
||||
function: this._samplerContract.sampleBuysFromLido,
|
||||
@ -1603,16 +1607,10 @@ export class SamplerOperations {
|
||||
].map(path => this.getUniswapV3SellQuotes(router, quoter, path, takerFillAmounts));
|
||||
}
|
||||
case ERC20BridgeSource.Lido: {
|
||||
const lidoInfo = LIDO_INFO_BY_CHAIN[this.chainId];
|
||||
if (
|
||||
lidoInfo.stEthToken === NULL_ADDRESS ||
|
||||
lidoInfo.wethToken === NULL_ADDRESS ||
|
||||
takerToken.toLowerCase() !== lidoInfo.wethToken.toLowerCase() ||
|
||||
makerToken.toLowerCase() !== lidoInfo.stEthToken.toLowerCase()
|
||||
) {
|
||||
if (!this._isLidoSupported(takerToken, makerToken)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const lidoInfo = LIDO_INFO_BY_CHAIN[this.chainId];
|
||||
return this.getLidoSellQuotes(lidoInfo, makerToken, takerToken, takerFillAmounts);
|
||||
}
|
||||
case ERC20BridgeSource.AaveV2: {
|
||||
@ -1685,6 +1683,24 @@ export class SamplerOperations {
|
||||
return allOps;
|
||||
}
|
||||
|
||||
private _isLidoSupported(takerTokenAddress: string, makerTokenAddress: string): boolean {
|
||||
const lidoInfo = LIDO_INFO_BY_CHAIN[this.chainId];
|
||||
if (lidoInfo.wethToken === NULL_ADDRESS) {
|
||||
return false;
|
||||
}
|
||||
const takerToken = takerTokenAddress.toLowerCase();
|
||||
const makerToken = makerTokenAddress.toLowerCase();
|
||||
const wethToken = lidoInfo.wethToken.toLowerCase();
|
||||
const stEthToken = lidoInfo.stEthToken.toLowerCase();
|
||||
const wstEthToken = lidoInfo.wstEthToken.toLowerCase();
|
||||
|
||||
if (takerToken === wethToken && makerToken === stEthToken) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return _.difference([stEthToken, wstEthToken], [takerToken, makerToken]).length === 0;
|
||||
}
|
||||
|
||||
private _getBuyQuoteOperations(
|
||||
sources: ERC20BridgeSource[],
|
||||
makerToken: string,
|
||||
@ -1924,17 +1940,10 @@ export class SamplerOperations {
|
||||
].map(path => this.getUniswapV3BuyQuotes(router, quoter, path, makerFillAmounts));
|
||||
}
|
||||
case ERC20BridgeSource.Lido: {
|
||||
const lidoInfo = LIDO_INFO_BY_CHAIN[this.chainId];
|
||||
|
||||
if (
|
||||
lidoInfo.stEthToken === NULL_ADDRESS ||
|
||||
lidoInfo.wethToken === NULL_ADDRESS ||
|
||||
takerToken.toLowerCase() !== lidoInfo.wethToken.toLowerCase() ||
|
||||
makerToken.toLowerCase() !== lidoInfo.stEthToken.toLowerCase()
|
||||
) {
|
||||
if (!this._isLidoSupported(takerToken, makerToken)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const lidoInfo = LIDO_INFO_BY_CHAIN[this.chainId];
|
||||
return this.getLidoBuyQuotes(lidoInfo, makerToken, takerToken, makerFillAmounts);
|
||||
}
|
||||
case ERC20BridgeSource.AaveV2: {
|
||||
|
@ -167,6 +167,7 @@ export interface PsmInfo {
|
||||
export interface LidoInfo {
|
||||
stEthToken: string;
|
||||
wethToken: string;
|
||||
wstEthToken: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -325,7 +326,9 @@ export interface FinalUniswapV3FillData extends Omit<UniswapV3FillData, 'pathAmo
|
||||
|
||||
export interface LidoFillData extends FillData {
|
||||
stEthTokenAddress: string;
|
||||
wstEthTokenAddress: string;
|
||||
takerToken: string;
|
||||
makerToken: string;
|
||||
}
|
||||
|
||||
export interface AaveV2FillData extends FillData {
|
||||
@ -385,7 +388,7 @@ export interface Fill<TFillData extends FillData = FillData> {
|
||||
input: BigNumber;
|
||||
// Output fill amount (maker asset amount in a sell, taker asset amount in a buy).
|
||||
output: BigNumber;
|
||||
// The output fill amount, ajdusted by fees.
|
||||
// The output fill amount, adjusted by fees.
|
||||
adjustedOutput: BigNumber;
|
||||
// Fill that must precede this one. This enforces certain fills to be contiguous.
|
||||
parent?: Fill;
|
||||
|
Loading…
x
Reference in New Issue
Block a user