diff --git a/contracts/zero-ex/CHANGELOG.json b/contracts/zero-ex/CHANGELOG.json index 8eb9ec4367..73397391fc 100644 --- a/contracts/zero-ex/CHANGELOG.json +++ b/contracts/zero-ex/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "0.30.0", + "changes": [ + { + "note": "Add `AaveV2` and `Compound` deposit/withdrawal liquidity source", + "pr": 321 + } + ] + }, { "timestamp": 1637102971, "version": "0.29.5", diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/BridgeAdapter.sol b/contracts/zero-ex/contracts/src/transformers/bridges/BridgeAdapter.sol index 0b8d5937e6..17d8acce7d 100644 --- a/contracts/zero-ex/contracts/src/transformers/bridges/BridgeAdapter.sol +++ b/contracts/zero-ex/contracts/src/transformers/bridges/BridgeAdapter.sol @@ -22,10 +22,12 @@ pragma experimental ABIEncoderV2; import "./IBridgeAdapter.sol"; import "./BridgeProtocols.sol"; +import "./mixins/MixinAaveV2.sol"; import "./mixins/MixinBalancer.sol"; import "./mixins/MixinBalancerV2.sol"; import "./mixins/MixinBancor.sol"; import "./mixins/MixinCoFiX.sol"; +import "./mixins/MixinCompound.sol"; import "./mixins/MixinCurve.sol"; import "./mixins/MixinCurveV2.sol"; import "./mixins/MixinCryptoCom.sol"; @@ -47,10 +49,12 @@ import "./mixins/MixinZeroExBridge.sol"; contract BridgeAdapter is IBridgeAdapter, + MixinAaveV2, MixinBalancer, MixinBalancerV2, MixinBancor, MixinCoFiX, + MixinCompound, MixinCurve, MixinCurveV2, MixinCryptoCom, @@ -72,10 +76,12 @@ contract BridgeAdapter is { constructor(IEtherTokenV06 weth) public + MixinAaveV2() MixinBalancer() MixinBalancerV2() MixinBancor(weth) MixinCoFiX() + MixinCompound(weth) MixinCurve(weth) MixinCurveV2() MixinCryptoCom() @@ -245,6 +251,20 @@ contract BridgeAdapter is sellAmount, order.bridgeData ); + } else if (protocolId == BridgeProtocols.AAVEV2) { + boughtAmount = _tradeAaveV2( + sellToken, + buyToken, + sellAmount, + order.bridgeData + ); + } else if (protocolId == BridgeProtocols.COMPOUND) { + boughtAmount = _tradeCompound( + sellToken, + buyToken, + sellAmount, + order.bridgeData + ); } else { boughtAmount = _tradeZeroExBridge( sellToken, diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/BridgeProtocols.sol b/contracts/zero-ex/contracts/src/transformers/bridges/BridgeProtocols.sol index acd0439e70..ea363779e5 100644 --- a/contracts/zero-ex/contracts/src/transformers/bridges/BridgeProtocols.sol +++ b/contracts/zero-ex/contracts/src/transformers/bridges/BridgeProtocols.sol @@ -50,4 +50,6 @@ library BridgeProtocols { uint128 internal constant CURVEV2 = 20; uint128 internal constant LIDO = 21; uint128 internal constant CLIPPER = 22; // Not used: Clipper is now using PLP interface + uint128 internal constant AAVEV2 = 23; + uint128 internal constant COMPOUND = 24; } diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinAaveV2.sol b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinAaveV2.sol new file mode 100644 index 0000000000..ad0c51c75b --- /dev/null +++ b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinAaveV2.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + + Copyright 2021 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6.5; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; +import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; + +// Minimal Aave V2 LendingPool interface +interface ILendingPool { + /** + * @dev Deposits an `amount` of underlying asset into the reserve, receiving in return overlying aTokens. + * - E.g. User deposits 100 USDC and gets in return 100 aUSDC + * @param asset The address of the underlying asset to deposit + * @param amount The amount to be deposited + * @param onBehalfOf The address that will receive the aTokens, same as msg.sender if the user + * wants to receive them on his own wallet, or a different address if the beneficiary of aTokens + * is a different wallet + * @param referralCode Code used to register the integrator originating the operation, for potential rewards. + * 0 if the action is executed directly by the user, without any middle-man + **/ + function deposit( + address asset, + uint256 amount, + address onBehalfOf, + uint16 referralCode + ) external; + + /** + * @dev Withdraws an `amount` of underlying asset from the reserve, burning the equivalent aTokens owned + * E.g. User has 100 aUSDC, calls withdraw() and receives 100 USDC, burning the 100 aUSDC + * @param asset The address of the underlying asset to withdraw + * @param amount The underlying amount to be withdrawn + * - Send the value type(uint256).max in order to withdraw the whole aToken balance + * @param to Address that will receive the underlying, same as msg.sender if the user + * wants to receive it on his own wallet, or a different address if the beneficiary is a + * different wallet + * @return The final amount withdrawn + **/ + function withdraw( + address asset, + uint256 amount, + address to + ) external returns (uint256); +} + +contract MixinAaveV2 { + using LibERC20TokenV06 for IERC20TokenV06; + + function _tradeAaveV2( + IERC20TokenV06 sellToken, + IERC20TokenV06 buyToken, + uint256 sellAmount, + bytes memory bridgeData + ) + internal + returns (uint256) + { + (ILendingPool lendingPool, address aToken) = abi.decode(bridgeData, (ILendingPool, address)); + + sellToken.approveIfBelow( + address(lendingPool), + sellAmount + ); + + if (address(buyToken) == aToken) { + lendingPool.deposit(address(sellToken), sellAmount, address(this), 0); + // 1:1 mapping token -> aToken and have the same number of decimals as the underlying token + return sellAmount; + } else if (address(sellToken) == aToken) { + return lendingPool.withdraw(address(buyToken), sellAmount, address(this)); + } + + revert("MixinAaveV2/UNSUPPORTED_TOKEN_PAIR"); + } +} diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinCompound.sol b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinCompound.sol new file mode 100644 index 0000000000..5adfb1abf9 --- /dev/null +++ b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinCompound.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + + Copyright 2021 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6.5; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; +import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; +import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; + + +/// @dev Minimal CToken interface +interface ICToken { + /// @dev deposits specified amount underlying tokens and mints cToken for the sender + /// @param mintAmountInUnderlying amount of underlying tokens to deposit to mint cTokens + /// @return status code of whether the mint was successful or not + function mint(uint256 mintAmountInUnderlying) external returns (uint256); + /// @dev redeems specified amount of cTokens and returns the underlying token to the sender + /// @param redeemTokensInCtokens amount of cTokens to redeem for underlying collateral + /// @return status code of whether the redemption was successful or not + function redeem(uint256 redeemTokensInCtokens) external returns (uint256); +} +/// @dev Minimal CEther interface +interface ICEther { + /// @dev deposits the amount of Ether sent as value and return mints cEther for the sender + function mint() payable external; + /// @dev redeems specified amount of cETH and returns the underlying ether to the sender + /// @dev redeemTokensInCEther amount of cETH to redeem for underlying ether + /// @return status code of whether the redemption was successful or not + function redeem(uint256 redeemTokensInCEther) external returns (uint256); +} + +contract MixinCompound { + using LibERC20TokenV06 for IERC20TokenV06; + using LibSafeMathV06 for uint256; + + IEtherTokenV06 private immutable WETH; + + constructor(IEtherTokenV06 weth) + public + { + WETH = weth; + } + + uint256 constant private COMPOUND_SUCCESS_CODE = 0; + + function _tradeCompound( + IERC20TokenV06 sellToken, + IERC20TokenV06 buyToken, + uint256 sellAmount, + bytes memory bridgeData + ) + internal + returns (uint256) + { + (address cTokenAddress) = abi.decode(bridgeData, (address)); + uint256 beforeBalance = buyToken.balanceOf(address(this)); + + if (address(buyToken) == cTokenAddress) { + if (address(sellToken) == address(WETH)) { + // ETH/WETH -> cETH + ICEther cETH = ICEther(cTokenAddress); + // Compound expects ETH to be sent with mint call + WETH.withdraw(sellAmount); + // NOTE: cETH mint will revert on failure instead of returning a status code + cETH.mint{value: sellAmount}(); + } else { + sellToken.approveIfBelow( + cTokenAddress, + sellAmount + ); + // Token -> cToken + ICToken cToken = ICToken(cTokenAddress); + require(cToken.mint(sellAmount) == COMPOUND_SUCCESS_CODE, "MixinCompound/FAILED_TO_MINT_CTOKEN"); + } + } else if (address(sellToken) == cTokenAddress) { + if (address(buyToken) == address(WETH)) { + // cETH -> ETH/WETH + uint256 etherBalanceBefore = address(this).balance; + ICEther cETH = ICEther(cTokenAddress); + require(cETH.redeem(sellAmount) == COMPOUND_SUCCESS_CODE, "MixinCompound/FAILED_TO_REDEEM_CETHER"); + uint256 etherBalanceAfter = address(this).balance; + uint256 receivedEtherBalance = etherBalanceAfter.safeSub(etherBalanceBefore); + WETH.deposit{value: receivedEtherBalance}(); + } else { + ICToken cToken = ICToken(cTokenAddress); + require(cToken.redeem(sellAmount) == COMPOUND_SUCCESS_CODE, "MixinCompound/FAILED_TO_REDEEM_CTOKEN"); + } + } + + return buyToken.balanceOf(address(this)).safeSub(beforeBalance); + } +} diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index c640661045..f1d1d65648 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -43,7 +43,7 @@ "config": { "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,PositiveSlippageFeeTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,AffiliateFeeTransformer,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature,ILiquidityProviderFeature,NativeOrdersFeature,INativeOrdersFeature,FeeCollectorController,FeeCollector,CurveLiquidityProvider,BatchFillNativeOrdersFeature,IBatchFillNativeOrdersFeature,MultiplexFeature,IMultiplexFeature,OtcOrdersFeature,IOtcOrdersFeature", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|BatchFillNativeOrdersFeature|BootstrapFeature|BridgeAdapter|BridgeProtocols|CurveLiquidityProvider|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|FundRecoveryFeature|IBatchFillNativeOrdersFeature|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IFeature|IFlashWallet|IFundRecoveryFeature|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|IMooniswapPool|IMultiplexFeature|INativeOrdersEvents|INativeOrdersFeature|IOtcOrdersFeature|IOwnableFeature|IPancakeSwapFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IUniswapV2Pair|IUniswapV3Feature|IUniswapV3Pool|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOtcOrdersStorage|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinBalancer|MixinBalancerV2|MixinBancor|MixinCoFiX|MixinCryptoCom|MixinCurve|MixinCurveV2|MixinDodo|MixinDodoV2|MixinKyber|MixinKyberDmm|MixinLido|MixinMStable|MixinMakerPSM|MixinMooniswap|MixinNerve|MixinOasis|MixinShell|MixinUniswap|MixinUniswapV2|MixinUniswapV3|MixinZeroExBridge|MooniswapLiquidityProvider|MultiplexFeature|MultiplexLiquidityProvider|MultiplexOtc|MultiplexRfq|MultiplexTransformERC20|MultiplexUniswapV2|MultiplexUniswapV3|NativeOrdersCancellation|NativeOrdersFeature|NativeOrdersInfo|NativeOrdersProtocolFees|NativeOrdersSettlement|OtcOrdersFeature|OwnableFeature|PancakeSwapFeature|PayTakerTransformer|PermissionlessTransformerDeployer|PositiveSlippageFeeTransformer|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestCurve|TestDelegateCaller|TestFeeCollectorController|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFixinProtocolFees|TestFixinTokenSpender|TestFullMigration|TestInitialMigration|TestLibNativeOrder|TestLibSignature|TestLiquidityProvider|TestMetaTransactionsNativeOrdersFeature|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestMooniswap|TestNativeOrdersFeature|TestNoEthRecipient|TestOrderSignerRegistryWithContractWallet|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestUniswapV2Factory|TestUniswapV2Pool|TestUniswapV3Factory|TestUniswapV3Feature|TestUniswapV3Pool|TestWeth|TestWethTransformerHost|TestZeroExFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|UniswapV3Feature|WethTransformer|ZeroEx|ZeroExOptimized).json" + "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|BatchFillNativeOrdersFeature|BootstrapFeature|BridgeAdapter|BridgeProtocols|CurveLiquidityProvider|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|FundRecoveryFeature|IBatchFillNativeOrdersFeature|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IFeature|IFlashWallet|IFundRecoveryFeature|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|IMooniswapPool|IMultiplexFeature|INativeOrdersEvents|INativeOrdersFeature|IOtcOrdersFeature|IOwnableFeature|IPancakeSwapFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IUniswapV2Pair|IUniswapV3Feature|IUniswapV3Pool|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOtcOrdersStorage|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinAaveV2|MixinBalancer|MixinBalancerV2|MixinBancor|MixinCoFiX|MixinCompound|MixinCryptoCom|MixinCurve|MixinCurveV2|MixinDodo|MixinDodoV2|MixinKyber|MixinKyberDmm|MixinLido|MixinMStable|MixinMakerPSM|MixinMooniswap|MixinNerve|MixinOasis|MixinShell|MixinUniswap|MixinUniswapV2|MixinUniswapV3|MixinZeroExBridge|MooniswapLiquidityProvider|MultiplexFeature|MultiplexLiquidityProvider|MultiplexOtc|MultiplexRfq|MultiplexTransformERC20|MultiplexUniswapV2|MultiplexUniswapV3|NativeOrdersCancellation|NativeOrdersFeature|NativeOrdersInfo|NativeOrdersProtocolFees|NativeOrdersSettlement|OtcOrdersFeature|OwnableFeature|PancakeSwapFeature|PayTakerTransformer|PermissionlessTransformerDeployer|PositiveSlippageFeeTransformer|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestCurve|TestDelegateCaller|TestFeeCollectorController|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFixinProtocolFees|TestFixinTokenSpender|TestFullMigration|TestInitialMigration|TestLibNativeOrder|TestLibSignature|TestLiquidityProvider|TestMetaTransactionsNativeOrdersFeature|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestMooniswap|TestNativeOrdersFeature|TestNoEthRecipient|TestOrderSignerRegistryWithContractWallet|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestUniswapV2Factory|TestUniswapV2Pool|TestUniswapV3Factory|TestUniswapV3Feature|TestUniswapV3Pool|TestWeth|TestWethTransformerHost|TestZeroExFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|UniswapV3Feature|WethTransformer|ZeroEx|ZeroExOptimized).json" }, "repository": { "type": "git", diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index 1b0b46fc75..7073aaa136 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -81,10 +81,12 @@ import * as LiquidityProviderFeature from '../test/generated-artifacts/Liquidity import * as LiquidityProviderSandbox from '../test/generated-artifacts/LiquidityProviderSandbox.json'; import * as LogMetadataTransformer from '../test/generated-artifacts/LogMetadataTransformer.json'; import * as MetaTransactionsFeature from '../test/generated-artifacts/MetaTransactionsFeature.json'; +import * as MixinAaveV2 from '../test/generated-artifacts/MixinAaveV2.json'; import * as MixinBalancer from '../test/generated-artifacts/MixinBalancer.json'; import * as MixinBalancerV2 from '../test/generated-artifacts/MixinBalancerV2.json'; import * as MixinBancor from '../test/generated-artifacts/MixinBancor.json'; import * as MixinCoFiX from '../test/generated-artifacts/MixinCoFiX.json'; +import * as MixinCompound from '../test/generated-artifacts/MixinCompound.json'; import * as MixinCryptoCom from '../test/generated-artifacts/MixinCryptoCom.json'; import * as MixinCurve from '../test/generated-artifacts/MixinCurve.json'; import * as MixinCurveV2 from '../test/generated-artifacts/MixinCurveV2.json'; @@ -272,10 +274,12 @@ export const artifacts = { BridgeAdapter: BridgeAdapter as ContractArtifact, BridgeProtocols: BridgeProtocols as ContractArtifact, IBridgeAdapter: IBridgeAdapter as ContractArtifact, + MixinAaveV2: MixinAaveV2 as ContractArtifact, MixinBalancer: MixinBalancer as ContractArtifact, MixinBalancerV2: MixinBalancerV2 as ContractArtifact, MixinBancor: MixinBancor as ContractArtifact, MixinCoFiX: MixinCoFiX as ContractArtifact, + MixinCompound: MixinCompound as ContractArtifact, MixinCryptoCom: MixinCryptoCom as ContractArtifact, MixinCurve: MixinCurve as ContractArtifact, MixinCurveV2: MixinCurveV2 as ContractArtifact, diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index 5af5690078..5d11e9cefc 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -79,10 +79,12 @@ export * from '../test/generated-wrappers/liquidity_provider_feature'; export * from '../test/generated-wrappers/liquidity_provider_sandbox'; export * from '../test/generated-wrappers/log_metadata_transformer'; export * from '../test/generated-wrappers/meta_transactions_feature'; +export * from '../test/generated-wrappers/mixin_aave_v2'; export * from '../test/generated-wrappers/mixin_balancer'; export * from '../test/generated-wrappers/mixin_balancer_v2'; export * from '../test/generated-wrappers/mixin_bancor'; export * from '../test/generated-wrappers/mixin_co_fi_x'; +export * from '../test/generated-wrappers/mixin_compound'; export * from '../test/generated-wrappers/mixin_crypto_com'; export * from '../test/generated-wrappers/mixin_curve'; export * from '../test/generated-wrappers/mixin_curve_v2'; diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index 3241e9801f..1b39a1a51e 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -112,10 +112,12 @@ "test/generated-artifacts/LiquidityProviderSandbox.json", "test/generated-artifacts/LogMetadataTransformer.json", "test/generated-artifacts/MetaTransactionsFeature.json", + "test/generated-artifacts/MixinAaveV2.json", "test/generated-artifacts/MixinBalancer.json", "test/generated-artifacts/MixinBalancerV2.json", "test/generated-artifacts/MixinBancor.json", "test/generated-artifacts/MixinCoFiX.json", + "test/generated-artifacts/MixinCompound.json", "test/generated-artifacts/MixinCryptoCom.json", "test/generated-artifacts/MixinCurve.json", "test/generated-artifacts/MixinCurveV2.json", diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index 0a9097a229..cf6555c272 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "16.40.0", + "changes": [ + { + "note": "Add `AaveV2` and `Compound` deposit/withdrawal liquidity source", + "pr": 321 + } + ] + }, { "version": "16.39.0", "changes": [ diff --git a/packages/asset-swapper/contracts/src/CompoundSampler.sol b/packages/asset-swapper/contracts/src/CompoundSampler.sol new file mode 100644 index 0000000000..2f68f59c7d --- /dev/null +++ b/packages/asset-swapper/contracts/src/CompoundSampler.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + + Copyright 2021 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6; +pragma experimental ABIEncoderV2; + +import "./SamplerUtils.sol"; +import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; + +// Minimal CToken interface +interface ICToken { + function mint(uint mintAmount) external returns (uint); + function redeem(uint redeemTokens) external returns (uint); + function redeemUnderlying(uint redeemAmount) external returns (uint); + function exchangeRateStored() external view returns (uint); + function decimals() external view returns (uint8); +} + +contract CompoundSampler is SamplerUtils { + uint256 constant private EXCHANGE_RATE_SCALE = 1e10; + + function sampleSellsFromCompound( + ICToken cToken, + IERC20TokenV06 takerToken, + IERC20TokenV06 makerToken, + uint256[] memory takerTokenAmounts + ) + public + view + returns (uint256[] memory makerTokenAmounts) + { + uint256 numSamples = takerTokenAmounts.length; + makerTokenAmounts = new uint256[](numSamples); + // Exchange rate is scaled by 1 * 10^(18 - 8 + Underlying Token Decimals + uint256 exchangeRate = cToken.exchangeRateStored(); + uint256 cTokenDecimals = uint256(cToken.decimals()); + + if (address(makerToken) == address(cToken)) { + // mint + for (uint256 i = 0; i < numSamples; i++) { + makerTokenAmounts[i] = (takerTokenAmounts[i] * EXCHANGE_RATE_SCALE * 10 ** cTokenDecimals) / exchangeRate; + } + + } else if (address(takerToken) == address(cToken)) { + // redeem + for (uint256 i = 0; i < numSamples; i++) { + makerTokenAmounts[i] = (takerTokenAmounts[i] * exchangeRate) / (EXCHANGE_RATE_SCALE * 10 ** cTokenDecimals); + } + } + } + + function sampleBuysFromCompound( + ICToken cToken, + IERC20TokenV06 takerToken, + IERC20TokenV06 makerToken, + uint256[] memory makerTokenAmounts + ) + public + view + returns (uint256[] memory takerTokenAmounts) + { + uint256 numSamples = makerTokenAmounts.length; + takerTokenAmounts = new uint256[](numSamples); + // Exchange rate is scaled by 1 * 10^(18 - 8 + Underlying Token Decimals + uint256 exchangeRate = cToken.exchangeRateStored(); + uint256 cTokenDecimals = uint256(cToken.decimals()); + + if (address(makerToken) == address(cToken)) { + // mint + for (uint256 i = 0; i < numSamples; i++) { + takerTokenAmounts[i] = makerTokenAmounts[i] * exchangeRate / (EXCHANGE_RATE_SCALE * 10 ** cTokenDecimals); + } + } else if (address(takerToken) == address(cToken)) { + // redeem + for (uint256 i = 0; i < numSamples; i++) { + takerTokenAmounts[i] = (makerTokenAmounts[i] * EXCHANGE_RATE_SCALE * 10 ** cTokenDecimals)/exchangeRate; + } + } + } +} diff --git a/packages/asset-swapper/contracts/src/ERC20BridgeSampler.sol b/packages/asset-swapper/contracts/src/ERC20BridgeSampler.sol index 60a2202615..17493e2a46 100644 --- a/packages/asset-swapper/contracts/src/ERC20BridgeSampler.sol +++ b/packages/asset-swapper/contracts/src/ERC20BridgeSampler.sol @@ -23,6 +23,7 @@ pragma experimental ABIEncoderV2; import "./BalancerSampler.sol"; import "./BalancerV2Sampler.sol"; import "./BancorSampler.sol"; +import "./CompoundSampler.sol"; import "./CurveSampler.sol"; import "./DODOSampler.sol"; import "./DODOV2Sampler.sol"; @@ -48,6 +49,7 @@ contract ERC20BridgeSampler is BalancerSampler, BalancerV2Sampler, BancorSampler, + CompoundSampler, CurveSampler, DODOSampler, DODOV2Sampler, diff --git a/packages/asset-swapper/package.json b/packages/asset-swapper/package.json index 62e45b1d77..a2a0931987 100644 --- a/packages/asset-swapper/package.json +++ b/packages/asset-swapper/package.json @@ -39,7 +39,7 @@ "config": { "publicInterfaceContracts": "ERC20BridgeSampler,BalanceChecker,FakeTaker", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(ApproximateBuys|BalanceChecker|BalancerSampler|BalancerV2Sampler|BancorSampler|CurveSampler|DODOSampler|DODOV2Sampler|DummyLiquidityProvider|ERC20BridgeSampler|FakeTaker|IBalancer|IBancor|ICurve|IKyberNetwork|IMStable|IMooniswap|IMultiBridge|IShell|ISmoothy|IUniswapExchangeQuotes|IUniswapV2Router01|KyberDmmSampler|KyberSampler|LidoSampler|LiquidityProviderSampler|MStableSampler|MakerPSMSampler|MooniswapSampler|MultiBridgeSampler|NativeOrderSampler|SamplerUtils|ShellSampler|SmoothySampler|TestERC20BridgeSampler|TestNativeOrderSampler|TwoHopSampler|UniswapSampler|UniswapV2Sampler|UniswapV3Sampler|UtilitySampler).json", + "abis": "./test/generated-artifacts/@(ApproximateBuys|BalanceChecker|BalancerSampler|BalancerV2Sampler|BancorSampler|CompoundSampler|CurveSampler|DODOSampler|DODOV2Sampler|DummyLiquidityProvider|ERC20BridgeSampler|FakeTaker|IBalancer|IBancor|ICurve|IKyberNetwork|IMStable|IMooniswap|IMultiBridge|IShell|ISmoothy|IUniswapExchangeQuotes|IUniswapV2Router01|KyberDmmSampler|KyberSampler|LidoSampler|LiquidityProviderSampler|MStableSampler|MakerPSMSampler|MooniswapSampler|MultiBridgeSampler|NativeOrderSampler|SamplerUtils|ShellSampler|SmoothySampler|TestERC20BridgeSampler|TestNativeOrderSampler|TwoHopSampler|UniswapSampler|UniswapV2Sampler|UniswapV3Sampler|UtilitySampler).json", "postpublish": { "assets": [] } diff --git a/packages/asset-swapper/src/constants.ts b/packages/asset-swapper/src/constants.ts index 250a7a8478..9256659599 100644 --- a/packages/asset-swapper/src/constants.ts +++ b/packages/asset-swapper/src/constants.ts @@ -28,7 +28,6 @@ const ONE_SECOND_MS = 1000; const ONE_MINUTE_SECS = 60; const ONE_MINUTE_MS = ONE_SECOND_MS * ONE_MINUTE_SECS; const DEFAULT_PER_PAGE = 1000; -const ZERO_AMOUNT = new BigNumber(0); const ALT_MM_IMPUTED_INDICATIVE_EXPIRY_SECONDS = 180; const DEFAULT_ORDER_PRUNER_OPTS: OrderPrunerOpts = { @@ -43,6 +42,7 @@ const PROTOCOL_FEE_MULTIPLIER = new BigNumber(0); // default 50% buffer for selecting native orders to be aggregated with other sources const MARKET_UTILS_AMOUNT_BUFFER_PERCENTAGE = 0.5; +export const ZERO_AMOUNT = new BigNumber(0); const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = { chainId: ChainId.Mainnet, orderRefreshIntervalMs: 10000, // 10 seconds diff --git a/packages/asset-swapper/src/noop_samplers/AaveV2Sampler.ts b/packages/asset-swapper/src/noop_samplers/AaveV2Sampler.ts new file mode 100644 index 0000000000..c56f12601e --- /dev/null +++ b/packages/asset-swapper/src/noop_samplers/AaveV2Sampler.ts @@ -0,0 +1,57 @@ +import { BigNumber } from '@0x/utils'; + +import { ZERO_AMOUNT } from '../constants'; +export interface AaveInfo { + lendingPool: string; + aToken: string; + underlyingToken: string; +} +// tslint:disable-next-line:no-unnecessary-class +export class AaveV2Sampler { + public static sampleSellsFromAaveV2( + aaveInfo: AaveInfo, + takerToken: string, + makerToken: string, + takerTokenAmounts: BigNumber[], + ): BigNumber[] { + // Deposit/Withdrawal underlying <-> aToken is always 1:1 + if ( + (takerToken.toLowerCase() === aaveInfo.aToken.toLowerCase() && + makerToken.toLowerCase() === aaveInfo.underlyingToken.toLowerCase()) || + (takerToken.toLowerCase() === aaveInfo.underlyingToken.toLowerCase() && + makerToken.toLowerCase() === aaveInfo.aToken.toLowerCase()) + ) { + return takerTokenAmounts; + } + + // Not matching the reserve return 0 results + const numSamples = takerTokenAmounts.length; + + const makerTokenAmounts = new Array(numSamples); + makerTokenAmounts.fill(ZERO_AMOUNT); + return makerTokenAmounts; + } + + public static sampleBuysFromAaveV2( + aaveInfo: AaveInfo, + takerToken: string, + makerToken: string, + makerTokenAmounts: BigNumber[], + ): BigNumber[] { + // Deposit/Withdrawal underlying <-> aToken is always 1:1 + if ( + (takerToken.toLowerCase() === aaveInfo.aToken.toLowerCase() && + makerToken.toLowerCase() === aaveInfo.underlyingToken.toLowerCase()) || + (takerToken.toLowerCase() === aaveInfo.underlyingToken.toLowerCase() && + makerToken.toLowerCase() === aaveInfo.aToken.toLowerCase()) + ) { + return makerTokenAmounts; + } + + // Not matching the reserve return 0 results + const numSamples = makerTokenAmounts.length; + const takerTokenAmounts = new Array(numSamples); + takerTokenAmounts.fill(ZERO_AMOUNT); + return takerTokenAmounts; + } +} diff --git a/packages/asset-swapper/src/utils/market_operation_utils/aave_reserves_cache.ts b/packages/asset-swapper/src/utils/market_operation_utils/aave_reserves_cache.ts new file mode 100644 index 0000000000..c890691a04 --- /dev/null +++ b/packages/asset-swapper/src/utils/market_operation_utils/aave_reserves_cache.ts @@ -0,0 +1,106 @@ +import { logUtils } from '@0x/utils'; +import { gql, request } from 'graphql-request'; + +import { constants } from '../../constants'; + +const RESERVES_GQL_QUERY = gql` + { + reserves( + first: 300 + where: { isActive: true, isFrozen: false } + orderBy: totalLiquidity + orderDirection: desc + ) { + id + underlyingAsset + aToken { + id + } + pool { + id + lendingPool + } + } + } +`; + +export interface AaveReserve { + id: string; + underlyingAsset: string; + aToken: { + id: string; + }; + pool: { + id: string; + lendingPool: string; + }; +} + +interface Cache { + [key: string]: AaveReserve[]; +} + +// tslint:disable-next-line:custom-no-magic-numbers +const RESERVES_REFRESH_INTERVAL_MS = 30 * constants.ONE_MINUTE_MS; + +/** + * Fetches Aave V2 reserve information from the official subgraph(s). + * The reserve information is updated every 30 minutes and cached + * so that it can be accessed with the underlying token's address + */ +export class AaveV2ReservesCache { + private _cache: Cache = {}; + constructor(private readonly _subgraphUrl: string) { + const resfreshReserves = async () => this.fetchAndUpdateReservesAsync(); + // tslint:disable-next-line:no-floating-promises + resfreshReserves(); + setInterval(resfreshReserves, RESERVES_REFRESH_INTERVAL_MS); + } + /** + * Fetches Aave V2 reserves from the subgraph and updates the cache + */ + public async fetchAndUpdateReservesAsync(): Promise { + try { + const { reserves } = await request<{ reserves: AaveReserve[] }>(this._subgraphUrl, RESERVES_GQL_QUERY); + const newCache = reserves.reduce((memo, reserve) => { + const underlyingAsset = reserve.underlyingAsset.toLowerCase(); + if (!memo[underlyingAsset]) { + memo[underlyingAsset] = []; + } + + memo[underlyingAsset].push(reserve); + return memo; + }, {}); + + this._cache = newCache; + } catch (err) { + logUtils.warn(`Failed to update Aave V2 reserves cache: ${err.message}`); + // Empty cache just to be safe + this._cache = {}; + } + } + public get(takerToken: string, makerToken: string): AaveReserve | undefined { + // Deposit takerToken into reserve + if (this._cache[takerToken.toLowerCase()]) { + const matchingReserve = this._cache[takerToken.toLowerCase()].find( + r => r.aToken.id === makerToken.toLowerCase(), + ); + if (matchingReserve) { + return matchingReserve; + } + } + + // Withdraw makerToken from reserve + if (this._cache[makerToken.toLowerCase()]) { + const matchingReserve = this._cache[makerToken.toLowerCase()].find( + r => r.aToken.id === takerToken.toLowerCase(), + ); + if (matchingReserve) { + return matchingReserve; + } + } + + // No match + return undefined; + } +} diff --git a/packages/asset-swapper/src/utils/market_operation_utils/compound_ctoken_cache.ts b/packages/asset-swapper/src/utils/market_operation_utils/compound_ctoken_cache.ts new file mode 100644 index 0000000000..ca0fd0c78f --- /dev/null +++ b/packages/asset-swapper/src/utils/market_operation_utils/compound_ctoken_cache.ts @@ -0,0 +1,78 @@ +import { logUtils } from '@0x/utils'; +import axios from 'axios'; + +import { constants } from '../../constants'; + +export interface CToken { + tokenAddress: string; + underlyingAddress: string; +} + +interface CTokenApiResponse { + cToken: Array<{ + token_address: string; + underlying_address: string; + }>; +} + +interface Cache { + [key: string]: CToken; +} + +// tslint:disable-next-line:custom-no-magic-numbers +const CTOKEN_REFRESH_INTERVAL_MS = 30 * constants.ONE_MINUTE_MS; + +/** + * Fetches a list of CTokens from Compound's official API. + * The token information is updated every 30 minutes and cached + * so that it can be accessed with the underlying token's address. + */ +export class CompoundCTokenCache { + private _cache: Cache = {}; + constructor(private readonly _apiUrl: string, private readonly _wethAddress: string) { + const refreshCTokenCache = async () => this.fetchAndUpdateCTokensAsync(); + // tslint:disable-next-line:no-floating-promises + refreshCTokenCache(); + setInterval(refreshCTokenCache, CTOKEN_REFRESH_INTERVAL_MS); + } + + public async fetchAndUpdateCTokensAsync(): Promise { + try { + const { data } = await axios.get(`${this._apiUrl}/ctoken`); + const newCache = data?.cToken.reduce((memo, cToken) => { + // NOTE: Re-map cETH with null underlying token address to WETH address (we only handle WETH internally) + const underlyingAddressClean = cToken.underlying_address + ? cToken.underlying_address.toLowerCase() + : this._wethAddress; + + const tokenData: CToken = { + tokenAddress: cToken.token_address.toLowerCase(), + underlyingAddress: underlyingAddressClean, + }; + memo[underlyingAddressClean] = tokenData; + return memo; + }, {}); + + this._cache = newCache; + } catch (err) { + logUtils.warn(`Failed to update Compound cToken cache: ${err.message}`); + // NOTE: Safe to keep already cached data as tokens should only be added to the list + } + } + public get(takerToken: string, makerToken: string): CToken | undefined { + // mint cToken + let cToken = this._cache[takerToken.toLowerCase()]; + if (cToken && makerToken.toLowerCase() === cToken.tokenAddress.toLowerCase()) { + return cToken; + } + + // redeem cToken + cToken = this._cache[makerToken.toLowerCase()]; + if (cToken && takerToken.toLowerCase() === cToken.tokenAddress.toLowerCase()) { + return cToken; + } + + // No match + return undefined; + } +} 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 e63648f4e4..9b4e5efe30 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts @@ -7,7 +7,9 @@ import { TokenAdjacencyGraphBuilder } from '../token_adjacency_graph_builder'; import { SourceFilters } from './source_filters'; import { + AaveV2FillData, BancorFillData, + CompoundFillData, CurveFillData, CurveFunctionSelectors, CurveInfo, @@ -101,6 +103,9 @@ export const SELL_SOURCE_FILTER_BY_CHAIN_ID = valueByChainId( ERC20BridgeSource.UniswapV3, ERC20BridgeSource.CurveV2, ERC20BridgeSource.ShibaSwap, + // TODO: enable after FQT has been redeployed on Ethereum mainnet + // ERC20BridgeSource.AaveV2, + // ERC20BridgeSource.Compound, ]), [ChainId.Ropsten]: new SourceFilters([ ERC20BridgeSource.Kyber, @@ -159,6 +164,7 @@ export const SELL_SOURCE_FILTER_BY_CHAIN_ID = valueByChainId( ERC20BridgeSource.MultiHop, ERC20BridgeSource.JetSwap, ERC20BridgeSource.IronSwap, + ERC20BridgeSource.AaveV2, ]), [ChainId.Avalanche]: new SourceFilters([ ERC20BridgeSource.MultiHop, @@ -168,6 +174,7 @@ export const SELL_SOURCE_FILTER_BY_CHAIN_ID = valueByChainId( ERC20BridgeSource.Curve, ERC20BridgeSource.CurveV2, ERC20BridgeSource.KyberDmm, + ERC20BridgeSource.AaveV2, ]), [ChainId.Fantom]: new SourceFilters([ ERC20BridgeSource.MultiHop, @@ -227,6 +234,9 @@ export const BUY_SOURCE_FILTER_BY_CHAIN_ID = valueByChainId( ERC20BridgeSource.UniswapV3, ERC20BridgeSource.CurveV2, ERC20BridgeSource.ShibaSwap, + // TODO: enable after FQT has been redeployed on Ethereum mainnet + // ERC20BridgeSource.AaveV2, + // ERC20BridgeSource.Compound, ]), [ChainId.Ropsten]: new SourceFilters([ ERC20BridgeSource.Kyber, @@ -285,6 +295,7 @@ export const BUY_SOURCE_FILTER_BY_CHAIN_ID = valueByChainId( ERC20BridgeSource.MultiHop, ERC20BridgeSource.JetSwap, ERC20BridgeSource.IronSwap, + ERC20BridgeSource.AaveV2, ]), [ChainId.Avalanche]: new SourceFilters([ ERC20BridgeSource.MultiHop, @@ -294,6 +305,7 @@ export const BUY_SOURCE_FILTER_BY_CHAIN_ID = valueByChainId( ERC20BridgeSource.Curve, ERC20BridgeSource.CurveV2, ERC20BridgeSource.KyberDmm, + ERC20BridgeSource.AaveV2, ]), [ChainId.Fantom]: new SourceFilters([ ERC20BridgeSource.MultiHop, @@ -1700,6 +1712,24 @@ export const UNISWAPV3_CONFIG_BY_CHAIN_ID = valueByChainId( { quoter: NULL_ADDRESS, router: NULL_ADDRESS }, ); +export const AAVE_V2_SUBGRAPH_URL_BY_CHAIN_ID = valueByChainId( + { + // TODO: enable after FQT has been redeployed on Ethereum mainnet + // [ChainId.Mainnet]: 'https://api.thegraph.com/subgraphs/name/aave/protocol-v2', + [ChainId.Polygon]: 'https://api.thegraph.com/subgraphs/name/aave/aave-v2-matic', + [ChainId.Avalanche]: 'https://api.thegraph.com/subgraphs/name/aave/protocol-v2-avalanche', + }, + null, +); + +export const COMPOUND_API_URL_BY_CHAIN_ID = valueByChainId( + { + // TODO: enable after FQT has been redeployed on Ethereum mainnet + // [ChainId.Mainnet]: 'https://api.compound.finance/api/v2', + }, + null, +); + // // BSC // @@ -1965,6 +1995,21 @@ export const DEFAULT_GAS_SCHEDULE: Required = { return gas; }, [ERC20BridgeSource.Lido]: () => 226e3, + [ERC20BridgeSource.AaveV2]: (fillData?: FillData) => { + const aaveFillData = fillData as AaveV2FillData; + // NOTE: The Aave deposit method is more expensive than the withdraw + return aaveFillData.takerToken === aaveFillData.underlyingToken ? 400e3 : 300e3; + }, + [ERC20BridgeSource.Compound]: (fillData?: FillData) => { + // NOTE: cETH is handled differently than other cTokens + const wethAddress = NATIVE_FEE_TOKEN_BY_CHAIN_ID[ChainId.Mainnet]; + const compoundFillData = fillData as CompoundFillData; + if (compoundFillData.takerToken === compoundFillData.cToken) { + return compoundFillData.makerToken === wethAddress ? 120e3 : 150e3; + } else { + return compoundFillData.takerToken === wethAddress ? 210e3 : 250e3; + } + }, // // BSC 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 fedca1c258..db16f66425 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts @@ -5,11 +5,13 @@ import { AssetSwapperContractAddresses, MarketOperation } from '../../types'; import { MAX_UINT256, ZERO_AMOUNT } from './constants'; import { + AaveV2FillData, AggregationError, BalancerFillData, BalancerV2FillData, BancorFillData, CollapsedFill, + CompoundFillData, CurveFillData, DexSample, DODOFillData, @@ -194,6 +196,10 @@ export function getErc20BridgeSourceToBridgeSource(source: ERC20BridgeSource): s return encodeBridgeSourceId(BridgeProtocol.UniswapV2, 'SpookySwap'); case ERC20BridgeSource.MorpheusSwap: return encodeBridgeSourceId(BridgeProtocol.UniswapV2, 'MorpheusSwap'); + case ERC20BridgeSource.AaveV2: + return encodeBridgeSourceId(BridgeProtocol.AaveV2, 'AaveV2'); + case ERC20BridgeSource.Compound: + return encodeBridgeSourceId(BridgeProtocol.Compound, 'Compound'); default: throw new Error(AggregationError.NoBridgeForSource); } @@ -339,6 +345,15 @@ export function createBridgeDataForBridgeOrder(order: OptimizedMarketBridgeOrder const lidoFillData = (order as OptimizedMarketBridgeOrder).fillData; bridgeData = encoder.encode([lidoFillData.stEthTokenAddress]); break; + case ERC20BridgeSource.AaveV2: + const aaveFillData = (order as OptimizedMarketBridgeOrder).fillData; + bridgeData = encoder.encode([aaveFillData.lendingPool, aaveFillData.aToken]); + break; + case ERC20BridgeSource.Compound: + const compoundFillData = (order as OptimizedMarketBridgeOrder).fillData; + bridgeData = encoder.encode([compoundFillData.cToken]); + break; + default: throw new Error(AggregationError.NoBridgeForSource); } @@ -504,6 +519,8 @@ export const BRIDGE_ENCODERS: { ]), [ERC20BridgeSource.KyberDmm]: AbiEncoder.create('(address,address[],address[])'), [ERC20BridgeSource.Lido]: AbiEncoder.create('(address)'), + [ERC20BridgeSource.AaveV2]: AbiEncoder.create('(address,address)'), + [ERC20BridgeSource.Compound]: AbiEncoder.create('(address)'), }; function getFillTokenAmounts(fill: CollapsedFill, side: MarketOperation): [BigNumber, BigNumber] { diff --git a/packages/asset-swapper/src/utils/market_operation_utils/sampler_no_operation.ts b/packages/asset-swapper/src/utils/market_operation_utils/sampler_no_operation.ts new file mode 100644 index 0000000000..8c4abd132a --- /dev/null +++ b/packages/asset-swapper/src/utils/market_operation_utils/sampler_no_operation.ts @@ -0,0 +1,36 @@ +import { BigNumber, logUtils, NULL_BYTES } from '@0x/utils'; + +import { ERC20BridgeSource, FillData, SourceQuoteOperation } from './types'; + +interface SamplerNoOperationCall { + callback: () => BigNumber[]; +} + +/** + * SamplerNoOperation can be used for sources where we already have all the necessary information + * required to perform the sample operations, without needing access to any on-chain data. Using a noop sample + * you can skip the eth_call, and just calculate the results directly in typescript land. + */ +export class SamplerNoOperation implements SourceQuoteOperation { + public readonly source: ERC20BridgeSource; + public fillData: TFillData; + private readonly _callback: () => BigNumber[]; + + constructor(opts: { source: ERC20BridgeSource; fillData?: TFillData } & SamplerNoOperationCall) { + this.source = opts.source; + this.fillData = opts.fillData || ({} as TFillData); // tslint:disable-line:no-object-literal-type-assertion + this._callback = opts.callback; + } + + // tslint:disable-next-line:prefer-function-over-method + public encodeCall(): string { + return NULL_BYTES; + } + public handleCallResults(_callResults: string): BigNumber[] { + return this._callback(); + } + public handleRevert(_callResults: string): BigNumber[] { + logUtils.warn(`SamplerNoOperation: ${this.source} reverted`); + return []; + } +} 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 8554d9e28c..783ad9ea65 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 @@ -3,9 +3,11 @@ import { LimitOrderFields } from '@0x/protocol-utils'; import { BigNumber, logUtils } from '@0x/utils'; import * as _ from 'lodash'; +import { AaveV2Sampler } from '../../noop_samplers/AaveV2Sampler'; import { SamplerCallResult, SignedNativeOrder } from '../../types'; import { ERC20BridgeSamplerContract } from '../../wrappers'; +import { AaveV2ReservesCache } from './aave_reserves_cache'; import { BancorService } from './bancor_service'; import { getCurveLikeInfosForPair, @@ -17,11 +19,14 @@ import { isValidAddress, uniswapV2LikeRouterAddress, } from './bridge_source_utils'; +import { CompoundCTokenCache } from './compound_ctoken_cache'; import { + AAVE_V2_SUBGRAPH_URL_BY_CHAIN_ID, BALANCER_V2_VAULT_ADDRESS_BY_CHAIN, BANCOR_REGISTRY_BY_CHAIN_ID, BEETHOVEN_X_SUBGRAPH_URL_BY_CHAIN, BEETHOVEN_X_VAULT_ADDRESS_BY_CHAIN, + COMPOUND_API_URL_BY_CHAIN_ID, DODOV1_CONFIG_BY_CHAIN_ID, DODOV2_FACTORIES_BY_CHAIN_ID, KYBER_CONFIG_BY_CHAIN_ID, @@ -45,13 +50,17 @@ import { getLiquidityProvidersForPair } from './liquidity_provider_utils'; import { getIntermediateTokens } from './multihop_utils'; import { BalancerPoolsCache, BalancerV2PoolsCache, CreamPoolsCache, PoolsCache } from './pools_cache'; import { SamplerContractOperation } from './sampler_contract_operation'; +import { SamplerNoOperation } from './sampler_no_operation'; import { SourceFilters } from './source_filters'; import { + AaveV2FillData, + AaveV2Info, BalancerFillData, BalancerV2FillData, BalancerV2PoolInfo, BancorFillData, BatchedOperation, + CompoundFillData, CurveFillData, CurveInfo, DexSample, @@ -99,6 +108,8 @@ export const BATCH_SOURCE_FILTERS = SourceFilters.all().exclude([ERC20BridgeSour export class SamplerOperations { public readonly liquidityProviderRegistry: LiquidityProviderRegistry; public readonly poolsCaches: { [key in SourcesWithPoolsCache]: PoolsCache }; + public readonly aaveReservesCache: AaveV2ReservesCache | undefined; + public readonly compoundCTokenCache: CompoundCTokenCache | undefined; protected _bancorService?: BancorService; public static constant(result: T): BatchedOperation { return { @@ -131,6 +142,19 @@ export class SamplerOperations { [ERC20BridgeSource.Balancer]: new BalancerPoolsCache(), [ERC20BridgeSource.Cream]: new CreamPoolsCache(), }; + + const aaveSubgraphUrl = AAVE_V2_SUBGRAPH_URL_BY_CHAIN_ID[chainId]; + if (aaveSubgraphUrl) { + this.aaveReservesCache = new AaveV2ReservesCache(aaveSubgraphUrl); + } + + const compoundApiUrl = COMPOUND_API_URL_BY_CHAIN_ID[chainId]; + if (compoundApiUrl) { + this.compoundCTokenCache = new CompoundCTokenCache( + compoundApiUrl, + NATIVE_FEE_TOKEN_BY_CHAIN_ID[this.chainId], + ); + } // Initialize the Bancor service, fetching paths in the background bancorServiceFn() .then(service => (this._bancorService = service)) @@ -1099,6 +1123,64 @@ export class SamplerOperations { }); } + // tslint:disable-next-line:prefer-function-over-method + public getAaveV2SellQuotes( + aaveInfo: AaveV2Info, + makerToken: string, + takerToken: string, + takerFillAmounts: BigNumber[], + ): SourceQuoteOperation { + return new SamplerNoOperation({ + source: ERC20BridgeSource.AaveV2, + fillData: { ...aaveInfo, takerToken }, + callback: () => AaveV2Sampler.sampleSellsFromAaveV2(aaveInfo, takerToken, makerToken, takerFillAmounts), + }); + } + + // tslint:disable-next-line:prefer-function-over-method + public getAaveV2BuyQuotes( + aaveInfo: AaveV2Info, + makerToken: string, + takerToken: string, + makerFillAmounts: BigNumber[], + ): SourceQuoteOperation { + return new SamplerNoOperation({ + source: ERC20BridgeSource.AaveV2, + fillData: { ...aaveInfo, takerToken }, + callback: () => AaveV2Sampler.sampleBuysFromAaveV2(aaveInfo, takerToken, makerToken, makerFillAmounts), + }); + } + + public getCompoundSellQuotes( + cToken: string, + makerToken: string, + takerToken: string, + takerFillAmounts: BigNumber[], + ): SourceQuoteOperation { + return new SamplerContractOperation({ + source: ERC20BridgeSource.Compound, + fillData: { cToken, takerToken, makerToken }, + contract: this._samplerContract, + function: this._samplerContract.sampleSellsFromCompound, + params: [cToken, takerToken, makerToken, takerFillAmounts], + }); + } + + public getCompoundBuyQuotes( + cToken: string, + makerToken: string, + takerToken: string, + makerFillAmounts: BigNumber[], + ): SourceQuoteOperation { + return new SamplerContractOperation({ + source: ERC20BridgeSource.Compound, + fillData: { cToken, takerToken, makerToken }, + contract: this._samplerContract, + function: this._samplerContract.sampleBuysFromCompound, + params: [cToken, takerToken, makerToken, makerFillAmounts], + }); + } + public getMedianSellRate( sources: ERC20BridgeSource[], makerToken: string, @@ -1449,6 +1531,38 @@ export class SamplerOperations { return this.getLidoSellQuotes(lidoInfo, makerToken, takerToken, takerFillAmounts); } + case ERC20BridgeSource.AaveV2: { + if (!this.aaveReservesCache) { + return []; + } + const reserve = this.aaveReservesCache.get(takerToken, makerToken); + if (!reserve) { + return []; + } + + const info: AaveV2Info = { + lendingPool: reserve.pool.lendingPool, + aToken: reserve.aToken.id, + underlyingToken: reserve.underlyingAsset, + }; + return this.getAaveV2SellQuotes(info, makerToken, takerToken, takerFillAmounts); + } + case ERC20BridgeSource.Compound: { + if (!this.compoundCTokenCache) { + return []; + } + + const cToken = this.compoundCTokenCache.get(takerToken, makerToken); + if (!cToken) { + return []; + } + return this.getCompoundSellQuotes( + cToken.tokenAddress, + makerToken, + takerToken, + takerFillAmounts, + ); + } default: throw new Error(`Unsupported sell sample source: ${source}`); } @@ -1718,6 +1832,32 @@ export class SamplerOperations { return this.getLidoBuyQuotes(lidoInfo, makerToken, takerToken, makerFillAmounts); } + case ERC20BridgeSource.AaveV2: { + if (!this.aaveReservesCache) { + return []; + } + const reserve = this.aaveReservesCache.get(takerToken, makerToken); + if (!reserve) { + return []; + } + const info: AaveV2Info = { + lendingPool: reserve.pool.lendingPool, + aToken: reserve.aToken.id, + underlyingToken: reserve.underlyingAsset, + }; + return this.getAaveV2BuyQuotes(info, makerToken, takerToken, makerFillAmounts); + } + case ERC20BridgeSource.Compound: { + if (!this.compoundCTokenCache) { + return []; + } + + const cToken = this.compoundCTokenCache.get(takerToken, makerToken); + if (!cToken) { + return []; + } + return this.getCompoundBuyQuotes(cToken.tokenAddress, makerToken, takerToken, makerFillAmounts); + } default: throw new Error(`Unsupported buy sample source: ${source}`); } 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 2cf1ae9de1..bed7c20c41 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/types.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/types.ts @@ -68,6 +68,8 @@ export enum ERC20BridgeSource { CurveV2 = 'Curve_V2', Lido = 'Lido', ShibaSwap = 'ShibaSwap', + AaveV2 = 'Aave_V2', + Compound = 'Compound', // BSC only PancakeSwap = 'PancakeSwap', PancakeSwapV2 = 'PancakeSwap_V2', @@ -172,6 +174,12 @@ export interface BalancerV2PoolInfo { vault: string; } +export interface AaveV2Info { + lendingPool: string; + aToken: string; + underlyingToken: string; +} + // Internal `fillData` field for `Fill` objects. export interface FillData {} @@ -279,6 +287,19 @@ export interface LidoFillData extends FillData { takerToken: string; } +export interface AaveV2FillData extends FillData { + lendingPool: string; + aToken: string; + underlyingToken: string; + takerToken: string; +} + +export interface CompoundFillData extends FillData { + cToken: string; + takerToken: string; + makerToken: string; +} + /** * Represents a node on a fill path. */ diff --git a/packages/asset-swapper/test/artifacts.ts b/packages/asset-swapper/test/artifacts.ts index 3214a68a96..c031b54f9c 100644 --- a/packages/asset-swapper/test/artifacts.ts +++ b/packages/asset-swapper/test/artifacts.ts @@ -10,6 +10,7 @@ import * as BalanceChecker from '../test/generated-artifacts/BalanceChecker.json import * as BalancerSampler from '../test/generated-artifacts/BalancerSampler.json'; import * as BalancerV2Sampler from '../test/generated-artifacts/BalancerV2Sampler.json'; import * as BancorSampler from '../test/generated-artifacts/BancorSampler.json'; +import * as CompoundSampler from '../test/generated-artifacts/CompoundSampler.json'; import * as CurveSampler from '../test/generated-artifacts/CurveSampler.json'; import * as DODOSampler from '../test/generated-artifacts/DODOSampler.json'; import * as DODOV2Sampler from '../test/generated-artifacts/DODOV2Sampler.json'; @@ -52,6 +53,7 @@ export const artifacts = { BalancerSampler: BalancerSampler as ContractArtifact, BalancerV2Sampler: BalancerV2Sampler as ContractArtifact, BancorSampler: BancorSampler as ContractArtifact, + CompoundSampler: CompoundSampler as ContractArtifact, CurveSampler: CurveSampler as ContractArtifact, DODOSampler: DODOSampler as ContractArtifact, DODOV2Sampler: DODOV2Sampler as ContractArtifact, diff --git a/packages/asset-swapper/test/wrappers.ts b/packages/asset-swapper/test/wrappers.ts index 0649844b3b..ab7db5a605 100644 --- a/packages/asset-swapper/test/wrappers.ts +++ b/packages/asset-swapper/test/wrappers.ts @@ -8,6 +8,7 @@ export * from '../test/generated-wrappers/balance_checker'; export * from '../test/generated-wrappers/balancer_sampler'; export * from '../test/generated-wrappers/balancer_v2_sampler'; export * from '../test/generated-wrappers/bancor_sampler'; +export * from '../test/generated-wrappers/compound_sampler'; export * from '../test/generated-wrappers/curve_sampler'; export * from '../test/generated-wrappers/d_o_d_o_sampler'; export * from '../test/generated-wrappers/d_o_d_o_v2_sampler'; diff --git a/packages/asset-swapper/tsconfig.json b/packages/asset-swapper/tsconfig.json index 83a7c21009..47af31e7a4 100644 --- a/packages/asset-swapper/tsconfig.json +++ b/packages/asset-swapper/tsconfig.json @@ -11,6 +11,7 @@ "test/generated-artifacts/BalancerSampler.json", "test/generated-artifacts/BalancerV2Sampler.json", "test/generated-artifacts/BancorSampler.json", + "test/generated-artifacts/CompoundSampler.json", "test/generated-artifacts/CurveSampler.json", "test/generated-artifacts/DODOSampler.json", "test/generated-artifacts/DODOV2Sampler.json", diff --git a/packages/contract-addresses/CHANGELOG.json b/packages/contract-addresses/CHANGELOG.json index 1a7cd09a71..2bb0e06506 100644 --- a/packages/contract-addresses/CHANGELOG.json +++ b/packages/contract-addresses/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "6.10.0", + "changes": [ + { + "note": "Add Aave supported FQT addresses for Polygon, Avalanche", + "pr": 321 + } + ] + }, { "version": "6.9.0", "changes": [ diff --git a/packages/contract-addresses/addresses.json b/packages/contract-addresses/addresses.json index e6bd076e8f..f24924f710 100644 --- a/packages/contract-addresses/addresses.json +++ b/packages/contract-addresses/addresses.json @@ -289,7 +289,7 @@ "wethTransformer": "0xe309d011cc6f189a3e8dcba85922715a019fed38", "payTakerTransformer": "0x5ba7b9be86cda01cfbf56e0fb97184783be9dda1", "affiliateFeeTransformer": "0xbed27284b42e5684e987169cf1da09c5d6c49fa8", - "fillQuoteTransformer": "0xf708d512b8a82e2862543a630403327174410baf", + "fillQuoteTransformer": "0xd3afdf4a8ea9183e76c9c2306cda03ea4afffea5", "positiveSlippageFeeTransformer": "0x4cd8f1c0df4d40fcc1e073845d5f6f4ed5cc8dab" } }, @@ -373,7 +373,7 @@ "wethTransformer": "0x9b8b52391071d71cd4ad1e61d7f273268fa34c6c", "payTakerTransformer": "0x898c6fde239d646c73f0a57e3570b6f86a3d62a3", "affiliateFeeTransformer": "0x34617b855411e52fbc05899435f44cbd0503022c", - "fillQuoteTransformer": "0x8a5417dd7ffde61ec61e11b45797e16686e1d6b9", + "fillQuoteTransformer": "0xd421f50b3ae27f223aa35a04944236d257235412", "positiveSlippageFeeTransformer": "0x470ba89da18a6db6e8a0567b3c9214b960861857" } }, diff --git a/packages/protocol-utils/CHANGELOG.json b/packages/protocol-utils/CHANGELOG.json index 2f568bcbf1..056a950301 100644 --- a/packages/protocol-utils/CHANGELOG.json +++ b/packages/protocol-utils/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "1.10.0", + "changes": [ + { + "note": "Add `AaveV2` and `Compound` deposit/withdrawal liquidity source", + "pr": 321 + } + ] + }, { "timestamp": 1637102971, "version": "1.9.5", diff --git a/packages/protocol-utils/src/transformer_utils.ts b/packages/protocol-utils/src/transformer_utils.ts index f37cd1a92f..4d57084ab8 100644 --- a/packages/protocol-utils/src/transformer_utils.ts +++ b/packages/protocol-utils/src/transformer_utils.ts @@ -132,6 +132,8 @@ export enum BridgeProtocol { CurveV2, Lido, Clipper, // Not used: Clipper is now using PLP interface + AaveV2, + Compound, } // tslint:enable: enum-naming