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:
@@ -5,6 +5,10 @@
|
|||||||
{
|
{
|
||||||
"note": "Splits BridgeAdapter up by chain",
|
"note": "Splits BridgeAdapter up by chain",
|
||||||
"pr": 487
|
"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
|
/// @dev Minimal interface for minting StETH
|
||||||
interface ILido {
|
interface IStETH {
|
||||||
/// @dev Adds eth to the pool
|
/// @dev Adds eth to the pool
|
||||||
/// @param _referral optional address for referrals
|
/// @param _referral optional address for referrals
|
||||||
/// @return StETH Amount of shares generated
|
/// @return StETH Amount of shares generated
|
||||||
@@ -37,6 +37,33 @@ interface ILido {
|
|||||||
function getPooledEthByShares(uint256 _sharesAmount) external view returns (uint256);
|
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 {
|
contract MixinLido {
|
||||||
using LibERC20TokenV06 for IERC20TokenV06;
|
using LibERC20TokenV06 for IERC20TokenV06;
|
||||||
@@ -59,12 +86,43 @@ contract MixinLido {
|
|||||||
internal
|
internal
|
||||||
returns (uint256 boughtAmount)
|
returns (uint256 boughtAmount)
|
||||||
{
|
{
|
||||||
(ILido lido) = abi.decode(bridgeData, (ILido));
|
if (address(sellToken) == address(WETH)) {
|
||||||
if (address(sellToken) == address(WETH) && address(buyToken) == address(lido)) {
|
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);
|
WETH.withdraw(sellAmount);
|
||||||
boughtAmount = lido.getPooledEthByShares(lido.submit{ value: sellAmount}(address(0)));
|
return stETH.getPooledEthByShares(stETH.submit{ value: sellAmount}(address(0)));
|
||||||
} else {
|
}
|
||||||
|
|
||||||
revert("MixinLido/UNSUPPORTED_TOKEN_PAIR");
|
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",
|
"version": "16.61.0",
|
||||||
"changes": [
|
"changes": [
|
||||||
|
{
|
||||||
|
"note": "Add stETH wrap/unwrap support",
|
||||||
|
"pr": 476
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"note": "Offboard/clean up Oasis, CoFix, and legacy Kyber",
|
"note": "Offboard/clean up Oasis, CoFix, and legacy Kyber",
|
||||||
"pr": 482
|
"pr": 482
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
<<<<<<< HEAD
|
<<<<<<< HEAD
|
||||||
=======
|
=======
|
||||||
isFinalUniswapV3FillData,
|
isFinalUniswapV3FillData,
|
||||||
|
LidoFillData,
|
||||||
LidoInfo,
|
LidoInfo,
|
||||||
LiquidityProviderFillData,
|
LiquidityProviderFillData,
|
||||||
LiquidityProviderRegistry,
|
LiquidityProviderRegistry,
|
||||||
@@ -428,6 +429,7 @@ export const MAINNET_TOKENS = {
|
|||||||
sEUR: '0xd71ecff9342a5ced620049e616c5035f1db98620',
|
sEUR: '0xd71ecff9342a5ced620049e616c5035f1db98620',
|
||||||
sETH: '0x5e74c9036fb86bd7ecdcb084a0673efc32ea31cb',
|
sETH: '0x5e74c9036fb86bd7ecdcb084a0673efc32ea31cb',
|
||||||
stETH: '0xae7ab96520de3a18e5e111b5eaab095312d7fe84',
|
stETH: '0xae7ab96520de3a18e5e111b5eaab095312d7fe84',
|
||||||
|
wstETH: '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0',
|
||||||
LINK: '0x514910771af9ca656af840dff83e8264ecf986ca',
|
LINK: '0x514910771af9ca656af840dff83e8264ecf986ca',
|
||||||
MANA: '0x0f5d2fb29fb7d3cfee444a200298f468908cc942',
|
MANA: '0x0f5d2fb29fb7d3cfee444a200298f468908cc942',
|
||||||
KNC: '0xdefa4e8a7bcba345f687a2f1456f5edd9ce97202',
|
KNC: '0xdefa4e8a7bcba345f687a2f1456f5edd9ce97202',
|
||||||
@@ -916,6 +918,10 @@ export const DEFAULT_TOKEN_ADJACENCY_GRAPH_BY_CHAIN_ID = valueByChainId<TokenAdj
|
|||||||
builder
|
builder
|
||||||
.add(MAINNET_TOKENS.OHMV2, MAINNET_TOKENS.BTRFLY)
|
.add(MAINNET_TOKENS.OHMV2, MAINNET_TOKENS.BTRFLY)
|
||||||
.add(MAINNET_TOKENS.BTRFLY, MAINNET_TOKENS.OHMV2);
|
.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
|
||||||
.build(),
|
.build(),
|
||||||
@@ -2112,11 +2118,13 @@ export const BEETHOVEN_X_VAULT_ADDRESS_BY_CHAIN = valueByChainId<string>(
|
|||||||
export const LIDO_INFO_BY_CHAIN = valueByChainId<LidoInfo>(
|
export const LIDO_INFO_BY_CHAIN = valueByChainId<LidoInfo>(
|
||||||
{
|
{
|
||||||
[ChainId.Mainnet]: {
|
[ChainId.Mainnet]: {
|
||||||
stEthToken: '0xae7ab96520de3a18e5e111b5eaab095312d7fe84',
|
stEthToken: MAINNET_TOKENS.stETH,
|
||||||
|
wstEthToken: MAINNET_TOKENS.wstETH,
|
||||||
wethToken: MAINNET_TOKENS.WETH,
|
wethToken: MAINNET_TOKENS.WETH,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
wstEthToken: NULL_ADDRESS,
|
||||||
stEthToken: NULL_ADDRESS,
|
stEthToken: NULL_ADDRESS,
|
||||||
wethToken: NULL_ADDRESS,
|
wethToken: NULL_ADDRESS,
|
||||||
},
|
},
|
||||||
@@ -2511,7 +2519,18 @@ export const DEFAULT_GAS_SCHEDULE: Required<FeeSchedule> = {
|
|||||||
|
|
||||||
return gas;
|
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) => {
|
[ERC20BridgeSource.AaveV2]: (fillData?: FillData) => {
|
||||||
const aaveFillData = fillData as AaveV2FillData;
|
const aaveFillData = fillData as AaveV2FillData;
|
||||||
// NOTE: The Aave deposit method is more expensive than the withdraw
|
// NOTE: The Aave deposit method is more expensive than the withdraw
|
||||||
|
|||||||
@@ -322,7 +322,7 @@ export function createBridgeDataForBridgeOrder(order: OptimizedMarketBridgeOrder
|
|||||||
break;
|
break;
|
||||||
case ERC20BridgeSource.Lido:
|
case ERC20BridgeSource.Lido:
|
||||||
const lidoFillData = (order as OptimizedMarketBridgeOrder<LidoFillData>).fillData;
|
const lidoFillData = (order as OptimizedMarketBridgeOrder<LidoFillData>).fillData;
|
||||||
bridgeData = encoder.encode([lidoFillData.stEthTokenAddress]);
|
bridgeData = encoder.encode([lidoFillData.stEthTokenAddress, lidoFillData.wstEthTokenAddress]);
|
||||||
break;
|
break;
|
||||||
case ERC20BridgeSource.AaveV2:
|
case ERC20BridgeSource.AaveV2:
|
||||||
const aaveFillData = (order as OptimizedMarketBridgeOrder<AaveV2FillData>).fillData;
|
const aaveFillData = (order as OptimizedMarketBridgeOrder<AaveV2FillData>).fillData;
|
||||||
@@ -507,7 +507,7 @@ export const BRIDGE_ENCODERS: {
|
|||||||
{ name: 'path', type: 'bytes' },
|
{ name: 'path', type: 'bytes' },
|
||||||
]),
|
]),
|
||||||
[ERC20BridgeSource.KyberDmm]: AbiEncoder.create('(address,address[],address[])'),
|
[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.AaveV2]: AbiEncoder.create('(address,address)'),
|
||||||
[ERC20BridgeSource.Compound]: AbiEncoder.create('(address)'),
|
[ERC20BridgeSource.Compound]: AbiEncoder.create('(address)'),
|
||||||
[ERC20BridgeSource.Geist]: AbiEncoder.create('(address,address)'),
|
[ERC20BridgeSource.Geist]: AbiEncoder.create('(address,address)'),
|
||||||
|
|||||||
@@ -1106,8 +1106,10 @@ export class SamplerOperations {
|
|||||||
return new SamplerContractOperation({
|
return new SamplerContractOperation({
|
||||||
source: ERC20BridgeSource.Lido,
|
source: ERC20BridgeSource.Lido,
|
||||||
fillData: {
|
fillData: {
|
||||||
|
makerToken,
|
||||||
takerToken,
|
takerToken,
|
||||||
stEthTokenAddress: lidoInfo.stEthToken,
|
stEthTokenAddress: lidoInfo.stEthToken,
|
||||||
|
wstEthTokenAddress: lidoInfo.wstEthToken,
|
||||||
},
|
},
|
||||||
contract: this._samplerContract,
|
contract: this._samplerContract,
|
||||||
function: this._samplerContract.sampleSellsFromLido,
|
function: this._samplerContract.sampleSellsFromLido,
|
||||||
@@ -1124,8 +1126,10 @@ export class SamplerOperations {
|
|||||||
return new SamplerContractOperation({
|
return new SamplerContractOperation({
|
||||||
source: ERC20BridgeSource.Lido,
|
source: ERC20BridgeSource.Lido,
|
||||||
fillData: {
|
fillData: {
|
||||||
|
makerToken,
|
||||||
takerToken,
|
takerToken,
|
||||||
stEthTokenAddress: lidoInfo.stEthToken,
|
stEthTokenAddress: lidoInfo.stEthToken,
|
||||||
|
wstEthTokenAddress: lidoInfo.wstEthToken,
|
||||||
},
|
},
|
||||||
contract: this._samplerContract,
|
contract: this._samplerContract,
|
||||||
function: this._samplerContract.sampleBuysFromLido,
|
function: this._samplerContract.sampleBuysFromLido,
|
||||||
@@ -1603,16 +1607,10 @@ export class SamplerOperations {
|
|||||||
].map(path => this.getUniswapV3SellQuotes(router, quoter, path, takerFillAmounts));
|
].map(path => this.getUniswapV3SellQuotes(router, quoter, path, takerFillAmounts));
|
||||||
}
|
}
|
||||||
case ERC20BridgeSource.Lido: {
|
case ERC20BridgeSource.Lido: {
|
||||||
const lidoInfo = LIDO_INFO_BY_CHAIN[this.chainId];
|
if (!this._isLidoSupported(takerToken, makerToken)) {
|
||||||
if (
|
|
||||||
lidoInfo.stEthToken === NULL_ADDRESS ||
|
|
||||||
lidoInfo.wethToken === NULL_ADDRESS ||
|
|
||||||
takerToken.toLowerCase() !== lidoInfo.wethToken.toLowerCase() ||
|
|
||||||
makerToken.toLowerCase() !== lidoInfo.stEthToken.toLowerCase()
|
|
||||||
) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
const lidoInfo = LIDO_INFO_BY_CHAIN[this.chainId];
|
||||||
return this.getLidoSellQuotes(lidoInfo, makerToken, takerToken, takerFillAmounts);
|
return this.getLidoSellQuotes(lidoInfo, makerToken, takerToken, takerFillAmounts);
|
||||||
}
|
}
|
||||||
case ERC20BridgeSource.AaveV2: {
|
case ERC20BridgeSource.AaveV2: {
|
||||||
@@ -1685,6 +1683,24 @@ export class SamplerOperations {
|
|||||||
return allOps;
|
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(
|
private _getBuyQuoteOperations(
|
||||||
sources: ERC20BridgeSource[],
|
sources: ERC20BridgeSource[],
|
||||||
makerToken: string,
|
makerToken: string,
|
||||||
@@ -1924,17 +1940,10 @@ export class SamplerOperations {
|
|||||||
].map(path => this.getUniswapV3BuyQuotes(router, quoter, path, makerFillAmounts));
|
].map(path => this.getUniswapV3BuyQuotes(router, quoter, path, makerFillAmounts));
|
||||||
}
|
}
|
||||||
case ERC20BridgeSource.Lido: {
|
case ERC20BridgeSource.Lido: {
|
||||||
const lidoInfo = LIDO_INFO_BY_CHAIN[this.chainId];
|
if (!this._isLidoSupported(takerToken, makerToken)) {
|
||||||
|
|
||||||
if (
|
|
||||||
lidoInfo.stEthToken === NULL_ADDRESS ||
|
|
||||||
lidoInfo.wethToken === NULL_ADDRESS ||
|
|
||||||
takerToken.toLowerCase() !== lidoInfo.wethToken.toLowerCase() ||
|
|
||||||
makerToken.toLowerCase() !== lidoInfo.stEthToken.toLowerCase()
|
|
||||||
) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
const lidoInfo = LIDO_INFO_BY_CHAIN[this.chainId];
|
||||||
return this.getLidoBuyQuotes(lidoInfo, makerToken, takerToken, makerFillAmounts);
|
return this.getLidoBuyQuotes(lidoInfo, makerToken, takerToken, makerFillAmounts);
|
||||||
}
|
}
|
||||||
case ERC20BridgeSource.AaveV2: {
|
case ERC20BridgeSource.AaveV2: {
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ export interface PsmInfo {
|
|||||||
export interface LidoInfo {
|
export interface LidoInfo {
|
||||||
stEthToken: string;
|
stEthToken: string;
|
||||||
wethToken: string;
|
wethToken: string;
|
||||||
|
wstEthToken: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -312,8 +313,16 @@ export interface UniswapV3FillData extends BridgeFillData {
|
|||||||
encodedPath: Bytes;
|
encodedPath: Bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
export interface LiquidityProviderFillData extends BridgeFillData {
|
export interface LiquidityProviderFillData extends BridgeFillData {
|
||||||
poolAddress: Address;
|
poolAddress: Address;
|
||||||
|
=======
|
||||||
|
export interface LidoFillData extends FillData {
|
||||||
|
stEthTokenAddress: string;
|
||||||
|
wstEthTokenAddress: string;
|
||||||
|
takerToken: string;
|
||||||
|
makerToken: string;
|
||||||
|
>>>>>>> db76da58d (Add stETH wrap/unwrap support [TKR-377] (#476))
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CurveFillData extends BridgeFillData {
|
export interface CurveFillData extends BridgeFillData {
|
||||||
@@ -370,7 +379,7 @@ export interface Fill {
|
|||||||
input: BigNumber;
|
input: BigNumber;
|
||||||
// Output fill amount (maker asset amount in a sell, taker asset amount in a buy).
|
// Output fill amount (maker asset amount in a sell, taker asset amount in a buy).
|
||||||
output: BigNumber;
|
output: BigNumber;
|
||||||
// The output fill amount, ajdusted by fees.
|
// The output fill amount, adjusted by fees.
|
||||||
adjustedOutput: BigNumber;
|
adjustedOutput: BigNumber;
|
||||||
// Fill that must precede this one. This enforces certain fills to be contiguous.
|
// Fill that must precede this one. This enforces certain fills to be contiguous.
|
||||||
parent?: Fill;
|
parent?: Fill;
|
||||||
|
|||||||
Reference in New Issue
Block a user