From d3396fff3c99a40f885c0a96221e612a6338d251 Mon Sep 17 00:00:00 2001 From: Kyu Date: Tue, 31 May 2022 09:58:44 -0700 Subject: [PATCH] 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 --- contracts/zero-ex/CHANGELOG.json | 4 ++ .../transformers/bridges/mixins/MixinLido.sol | 70 +++++++++++++++++-- packages/asset-swapper/CHANGELOG.json | 4 ++ .../utils/market_operation_utils/constants.ts | 23 +++++- .../utils/market_operation_utils/orders.ts | 4 +- .../sampler_operations.ts | 43 +++++++----- .../src/utils/market_operation_utils/types.ts | 11 ++- 7 files changed, 131 insertions(+), 28 deletions(-) diff --git a/contracts/zero-ex/CHANGELOG.json b/contracts/zero-ex/CHANGELOG.json index 5f6b295c92..9a3c22e253 100644 --- a/contracts/zero-ex/CHANGELOG.json +++ b/contracts/zero-ex/CHANGELOG.json @@ -5,6 +5,10 @@ { "note": "Splits BridgeAdapter up by chain", "pr": 487 + }, + { + "note": "Add stETH wrap/unwrap support", + "pr": 476 } ] }, diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinLido.sol b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinLido.sol index b279089881..4b31e44518 100644 --- a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinLido.sol +++ b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinLido.sol @@ -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"); } } diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index a884063ddb..2afaeecc0f 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -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 diff --git a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts index 8bcfd29452..e69aef08ad 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts @@ -22,6 +22,7 @@ import { <<<<<<< HEAD ======= isFinalUniswapV3FillData, + LidoFillData, LidoInfo, LiquidityProviderFillData, LiquidityProviderRegistry, @@ -428,6 +429,7 @@ export const MAINNET_TOKENS = { sEUR: '0xd71ecff9342a5ced620049e616c5035f1db98620', sETH: '0x5e74c9036fb86bd7ecdcb084a0673efc32ea31cb', stETH: '0xae7ab96520de3a18e5e111b5eaab095312d7fe84', + wstETH: '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0', LINK: '0x514910771af9ca656af840dff83e8264ecf986ca', MANA: '0x0f5d2fb29fb7d3cfee444a200298f468908cc942', KNC: '0xdefa4e8a7bcba345f687a2f1456f5edd9ce97202', @@ -916,6 +918,10 @@ export const DEFAULT_TOKEN_ADJACENCY_GRAPH_BY_CHAIN_ID = valueByChainId( export const LIDO_INFO_BY_CHAIN = valueByChainId( { [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, }, @@ -2511,7 +2519,18 @@ export const DEFAULT_GAS_SCHEDULE: Required = { 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 diff --git a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts index 1e109916ba..e0719ef8b7 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts @@ -322,7 +322,7 @@ export function createBridgeDataForBridgeOrder(order: OptimizedMarketBridgeOrder break; case ERC20BridgeSource.Lido: const lidoFillData = (order as OptimizedMarketBridgeOrder).fillData; - bridgeData = encoder.encode([lidoFillData.stEthTokenAddress]); + bridgeData = encoder.encode([lidoFillData.stEthTokenAddress, lidoFillData.wstEthTokenAddress]); break; case ERC20BridgeSource.AaveV2: const aaveFillData = (order as OptimizedMarketBridgeOrder).fillData; @@ -507,7 +507,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)'), diff --git a/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts b/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts index 243f994b94..d5c9edb0a8 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts @@ -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: { diff --git a/packages/asset-swapper/src/utils/market_operation_utils/types.ts b/packages/asset-swapper/src/utils/market_operation_utils/types.ts index 6b7357a700..5d59dca8c1 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/types.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/types.ts @@ -174,6 +174,7 @@ export interface PsmInfo { export interface LidoInfo { stEthToken: string; wethToken: string; + wstEthToken: string; } /** @@ -312,8 +313,16 @@ export interface UniswapV3FillData extends BridgeFillData { encodedPath: Bytes; } +<<<<<<< HEAD export interface LiquidityProviderFillData extends BridgeFillData { 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 { @@ -370,7 +379,7 @@ export interface Fill { 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;