Skip to content

Commit

Permalink
Add stETH wrap/unwrap support [TKR-377] (#476)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
kyu-c authored and Noah Khamliche committed Jun 15, 2022
1 parent 68bc397 commit d3396ff
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 28 deletions.
4 changes: 4 additions & 0 deletions contracts/zero-ex/CHANGELOG.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
{
"note": "Splits BridgeAdapter up by chain",
"pr": 487
},
{
"note": "Add stETH wrap/unwrap support",
"pr": 476
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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");
}
}
4 changes: 4 additions & 0 deletions packages/asset-swapper/CHANGELOG.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
<<<<<<< HEAD
=======
isFinalUniswapV3FillData,
LidoFillData,
LidoInfo,
LiquidityProviderFillData,
LiquidityProviderRegistry,
Expand Down Expand Up @@ -428,6 +429,7 @@ export const MAINNET_TOKENS = {
sEUR: '0xd71ecff9342a5ced620049e616c5035f1db98620',
sETH: '0x5e74c9036fb86bd7ecdcb084a0673efc32ea31cb',
stETH: '0xae7ab96520de3a18e5e111b5eaab095312d7fe84',
wstETH: '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0',
LINK: '0x514910771af9ca656af840dff83e8264ecf986ca',
MANA: '0x0f5d2fb29fb7d3cfee444a200298f468908cc942',
KNC: '0xdefa4e8a7bcba345f687a2f1456f5edd9ce97202',
Expand Down Expand Up @@ -916,6 +918,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(),
Expand Down Expand Up @@ -2112,11 +2118,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,
},
Expand Down Expand Up @@ -2511,7 +2519,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,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;
Expand Down Expand Up @@ -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)'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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: {
Expand Down
11 changes: 10 additions & 1 deletion packages/asset-swapper/src/utils/market_operation_utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ export interface PsmInfo {
export interface LidoInfo {
stEthToken: string;
wethToken: string;
wstEthToken: string;
}

/**
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit d3396ff

Please sign in to comment.