diff --git a/contracts/zero-ex/CHANGELOG.json b/contracts/zero-ex/CHANGELOG.json index 1a5d467a47..163e05f6a0 100644 --- a/contracts/zero-ex/CHANGELOG.json +++ b/contracts/zero-ex/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "0.19.0", + "changes": [ + { + "note": "Add `CurveLiquidityProvider` and misc refactors", + "pr": 127 + } + ] + }, { "version": "0.18.2", "changes": [ diff --git a/contracts/zero-ex/contracts/src/external/ILiquidityProviderSandbox.sol b/contracts/zero-ex/contracts/src/external/ILiquidityProviderSandbox.sol index 7f32cea557..879d909300 100644 --- a/contracts/zero-ex/contracts/src/external/ILiquidityProviderSandbox.sol +++ b/contracts/zero-ex/contracts/src/external/ILiquidityProviderSandbox.sol @@ -20,6 +20,9 @@ pragma solidity ^0.6.5; pragma experimental ABIEncoderV2; +import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; +import "../vendor/ILiquidityProvider.sol"; + interface ILiquidityProviderSandbox { @@ -32,9 +35,9 @@ interface ILiquidityProviderSandbox { /// @param minBuyAmount The minimum acceptable amount of `outputToken` to buy. /// @param auxiliaryData Auxiliary data supplied to the `provider` contract. function executeSellTokenForToken( - address provider, - address inputToken, - address outputToken, + ILiquidityProvider provider, + IERC20TokenV06 inputToken, + IERC20TokenV06 outputToken, address recipient, uint256 minBuyAmount, bytes calldata auxiliaryData @@ -49,8 +52,8 @@ interface ILiquidityProviderSandbox { /// @param minBuyAmount The minimum acceptable amount of `outputToken` to buy. /// @param auxiliaryData Auxiliary data supplied to the `provider` contract. function executeSellEthForToken( - address provider, - address outputToken, + ILiquidityProvider provider, + IERC20TokenV06 outputToken, address recipient, uint256 minBuyAmount, bytes calldata auxiliaryData @@ -65,8 +68,8 @@ interface ILiquidityProviderSandbox { /// @param minBuyAmount The minimum acceptable amount of ETH to buy. /// @param auxiliaryData Auxiliary data supplied to the `provider` contract. function executeSellTokenForEth( - address provider, - address inputToken, + ILiquidityProvider provider, + IERC20TokenV06 inputToken, address recipient, uint256 minBuyAmount, bytes calldata auxiliaryData diff --git a/contracts/zero-ex/contracts/src/external/LiquidityProviderSandbox.sol b/contracts/zero-ex/contracts/src/external/LiquidityProviderSandbox.sol index 8c76aaca5b..867e38afd1 100644 --- a/contracts/zero-ex/contracts/src/external/LiquidityProviderSandbox.sol +++ b/contracts/zero-ex/contracts/src/external/LiquidityProviderSandbox.sol @@ -17,6 +17,7 @@ pragma experimental ABIEncoderV2; import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; import "@0x/contracts-utils/contracts/src/v06/errors/LibOwnableRichErrorsV06.sol"; +import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; import "../vendor/ILiquidityProvider.sol"; import "../vendor/v3/IERC20Bridge.sol"; import "./ILiquidityProviderSandbox.sol"; @@ -58,9 +59,9 @@ contract LiquidityProviderSandbox is /// @param minBuyAmount The minimum acceptable amount of `outputToken` to buy. /// @param auxiliaryData Auxiliary data supplied to the `provider` contract. function executeSellTokenForToken( - address provider, - address inputToken, - address outputToken, + ILiquidityProvider provider, + IERC20TokenV06 inputToken, + IERC20TokenV06 outputToken, address recipient, uint256 minBuyAmount, bytes calldata auxiliaryData @@ -69,7 +70,7 @@ contract LiquidityProviderSandbox is onlyOwner override { - ILiquidityProvider(provider).sellTokenForToken( + provider.sellTokenForToken( inputToken, outputToken, recipient, @@ -86,8 +87,8 @@ contract LiquidityProviderSandbox is /// @param minBuyAmount The minimum acceptable amount of `outputToken` to buy. /// @param auxiliaryData Auxiliary data supplied to the `provider` contract. function executeSellEthForToken( - address provider, - address outputToken, + ILiquidityProvider provider, + IERC20TokenV06 outputToken, address recipient, uint256 minBuyAmount, bytes calldata auxiliaryData @@ -96,7 +97,7 @@ contract LiquidityProviderSandbox is onlyOwner override { - ILiquidityProvider(provider).sellEthForToken( + provider.sellEthForToken( outputToken, recipient, minBuyAmount, @@ -112,8 +113,8 @@ contract LiquidityProviderSandbox is /// @param minBuyAmount The minimum acceptable amount of ETH to buy. /// @param auxiliaryData Auxiliary data supplied to the `provider` contract. function executeSellTokenForEth( - address provider, - address inputToken, + ILiquidityProvider provider, + IERC20TokenV06 inputToken, address recipient, uint256 minBuyAmount, bytes calldata auxiliaryData @@ -122,7 +123,7 @@ contract LiquidityProviderSandbox is onlyOwner override { - ILiquidityProvider(provider).sellTokenForEth( + provider.sellTokenForEth( inputToken, payable(recipient), minBuyAmount, diff --git a/contracts/zero-ex/contracts/src/features/ILiquidityProviderFeature.sol b/contracts/zero-ex/contracts/src/features/ILiquidityProviderFeature.sol index 70fb69aaee..170b5947af 100644 --- a/contracts/zero-ex/contracts/src/features/ILiquidityProviderFeature.sol +++ b/contracts/zero-ex/contracts/src/features/ILiquidityProviderFeature.sol @@ -20,10 +20,23 @@ pragma solidity ^0.6.5; pragma experimental ABIEncoderV2; +import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; +import "../vendor/ILiquidityProvider.sol"; + /// @dev Feature to swap directly with an on-chain liquidity provider. interface ILiquidityProviderFeature { + /// @dev Event for data pipeline. + event LiquidityProviderSwap( + IERC20TokenV06 inputToken, + IERC20TokenV06 outputToken, + uint256 inputTokenAmount, + uint256 outputTokenAmount, + ILiquidityProvider provider, + address recipient + ); + /// @dev Sells `sellAmount` of `inputToken` to the liquidity provider /// at the given `provider` address. /// @param inputToken The token being sold. @@ -38,9 +51,9 @@ interface ILiquidityProviderFeature { /// @param auxiliaryData Auxiliary data supplied to the `provider` contract. /// @return boughtAmount The amount of `outputToken` bought. function sellToLiquidityProvider( - address inputToken, - address outputToken, - address payable provider, + IERC20TokenV06 inputToken, + IERC20TokenV06 outputToken, + ILiquidityProvider provider, address recipient, uint256 sellAmount, uint256 minBuyAmount, diff --git a/contracts/zero-ex/contracts/src/features/LiquidityProviderFeature.sol b/contracts/zero-ex/contracts/src/features/LiquidityProviderFeature.sol index b65d4bf799..2e647e770f 100644 --- a/contracts/zero-ex/contracts/src/features/LiquidityProviderFeature.sol +++ b/contracts/zero-ex/contracts/src/features/LiquidityProviderFeature.sol @@ -23,12 +23,14 @@ pragma experimental ABIEncoderV2; import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; +import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; import "../errors/LibLiquidityProviderRichErrors.sol"; import "../external/ILiquidityProviderSandbox.sol"; import "../external/LiquidityProviderSandbox.sol"; import "../fixins/FixinCommon.sol"; import "../fixins/FixinTokenSpender.sol"; import "../migrations/LibMigrate.sol"; +import "../transformers/LibERC20Transformer.sol"; import "./IFeature.sol"; import "./ILiquidityProviderFeature.sol"; @@ -45,23 +47,11 @@ contract LiquidityProviderFeature is /// @dev Name of this feature. string public constant override FEATURE_NAME = "LiquidityProviderFeature"; /// @dev Version of this feature. - uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 2); + uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 3); - /// @dev ETH pseudo-token address. - address constant internal ETH_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; /// @dev The sandbox contract address. ILiquidityProviderSandbox public immutable sandbox; - /// @dev Event for data pipeline. - event LiquidityProviderSwap( - address inputToken, - address outputToken, - uint256 inputTokenAmount, - uint256 outputTokenAmount, - address provider, - address recipient - ); - constructor(LiquidityProviderSandbox sandbox_, bytes32 greedyTokensBloomFilter) public FixinCommon() @@ -95,9 +85,9 @@ contract LiquidityProviderFeature is /// @param auxiliaryData Auxiliary data supplied to the `provider` contract. /// @return boughtAmount The amount of `outputToken` bought. function sellToLiquidityProvider( - address inputToken, - address outputToken, - address payable provider, + IERC20TokenV06 inputToken, + IERC20TokenV06 outputToken, + ILiquidityProvider provider, address recipient, uint256 sellAmount, uint256 minBuyAmount, @@ -114,21 +104,21 @@ contract LiquidityProviderFeature is // Forward all attached ETH to the provider. if (msg.value > 0) { - provider.transfer(msg.value); + payable(address(provider)).transfer(msg.value); } - if (inputToken != ETH_TOKEN_ADDRESS) { + if (!LibERC20Transformer.isTokenETH(inputToken)) { // Transfer input ERC20 tokens to the provider. _transferERC20Tokens( - IERC20TokenV06(inputToken), + inputToken, msg.sender, - provider, + address(provider), sellAmount ); } - if (inputToken == ETH_TOKEN_ADDRESS) { - uint256 balanceBefore = IERC20TokenV06(outputToken).balanceOf(recipient); + if (LibERC20Transformer.isTokenETH(inputToken)) { + uint256 balanceBefore = outputToken.balanceOf(recipient); sandbox.executeSellEthForToken( provider, outputToken, @@ -137,7 +127,7 @@ contract LiquidityProviderFeature is auxiliaryData ); boughtAmount = IERC20TokenV06(outputToken).balanceOf(recipient).safeSub(balanceBefore); - } else if (outputToken == ETH_TOKEN_ADDRESS) { + } else if (LibERC20Transformer.isTokenETH(outputToken)) { uint256 balanceBefore = recipient.balance; sandbox.executeSellTokenForEth( provider, @@ -148,7 +138,7 @@ contract LiquidityProviderFeature is ); boughtAmount = recipient.balance.safeSub(balanceBefore); } else { - uint256 balanceBefore = IERC20TokenV06(outputToken).balanceOf(recipient); + uint256 balanceBefore = outputToken.balanceOf(recipient); sandbox.executeSellTokenForToken( provider, inputToken, @@ -157,14 +147,14 @@ contract LiquidityProviderFeature is minBuyAmount, auxiliaryData ); - boughtAmount = IERC20TokenV06(outputToken).balanceOf(recipient).safeSub(balanceBefore); + boughtAmount = outputToken.balanceOf(recipient).safeSub(balanceBefore); } if (boughtAmount < minBuyAmount) { LibLiquidityProviderRichErrors.LiquidityProviderIncompleteSellError( - provider, - outputToken, - inputToken, + address(provider), + address(outputToken), + address(inputToken), sellAmount, boughtAmount, minBuyAmount diff --git a/contracts/zero-ex/contracts/src/liquidity-providers/CurveLiquidityProvider.sol b/contracts/zero-ex/contracts/src/liquidity-providers/CurveLiquidityProvider.sol new file mode 100644 index 0000000000..c2e9cae646 --- /dev/null +++ b/contracts/zero-ex/contracts/src/liquidity-providers/CurveLiquidityProvider.sol @@ -0,0 +1,208 @@ +// 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-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; +import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; +import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; +import "../transformers/LibERC20Transformer.sol"; +import "../vendor/ILiquidityProvider.sol"; + + +contract CurveLiquidityProvider is + ILiquidityProvider +{ + using LibERC20TokenV06 for IERC20TokenV06; + using LibSafeMathV06 for uint256; + using LibRichErrorsV06 for bytes; + + struct CurveData { + address curveAddress; + bytes4 exchangeFunctionSelector; + int128 fromCoinIdx; + int128 toCoinIdx; + } + + /// @dev This contract must be payable because takers can transfer funds + /// in prior to calling the swap function. + receive() external payable {} + + /// @dev Trades `inputToken` for `outputToken`. The amount of `inputToken` + /// to sell must be transferred to the contract prior to calling this + /// function to trigger the trade. + /// @param inputToken The token being sold. + /// @param outputToken The token being bought. + /// @param recipient The recipient of the bought tokens. + /// @param minBuyAmount The minimum acceptable amount of `outputToken` to buy. + /// @param auxiliaryData Arbitrary auxiliary data supplied to the contract. + /// @return boughtAmount The amount of `outputToken` bought. + function sellTokenForToken( + IERC20TokenV06 inputToken, + IERC20TokenV06 outputToken, + address recipient, + uint256 minBuyAmount, + bytes calldata auxiliaryData + ) + external + override + returns (uint256 boughtAmount) + { + require( + !LibERC20Transformer.isTokenETH(inputToken) + && !LibERC20Transformer.isTokenETH(outputToken), + "CurveLiquidityProvider/INVALID_ARGS" + ); + boughtAmount = _executeSwap( + inputToken, + outputToken, + minBuyAmount, + abi.decode(auxiliaryData, (CurveData)) + ); + // Every pool contract currently checks this but why not. + require(boughtAmount >= minBuyAmount, "CurveLiquidityProvider/UNDERBOUGHT"); + outputToken.compatTransfer(recipient, boughtAmount); + } + + /// @dev Trades ETH for token. ETH must either be attached to this function + /// call or sent to the contract prior to calling this function to + /// trigger the trade. + /// @param outputToken The token being bought. + /// @param recipient The recipient of the bought tokens. + /// @param minBuyAmount The minimum acceptable amount of `outputToken` to buy. + /// @param auxiliaryData Arbitrary auxiliary data supplied to the contract. + /// @return boughtAmount The amount of `outputToken` bought. + function sellEthForToken( + IERC20TokenV06 outputToken, + address recipient, + uint256 minBuyAmount, + bytes calldata auxiliaryData + ) + external + payable + override + returns (uint256 boughtAmount) + { + require( + !LibERC20Transformer.isTokenETH(outputToken), + "CurveLiquidityProvider/INVALID_ARGS" + ); + boughtAmount = _executeSwap( + LibERC20Transformer.ETH_TOKEN, + outputToken, + minBuyAmount, + abi.decode(auxiliaryData, (CurveData)) + ); + // Every pool contract currently checks this but why not. + require(boughtAmount >= minBuyAmount, "CurveLiquidityProvider/UNDERBOUGHT"); + outputToken.compatTransfer(recipient, boughtAmount); + } + + /// @dev Trades token for ETH. The token must be sent to the contract prior + /// to calling this function to trigger the trade. + /// @param inputToken The token being sold. + /// @param recipient The recipient of the bought tokens. + /// @param minBuyAmount The minimum acceptable amount of ETH to buy. + /// @param auxiliaryData Arbitrary auxiliary data supplied to the contract. + /// @return boughtAmount The amount of ETH bought. + function sellTokenForEth( + IERC20TokenV06 inputToken, + address payable recipient, + uint256 minBuyAmount, + bytes calldata auxiliaryData + ) + external + override + returns (uint256 boughtAmount) + { + require( + !LibERC20Transformer.isTokenETH(inputToken), + "CurveLiquidityProvider/INVALID_ARGS" + ); + boughtAmount = _executeSwap( + inputToken, + LibERC20Transformer.ETH_TOKEN, + minBuyAmount, + abi.decode(auxiliaryData, (CurveData)) + ); + // Every pool contract currently checks this but why not. + require(boughtAmount >= minBuyAmount, "CurveLiquidityProvider/UNDERBOUGHT"); + recipient.transfer(boughtAmount); + } + + /// @dev Quotes the amount of `outputToken` that would be obtained by + /// selling `sellAmount` of `inputToken`. + function getSellQuote( + IERC20TokenV06 /* inputToken */, + IERC20TokenV06 /* outputToken */, + uint256 /* sellAmount */ + ) + external + view + override + returns (uint256) + { + revert("CurveLiquidityProvider/NOT_IMPLEMENTED"); + } + + /// @dev Perform the swap against the curve pool. Handles any combination of + /// tokens + function _executeSwap( + IERC20TokenV06 inputToken, + IERC20TokenV06 outputToken, + uint256 minBuyAmount, + CurveData memory data + ) + private + returns (uint256 boughtAmount) + { + uint256 sellAmount = + LibERC20Transformer.getTokenBalanceOf(inputToken, address(this)); + if (!LibERC20Transformer.isTokenETH(inputToken)) { + inputToken.approveIfBelow(data.curveAddress, sellAmount); + } + + (bool success, bytes memory resultData) = + data.curveAddress.call + { value: LibERC20Transformer.isTokenETH(inputToken) ? sellAmount : 0 } + (abi.encodeWithSelector( + data.exchangeFunctionSelector, + data.fromCoinIdx, + data.toCoinIdx, + // dx + sellAmount, + // min dy + minBuyAmount + )); + if (!success) { + resultData.rrevert(); + } + if (resultData.length == 32) { + // Pool returned a boughtAmount + boughtAmount = abi.decode(resultData, (uint256)); + } else { + // Not all pool contracts return a `boughtAmount`, so we return + // our balance of the output token if it wasn't returned. + boughtAmount = LibERC20Transformer + .getTokenBalanceOf(outputToken, address(this)); + } + } +} diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinZeroExBridge.sol b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinZeroExBridge.sol index 7903266079..584be64cd4 100644 --- a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinZeroExBridge.sol +++ b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinZeroExBridge.sol @@ -47,8 +47,8 @@ contract MixinZeroExBridge { sellAmount ); boughtAmount = provider.sellTokenForToken( - address(sellToken), - address(buyToken), + sellToken, + buyToken, address(this), // recipient 1, // minBuyAmount lpData diff --git a/contracts/zero-ex/contracts/src/vendor/ILiquidityProvider.sol b/contracts/zero-ex/contracts/src/vendor/ILiquidityProvider.sol index 1580fc5a75..7d8749c561 100644 --- a/contracts/zero-ex/contracts/src/vendor/ILiquidityProvider.sol +++ b/contracts/zero-ex/contracts/src/vendor/ILiquidityProvider.sol @@ -19,6 +19,9 @@ pragma solidity ^0.6.5; +import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; + + interface ILiquidityProvider { /// @dev Trades `inputToken` for `outputToken`. The amount of `inputToken` @@ -31,8 +34,8 @@ interface ILiquidityProvider { /// @param auxiliaryData Arbitrary auxiliary data supplied to the contract. /// @return boughtAmount The amount of `outputToken` bought. function sellTokenForToken( - address inputToken, - address outputToken, + IERC20TokenV06 inputToken, + IERC20TokenV06 outputToken, address recipient, uint256 minBuyAmount, bytes calldata auxiliaryData @@ -49,7 +52,7 @@ interface ILiquidityProvider { /// @param auxiliaryData Arbitrary auxiliary data supplied to the contract. /// @return boughtAmount The amount of `outputToken` bought. function sellEthForToken( - address outputToken, + IERC20TokenV06 outputToken, address recipient, uint256 minBuyAmount, bytes calldata auxiliaryData @@ -66,7 +69,7 @@ interface ILiquidityProvider { /// @param auxiliaryData Arbitrary auxiliary data supplied to the contract. /// @return boughtAmount The amount of ETH bought. function sellTokenForEth( - address inputToken, + IERC20TokenV06 inputToken, address payable recipient, uint256 minBuyAmount, bytes calldata auxiliaryData @@ -83,8 +86,8 @@ interface ILiquidityProvider { /// @param sellAmount Amount of `inputToken` to sell. /// @return outputTokenAmount Amount of `outputToken` that would be obtained. function getSellQuote( - address inputToken, - address outputToken, + IERC20TokenV06 inputToken, + IERC20TokenV06 outputToken, uint256 sellAmount ) external diff --git a/contracts/zero-ex/contracts/test/TestCurve.sol b/contracts/zero-ex/contracts/test/TestCurve.sol new file mode 100644 index 0000000000..875a79b59d --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestCurve.sol @@ -0,0 +1,100 @@ +// 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/IERC20TokenV06.sol"; +import "./TestMintableERC20Token.sol"; + +contract TestCurve { + + event CurveCalled( + uint256 value, + bytes4 selector, + int128 fromCoinIdx, + int128 toCoinIdx, + uint256 sellAmount, + uint256 minBuyAmount + ); + + // The lower 16 bits of the selector are reserved for flags. + bytes4 public constant BASE_SWAP_SELECTOR = 0x12340000; + bytes4 public constant RETURN_BOUGHT_AMOUNT_SELECTOR_FLAG = 0x00000001; + + int128 public constant SELL_TOKEN_COIN_IDX = 0; + int128 public constant BUY_TOKEN_COIN_IDX = 1; + int128 public constant ETH_COIN_IDX = 2; + + uint256 public buyAmount; + IERC20TokenV06 public sellToken; + TestMintableERC20Token public buyToken; + + constructor( + IERC20TokenV06 sellToken_, + TestMintableERC20Token buyToken_, + uint256 buyAmount_ + ) + public + payable + { + sellToken = sellToken_; + buyToken = buyToken_; + buyAmount = buyAmount_; + } + + receive() external payable {} + + fallback() external payable { + bytes4 selector = abi.decode(msg.data, (bytes4)); + bool shouldReturnBoughtAmount = + (selector & RETURN_BOUGHT_AMOUNT_SELECTOR_FLAG) != 0x0; + bytes4 baseSelector = selector & 0xffff0000; + require(baseSelector == BASE_SWAP_SELECTOR, "TestCurve/REVERT"); + ( + int128 fromCoinIdx, + int128 toCoinIdx, + uint256 sellAmount, + uint256 minBuyAmount + ) = abi.decode(msg.data[4:], (int128, int128, uint256, uint256)); + if (fromCoinIdx == SELL_TOKEN_COIN_IDX) { + sellToken.transferFrom(msg.sender, address(this), sellAmount); + } + if (toCoinIdx == BUY_TOKEN_COIN_IDX) { + buyToken.mint(msg.sender, buyAmount); + } else if (toCoinIdx == ETH_COIN_IDX) { + msg.sender.transfer(buyAmount); + } + emit CurveCalled( + msg.value, + selector, + fromCoinIdx, + toCoinIdx, + sellAmount, + minBuyAmount + ); + if (shouldReturnBoughtAmount) { + assembly { + mstore(0, sload(buyAmount_slot)) + return(0, 32) + } + } + assembly { return(0, 0) } + } +} diff --git a/contracts/zero-ex/contracts/test/TestLiquidityProvider.sol b/contracts/zero-ex/contracts/test/TestLiquidityProvider.sol index 7f9ab9629d..31dfb3cb37 100644 --- a/contracts/zero-ex/contracts/test/TestLiquidityProvider.sol +++ b/contracts/zero-ex/contracts/test/TestLiquidityProvider.sol @@ -74,7 +74,7 @@ contract TestLiquidityProvider { bytes calldata // auxiliaryData ) external - returns (uint256 boughtAmount) + returns (uint256) { emit SellTokenForToken( inputToken, @@ -98,7 +98,7 @@ contract TestLiquidityProvider { bytes calldata // auxiliaryData ) external - returns (uint256 boughtAmount) + returns (uint256) { emit SellEthForToken( outputToken, @@ -121,7 +121,7 @@ contract TestLiquidityProvider { bytes calldata // auxiliaryData ) external - returns (uint256 boughtAmount) + returns (uint256) { emit SellTokenForEth( inputToken, diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index 39212c0daa..7e82e923f8 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -43,7 +43,7 @@ "config": { "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature,ILiquidityProviderFeature,NativeOrdersFeature,INativeOrdersFeature,FeeCollectorController,FeeCollector", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|BridgeSource|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IFeature|IFlashWallet|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|INativeOrdersFeature|IOwnableFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinBalancer|MixinBancor|MixinCoFiX|MixinCryptoCom|MixinCurve|MixinDodo|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinShell|MixinSushiswap|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|NativeOrdersFeature|OwnableFeature|PayTakerTransformer|PermissionlessTransformerDeployer|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestDelegateCaller|TestFeeCollectorController|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFixinProtocolFees|TestFixinTokenSpender|TestFullMigration|TestInitialMigration|TestLibNativeOrder|TestLibSignature|TestLiquidityProvider|TestMetaTransactionsNativeOrdersFeature|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestNativeOrdersFeature|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx|ZeroExOptimized).json" + "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|BridgeSource|CurveLiquidityProvider|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IFeature|IFlashWallet|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|INativeOrdersFeature|IOwnableFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinBalancer|MixinBancor|MixinCoFiX|MixinCryptoCom|MixinCurve|MixinDodo|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinShell|MixinSushiswap|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|NativeOrdersFeature|OwnableFeature|PayTakerTransformer|PermissionlessTransformerDeployer|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestCurve|TestDelegateCaller|TestFeeCollectorController|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFixinProtocolFees|TestFixinTokenSpender|TestFullMigration|TestInitialMigration|TestLibNativeOrder|TestLibSignature|TestLiquidityProvider|TestMetaTransactionsNativeOrdersFeature|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestNativeOrdersFeature|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx|ZeroExOptimized).json" }, "repository": { "type": "git", diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index 9f6270d64a..080ba54203 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -10,6 +10,7 @@ import * as AllowanceTarget from '../test/generated-artifacts/AllowanceTarget.js import * as BootstrapFeature from '../test/generated-artifacts/BootstrapFeature.json'; import * as BridgeAdapter from '../test/generated-artifacts/BridgeAdapter.json'; import * as BridgeSource from '../test/generated-artifacts/BridgeSource.json'; +import * as CurveLiquidityProvider from '../test/generated-artifacts/CurveLiquidityProvider.json'; import * as FeeCollector from '../test/generated-artifacts/FeeCollector.json'; import * as FeeCollectorController from '../test/generated-artifacts/FeeCollectorController.json'; import * as FillQuoteTransformer from '../test/generated-artifacts/FillQuoteTransformer.json'; @@ -93,6 +94,7 @@ import * as PermissionlessTransformerDeployer from '../test/generated-artifacts/ import * as SimpleFunctionRegistryFeature from '../test/generated-artifacts/SimpleFunctionRegistryFeature.json'; import * as TestBridge from '../test/generated-artifacts/TestBridge.json'; import * as TestCallTarget from '../test/generated-artifacts/TestCallTarget.json'; +import * as TestCurve from '../test/generated-artifacts/TestCurve.json'; import * as TestDelegateCaller from '../test/generated-artifacts/TestDelegateCaller.json'; import * as TestFeeCollectorController from '../test/generated-artifacts/TestFeeCollectorController.json'; import * as TestFillQuoteTransformerBridge from '../test/generated-artifacts/TestFillQuoteTransformerBridge.json'; @@ -186,6 +188,7 @@ export const artifacts = { FixinProtocolFees: FixinProtocolFees as ContractArtifact, FixinReentrancyGuard: FixinReentrancyGuard as ContractArtifact, FixinTokenSpender: FixinTokenSpender as ContractArtifact, + CurveLiquidityProvider: CurveLiquidityProvider as ContractArtifact, FullMigration: FullMigration as ContractArtifact, InitialMigration: InitialMigration as ContractArtifact, LibBootstrap: LibBootstrap as ContractArtifact, @@ -231,6 +234,7 @@ export const artifacts = { ITestSimpleFunctionRegistryFeature: ITestSimpleFunctionRegistryFeature as ContractArtifact, TestBridge: TestBridge as ContractArtifact, TestCallTarget: TestCallTarget as ContractArtifact, + TestCurve: TestCurve as ContractArtifact, TestDelegateCaller: TestDelegateCaller as ContractArtifact, TestFeeCollectorController: TestFeeCollectorController as ContractArtifact, TestFillQuoteTransformerBridge: TestFillQuoteTransformerBridge as ContractArtifact, diff --git a/contracts/zero-ex/test/liqudity-providers/curve_test.ts b/contracts/zero-ex/test/liqudity-providers/curve_test.ts new file mode 100644 index 0000000000..351af5cc76 --- /dev/null +++ b/contracts/zero-ex/test/liqudity-providers/curve_test.ts @@ -0,0 +1,294 @@ +import { blockchainTests, constants, expect, getRandomInteger, verifyEventsFromLogs } from '@0x/contracts-test-utils'; +import { BigNumber, hexUtils } from '@0x/utils'; + +import { artifacts } from '../artifacts'; +import { CurveLiquidityProviderContract, TestCurveContract, TestMintableERC20TokenContract } from '../wrappers'; + +blockchainTests.resets('CurveLiquidityProvider feature', env => { + let lp: CurveLiquidityProviderContract; + let sellToken: TestMintableERC20TokenContract; + let buyToken: TestMintableERC20TokenContract; + let testCurve: TestCurveContract; + let owner: string; + let taker: string; + const RECIPIENT = hexUtils.random(20); + const SELL_AMOUNT = getRandomInteger('1e6', '1e18'); + const BUY_AMOUNT = getRandomInteger('1e6', '10e18'); + const REVERTING_SELECTOR = '0xdeaddead'; + const SWAP_SELECTOR = '0x12340000'; + const SWAP_WITH_RETURN_SELECTOR = '0x12340001'; + const ETH_TOKEN_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; + const SELL_TOKEN_COIN_IDX = 0; + const BUY_TOKEN_COIN_IDX = 1; + const ETH_COIN_IDX = 2; + const { ZERO_AMOUNT } = constants; + + before(async () => { + [owner, taker] = await env.getAccountAddressesAsync(); + [sellToken, buyToken] = await Promise.all( + new Array(2) + .fill(0) + .map(async () => + TestMintableERC20TokenContract.deployFrom0xArtifactAsync( + artifacts.TestMintableERC20Token, + env.provider, + env.txDefaults, + artifacts, + ), + ), + ); + testCurve = await TestCurveContract.deployFrom0xArtifactAsync( + artifacts.TestCurve, + env.provider, + { ...env.txDefaults, value: BUY_AMOUNT }, + artifacts, + sellToken.address, + buyToken.address, + BUY_AMOUNT, + ); + lp = await CurveLiquidityProviderContract.deployFrom0xArtifactAsync( + artifacts.CurveLiquidityProvider, + env.provider, + { ...env.txDefaults, from: taker }, + artifacts, + ); + }); + + interface CurveDataFields { + curveAddress: string; + exchangeFunctionSelector: string; + fromCoinIdx: number; + toCoinIdx: number; + } + + async function fundProviderContractAsync(fromCoinIdx: number, amount: BigNumber = SELL_AMOUNT): Promise { + if (fromCoinIdx === SELL_TOKEN_COIN_IDX) { + await sellToken.mint(lp.address, SELL_AMOUNT).awaitTransactionSuccessAsync(); + } else { + await env.web3Wrapper.awaitTransactionSuccessAsync( + await env.web3Wrapper.sendTransactionAsync({ + from: taker, + to: lp.address, + value: SELL_AMOUNT, + }), + ); + } + } + + function encodeCurveData(fields: Partial = {}): string { + const _fields = { + curveAddress: testCurve.address, + exchangeFunctionSelector: SWAP_SELECTOR, + fromCoinIdx: SELL_TOKEN_COIN_IDX, + toCoinIdx: BUY_TOKEN_COIN_IDX, + ...fields, + }; + return hexUtils.concat( + hexUtils.leftPad(_fields.curveAddress), + hexUtils.rightPad(_fields.exchangeFunctionSelector), + hexUtils.leftPad(_fields.fromCoinIdx), + hexUtils.leftPad(_fields.toCoinIdx), + ); + } + + it('can swap ERC20->ERC20', async () => { + await fundProviderContractAsync(SELL_TOKEN_COIN_IDX); + const call = lp.sellTokenForToken( + sellToken.address, + buyToken.address, + RECIPIENT, + BUY_AMOUNT, + encodeCurveData(), + ); + const boughtAmount = await call.callAsync(); + const { logs } = await call.awaitTransactionSuccessAsync(); + expect(boughtAmount).to.bignumber.eq(BUY_AMOUNT); + expect(await buyToken.balanceOf(RECIPIENT).callAsync()).to.bignumber.eq(BUY_AMOUNT); + verifyEventsFromLogs( + logs, + [ + { + value: ZERO_AMOUNT, + selector: SWAP_SELECTOR, + fromCoinIdx: new BigNumber(SELL_TOKEN_COIN_IDX), + toCoinIdx: new BigNumber(BUY_TOKEN_COIN_IDX), + sellAmount: SELL_AMOUNT, + minBuyAmount: BUY_AMOUNT, + }, + ], + 'CurveCalled', + ); + }); + + it('can swap ERC20->ETH', async () => { + await fundProviderContractAsync(SELL_TOKEN_COIN_IDX); + const call = lp.sellTokenForEth( + sellToken.address, + RECIPIENT, + BUY_AMOUNT, + encodeCurveData({ toCoinIdx: ETH_COIN_IDX }), + ); + const boughtAmount = await call.callAsync(); + const { logs } = await call.awaitTransactionSuccessAsync(); + expect(boughtAmount).to.bignumber.eq(BUY_AMOUNT); + expect(await env.web3Wrapper.getBalanceInWeiAsync(RECIPIENT)).to.bignumber.eq(BUY_AMOUNT); + verifyEventsFromLogs( + logs, + [ + { + value: ZERO_AMOUNT, + selector: SWAP_SELECTOR, + fromCoinIdx: new BigNumber(SELL_TOKEN_COIN_IDX), + toCoinIdx: new BigNumber(ETH_COIN_IDX), + sellAmount: SELL_AMOUNT, + minBuyAmount: BUY_AMOUNT, + }, + ], + 'CurveCalled', + ); + }); + + it('can swap ETH->ERC20', async () => { + await fundProviderContractAsync(ETH_COIN_IDX); + const call = lp.sellEthForToken( + buyToken.address, + RECIPIENT, + BUY_AMOUNT, + encodeCurveData({ fromCoinIdx: ETH_COIN_IDX }), + ); + const boughtAmount = await call.callAsync(); + const { logs } = await call.awaitTransactionSuccessAsync(); + expect(boughtAmount).to.bignumber.eq(BUY_AMOUNT); + expect(await buyToken.balanceOf(RECIPIENT).callAsync()).to.bignumber.eq(BUY_AMOUNT); + verifyEventsFromLogs( + logs, + [ + { + value: SELL_AMOUNT, + selector: SWAP_SELECTOR, + fromCoinIdx: new BigNumber(ETH_COIN_IDX), + toCoinIdx: new BigNumber(BUY_TOKEN_COIN_IDX), + sellAmount: SELL_AMOUNT, + minBuyAmount: BUY_AMOUNT, + }, + ], + 'CurveCalled', + ); + }); + + it('can swap ETH->ERC20 with attached ETH', async () => { + const call = lp.sellEthForToken( + buyToken.address, + RECIPIENT, + BUY_AMOUNT, + encodeCurveData({ fromCoinIdx: ETH_COIN_IDX }), + ); + const boughtAmount = await call.callAsync({ value: SELL_AMOUNT }); + const { logs } = await call.awaitTransactionSuccessAsync({ value: SELL_AMOUNT }); + expect(boughtAmount).to.bignumber.eq(BUY_AMOUNT); + expect(await buyToken.balanceOf(RECIPIENT).callAsync()).to.bignumber.eq(BUY_AMOUNT); + verifyEventsFromLogs( + logs, + [ + { + value: SELL_AMOUNT, + selector: SWAP_SELECTOR, + fromCoinIdx: new BigNumber(ETH_COIN_IDX), + toCoinIdx: new BigNumber(BUY_TOKEN_COIN_IDX), + sellAmount: SELL_AMOUNT, + minBuyAmount: BUY_AMOUNT, + }, + ], + 'CurveCalled', + ); + }); + + it('can swap with a pool that returns bought amount', async () => { + await fundProviderContractAsync(SELL_TOKEN_COIN_IDX); + const call = lp.sellTokenForToken( + sellToken.address, + buyToken.address, + RECIPIENT, + BUY_AMOUNT, + encodeCurveData({ exchangeFunctionSelector: SWAP_WITH_RETURN_SELECTOR }), + ); + const boughtAmount = await call.callAsync(); + const { logs } = await call.awaitTransactionSuccessAsync(); + expect(boughtAmount).to.bignumber.eq(BUY_AMOUNT); + expect(await buyToken.balanceOf(RECIPIENT).callAsync()).to.bignumber.eq(BUY_AMOUNT); + verifyEventsFromLogs( + logs, + [ + { + value: ZERO_AMOUNT, + selector: SWAP_WITH_RETURN_SELECTOR, + fromCoinIdx: new BigNumber(SELL_TOKEN_COIN_IDX), + toCoinIdx: new BigNumber(BUY_TOKEN_COIN_IDX), + sellAmount: SELL_AMOUNT, + minBuyAmount: BUY_AMOUNT, + }, + ], + 'CurveCalled', + ); + }); + + it('reverts if pool reverts', async () => { + await fundProviderContractAsync(SELL_TOKEN_COIN_IDX); + const call = lp.sellTokenForToken( + sellToken.address, + buyToken.address, + RECIPIENT, + BUY_AMOUNT, + encodeCurveData({ exchangeFunctionSelector: REVERTING_SELECTOR }), + ); + return expect(call.callAsync()).to.revertWith('TestCurve/REVERT'); + }); + + it('reverts if underbought', async () => { + await fundProviderContractAsync(SELL_TOKEN_COIN_IDX); + const call = lp.sellTokenForToken( + sellToken.address, + buyToken.address, + RECIPIENT, + BUY_AMOUNT.plus(1), + encodeCurveData(), + ); + return expect(call.callAsync()).to.revertWith('CurveLiquidityProvider/UNDERBOUGHT'); + }); + + it('reverts if ERC20->ERC20 receives an ETH input token', async () => { + await fundProviderContractAsync(ETH_COIN_IDX); + const call = lp.sellTokenForToken( + ETH_TOKEN_ADDRESS, + buyToken.address, + RECIPIENT, + BUY_AMOUNT, + encodeCurveData(), + ); + return expect(call.callAsync()).to.revertWith('CurveLiquidityProvider/INVALID_ARGS'); + }); + + it('reverts if ERC20->ERC20 receives an ETH output token', async () => { + await fundProviderContractAsync(SELL_TOKEN_COIN_IDX); + const call = lp.sellTokenForToken( + sellToken.address, + ETH_TOKEN_ADDRESS, + RECIPIENT, + BUY_AMOUNT, + encodeCurveData(), + ); + return expect(call.callAsync()).to.revertWith('CurveLiquidityProvider/INVALID_ARGS'); + }); + + it('reverts if ERC20->ETH receives an ETH input token', async () => { + await fundProviderContractAsync(SELL_TOKEN_COIN_IDX); + const call = lp.sellTokenForEth(ETH_TOKEN_ADDRESS, RECIPIENT, BUY_AMOUNT, encodeCurveData()); + return expect(call.callAsync()).to.revertWith('CurveLiquidityProvider/INVALID_ARGS'); + }); + + it('reverts if ETH->ERC20 receives an ETH output token', async () => { + await fundProviderContractAsync(ETH_COIN_IDX); + const call = lp.sellEthForToken(ETH_TOKEN_ADDRESS, RECIPIENT, BUY_AMOUNT, encodeCurveData()); + return expect(call.callAsync()).to.revertWith('CurveLiquidityProvider/INVALID_ARGS'); + }); +}); diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index c7d7c4cb3c..f7e1953c95 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -8,6 +8,7 @@ export * from '../test/generated-wrappers/allowance_target'; export * from '../test/generated-wrappers/bootstrap_feature'; export * from '../test/generated-wrappers/bridge_adapter'; export * from '../test/generated-wrappers/bridge_source'; +export * from '../test/generated-wrappers/curve_liquidity_provider'; export * from '../test/generated-wrappers/fee_collector'; export * from '../test/generated-wrappers/fee_collector_controller'; export * from '../test/generated-wrappers/fill_quote_transformer'; @@ -91,6 +92,7 @@ export * from '../test/generated-wrappers/permissionless_transformer_deployer'; export * from '../test/generated-wrappers/simple_function_registry_feature'; export * from '../test/generated-wrappers/test_bridge'; export * from '../test/generated-wrappers/test_call_target'; +export * from '../test/generated-wrappers/test_curve'; export * from '../test/generated-wrappers/test_delegate_caller'; export * from '../test/generated-wrappers/test_fee_collector_controller'; export * from '../test/generated-wrappers/test_fill_quote_transformer_bridge'; diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index 04edc1792a..1480799acc 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -36,6 +36,7 @@ "test/generated-artifacts/BootstrapFeature.json", "test/generated-artifacts/BridgeAdapter.json", "test/generated-artifacts/BridgeSource.json", + "test/generated-artifacts/CurveLiquidityProvider.json", "test/generated-artifacts/FeeCollector.json", "test/generated-artifacts/FeeCollectorController.json", "test/generated-artifacts/FillQuoteTransformer.json", @@ -119,6 +120,7 @@ "test/generated-artifacts/SimpleFunctionRegistryFeature.json", "test/generated-artifacts/TestBridge.json", "test/generated-artifacts/TestCallTarget.json", + "test/generated-artifacts/TestCurve.json", "test/generated-artifacts/TestDelegateCaller.json", "test/generated-artifacts/TestFeeCollectorController.json", "test/generated-artifacts/TestFillQuoteTransformerBridge.json", diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index f45ec8c975..342fb382f8 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -5,6 +5,14 @@ { "note": "Filter MultiHop where second source is not present", "pr": 138 + }, + { + "note": "Add CurveLiquidityProvider \"direct\" route to EP consumer.", + "pr": 127 + }, + { + "note": "Fix compiler error on `ILiquidityProvider` call", + "pr": 127 } ] }, diff --git a/packages/asset-swapper/contracts/src/LiquidityProviderSampler.sol b/packages/asset-swapper/contracts/src/LiquidityProviderSampler.sol index 52bab8da40..91debd823e 100644 --- a/packages/asset-swapper/contracts/src/LiquidityProviderSampler.sol +++ b/packages/asset-swapper/contracts/src/LiquidityProviderSampler.sol @@ -58,7 +58,11 @@ contract LiquidityProviderSampler is try ILiquidityProvider(providerAddress).getSellQuote {gas: DEFAULT_CALL_GAS} - (takerToken, makerToken, takerTokenAmounts[i]) + ( + IERC20TokenV06(takerToken), + IERC20TokenV06(makerToken), + takerTokenAmounts[i] + ) returns (uint256 amount) { makerTokenAmounts[i] = amount; diff --git a/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts b/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts index b1c757429f..7e21e60b8f 100644 --- a/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts +++ b/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts @@ -2,6 +2,7 @@ import { ContractAddresses } from '@0x/contract-addresses'; import { IZeroExContract } from '@0x/contract-wrappers'; import { encodeAffiliateFeeTransformerData, + encodeCurveLiquidityProviderData, encodeFillQuoteTransformerData, encodePayTakerTransformerData, encodeWethTransformerData, @@ -29,11 +30,13 @@ import { SwapQuoteGetOutputOpts, } from '../types'; import { assert } from '../utils/assert'; +import { CURVE_LIQUIDITY_PROVIDER_BY_CHAIN_ID } from '../utils/market_operation_utils/constants'; import { createBridgeDataForBridgeOrder, getERC20BridgeSourceToBridgeSource, } from '../utils/market_operation_utils/orders'; import { + CurveFillData, ERC20BridgeSource, LiquidityProviderFillData, NativeLimitOrderFillData, @@ -166,6 +169,31 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { }; } + if (isDirectSwapCompatible(quote, optsWithDefaults, [ERC20BridgeSource.Curve, ERC20BridgeSource.Swerve])) { + const fillData = quote.orders[0].fills[0].fillData as CurveFillData; + return { + calldataHexString: this._exchangeProxy + .sellToLiquidityProvider( + isFromETH ? ETH_TOKEN_ADDRESS : sellToken, + isToETH ? ETH_TOKEN_ADDRESS : buyToken, + CURVE_LIQUIDITY_PROVIDER_BY_CHAIN_ID[this.chainId], + NULL_ADDRESS, + sellAmount, + minBuyAmount, + encodeCurveLiquidityProviderData({ + curveAddress: fillData.pool.poolAddress, + exchangeFunctionSelector: fillData.pool.exchangeFunctionSelector, + fromCoinIdx: new BigNumber(fillData.fromTokenIdx), + toCoinIdx: new BigNumber(fillData.toTokenIdx), + }), + ) + .getABIEncodedTransactionData(), + ethAmount: isFromETH ? sellAmount : ZERO_AMOUNT, + toAddress: this._exchangeProxy.address, + allowanceTarget: this.contractAddresses.exchangeProxyAllowanceTarget, + }; + } + // Build up the transforms. const transforms = []; if (isFromETH) { 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 0124cc44da..39495c4cbf 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts @@ -422,6 +422,12 @@ export const MAINNET_MOONISWAP_V2_1_REGISTRY = '0xbaf9a5d4b0052359326a6cdab54bab export const MAINNET_DODO_HELPER = '0x533da777aedce766ceae696bf90f8541a4ba80eb'; +export const CURVE_LIQUIDITY_PROVIDER_BY_CHAIN_ID: { [id: string]: string } = { + '1': '0xe3a207e4225d459095491ea75d30b31968dff887', + '3': '0xe3a207e4225d459095491ea75d30b31968dff887', + '1337': '0xe3a207e4225d459095491ea75d30b31968dff887', +}; + export const MAINNET_SHELL_POOLS = { StableCoins: { poolAddress: '0x8f26d7bab7a73309141a291525c965ecdea7bf42', diff --git a/packages/protocol-utils/CHANGELOG.json b/packages/protocol-utils/CHANGELOG.json index 5bc8e969db..3d673472a5 100644 --- a/packages/protocol-utils/CHANGELOG.json +++ b/packages/protocol-utils/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "1.3.0", + "changes": [ + { + "note": "Add VIP utils", + "pr": 127 + } + ] + }, { "version": "1.2.0", "changes": [ diff --git a/packages/protocol-utils/src/index.ts b/packages/protocol-utils/src/index.ts index 15466ec4f3..a368e8fa72 100644 --- a/packages/protocol-utils/src/index.ts +++ b/packages/protocol-utils/src/index.ts @@ -8,3 +8,4 @@ export * from './meta_transactions'; export * from './signature_utils'; export * from './transformer_utils'; export * from './constants'; +export * from './vip_utils'; diff --git a/packages/protocol-utils/src/vip_utils.ts b/packages/protocol-utils/src/vip_utils.ts new file mode 100644 index 0000000000..85909b2fc7 --- /dev/null +++ b/packages/protocol-utils/src/vip_utils.ts @@ -0,0 +1,27 @@ +import { AbiEncoder, BigNumber } from '@0x/utils'; + +export interface CurveLiquidityProviderData { + curveAddress: string; + exchangeFunctionSelector: string; + fromCoinIdx: BigNumber; + toCoinIdx: BigNumber; +} + +export const curveLiquidityProviderDataEncoder = AbiEncoder.create([ + { name: 'curveAddress', type: 'address' }, + { name: 'exchangeFunctionSelector', type: 'bytes4' }, + { name: 'fromCoinIdx', type: 'int128' }, + { name: 'toCoinIdx', type: 'int128' }, +]); + +/** + * Encode data for the curve liquidity provider contract. + */ +export function encodeCurveLiquidityProviderData(data: CurveLiquidityProviderData): string { + return curveLiquidityProviderDataEncoder.encode([ + data.curveAddress, + data.exchangeFunctionSelector, + data.fromCoinIdx, + data.toCoinIdx, + ]); +}