From 78e3cd39d189202d4106963366c3d7dd3ca857d6 Mon Sep 17 00:00:00 2001 From: Michael Zhu Date: Fri, 28 Aug 2020 10:37:17 -0700 Subject: [PATCH] `@0x/contracts-zero-ex`: Add LiquidityProviderFeature contracts --- contracts/zero-ex/contracts/src/IZeroEx.sol | 4 +- .../errors/LibLiquidityProviderRichErrors.sol | 63 ++++ .../features/ILiquidityProviderFeature.sol | 66 ++++ .../src/features/LiquidityProviderFeature.sol | 204 +++++++++++++ .../src/features/MetaTransactionsFeature.sol | 4 +- .../storage/LibLiquidityProviderStorage.sol | 45 +++ .../contracts/src/storage/LibStorage.sol | 3 +- contracts/zero-ex/package.json | 2 +- contracts/zero-ex/test/artifacts.ts | 8 + .../test/features/liquidity_provider_test.ts | 48 +++ contracts/zero-ex/test/wrappers.ts | 4 + contracts/zero-ex/tsconfig.json | 4 + .../contract-artifacts/artifacts/IZeroEx.json | 60 ++++ .../src/generated-wrappers/i_zero_ex.ts | 286 ++++++++++++++++++ 14 files changed, 796 insertions(+), 5 deletions(-) create mode 100644 contracts/zero-ex/contracts/src/errors/LibLiquidityProviderRichErrors.sol create mode 100644 contracts/zero-ex/contracts/src/features/ILiquidityProviderFeature.sol create mode 100644 contracts/zero-ex/contracts/src/features/LiquidityProviderFeature.sol create mode 100644 contracts/zero-ex/contracts/src/storage/LibLiquidityProviderStorage.sol create mode 100644 contracts/zero-ex/test/features/liquidity_provider_test.ts diff --git a/contracts/zero-ex/contracts/src/IZeroEx.sol b/contracts/zero-ex/contracts/src/IZeroEx.sol index b31371a02f..8a84a0425a 100644 --- a/contracts/zero-ex/contracts/src/IZeroEx.sol +++ b/contracts/zero-ex/contracts/src/IZeroEx.sol @@ -26,6 +26,7 @@ import "./features/ISignatureValidatorFeature.sol"; import "./features/ITransformERC20Feature.sol"; import "./features/IMetaTransactionsFeature.sol"; import "./features/IUniswapFeature.sol"; +import "./features/ILiquidityProviderFeature.sol"; /// @dev Interface for a fully featured Exchange Proxy. @@ -36,7 +37,8 @@ interface IZeroEx is ISignatureValidatorFeature, ITransformERC20Feature, IMetaTransactionsFeature, - IUniswapFeature + IUniswapFeature, + ILiquidityProviderFeature { // solhint-disable state-visibility diff --git a/contracts/zero-ex/contracts/src/errors/LibLiquidityProviderRichErrors.sol b/contracts/zero-ex/contracts/src/errors/LibLiquidityProviderRichErrors.sol new file mode 100644 index 0000000000..177df23a08 --- /dev/null +++ b/contracts/zero-ex/contracts/src/errors/LibLiquidityProviderRichErrors.sol @@ -0,0 +1,63 @@ +/* + + Copyright 2020 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; + + +library LibLiquidityProviderRichErrors { + + // solhint-disable func-name-mixedcase + + function LiquidityProviderIncompleteSellError( + address providerAddress, + address makerToken, + address takerToken, + uint256 sellAmount, + uint256 boughtAmount, + uint256 minBuyAmount + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("LiquidityProviderIncompleteSellError(address,address,address,uint256,uint256,uint256)")), + providerAddress, + makerToken, + takerToken, + sellAmount, + boughtAmount, + minBuyAmount + ); + } + + function NoLiquidityProviderForMarketError( + address xAsset, + address yAsset + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("NoLiquidityProviderForMarketError(address,address)")), + xAsset, + yAsset + ); + } +} diff --git a/contracts/zero-ex/contracts/src/features/ILiquidityProviderFeature.sol b/contracts/zero-ex/contracts/src/features/ILiquidityProviderFeature.sol new file mode 100644 index 0000000000..02792df8f6 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/ILiquidityProviderFeature.sol @@ -0,0 +1,66 @@ +/* + + Copyright 2020 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; + + +/// @dev Feature to swap directly with an on-chain liquidity provider. +interface ILiquidityProviderFeature { + event LiquidityProviderForMarketUpdated( + address indexed xAsset, + address indexed yAsset, + address providerAddress + ); + + function sellToLiquidityProvider( + address makerToken, + address takerToken, + address payable recipient, + uint256 sellAmount, + uint256 minBuyAmount + ) + external + payable + returns (uint256 boughtAmount); + + /// @dev Sets address of the liquidity provider for a market given + /// (xAsset, yAsset). + /// @param xAsset First asset managed by the liquidity provider. + /// @param yAsset Second asset managed by the liquidity provider. + /// @param providerAddress Address of the liquidity provider. + function setLiquidityProviderForMarket( + address xAsset, + address yAsset, + address providerAddress + ) + external; + + /// @dev Returns the address of the liquidity provider for a market given + /// (xAsset, yAsset), or reverts if pool does not exist. + /// @param xAsset First asset managed by the liquidity provider. + /// @param yAsset Second asset managed by the liquidity provider. + /// @return providerAddress Address of the liquidity provider. + function getLiquidityProviderForMarket( + address xAsset, + address yAsset + ) + external + view + returns (address providerAddress); +} diff --git a/contracts/zero-ex/contracts/src/features/LiquidityProviderFeature.sol b/contracts/zero-ex/contracts/src/features/LiquidityProviderFeature.sol new file mode 100644 index 0000000000..d152394591 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/LiquidityProviderFeature.sol @@ -0,0 +1,204 @@ +/* + + Copyright 2020 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 "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol"; +import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; +import "../errors/LibLiquidityProviderRichErrors.sol"; +import "../fixins/FixinCommon.sol"; +import "../storage/LibLiquidityProviderStorage.sol"; +import "./IFeature.sol"; +import "./ILiquidityProviderFeature.sol"; +import "./ITokenSpenderFeature.sol"; + + +interface IERC20Bridge { + /// @dev Transfers `amount` of the ERC20 `tokenAddress` from `from` to `to`. + /// @param tokenAddress The address of the ERC20 token to transfer. + /// @param from Address to transfer asset from. + /// @param to Address to transfer asset to. + /// @param amount Amount of asset to transfer. + /// @param bridgeData Arbitrary asset data needed by the bridge contract. + /// @return success The magic bytes `0xdc1600f3` if successful. + function bridgeTransferFrom( + address tokenAddress, + address from, + address to, + uint256 amount, + bytes calldata bridgeData + ) + external + returns (bytes4 success); +} + +contract LiquidityProviderFeature is + IFeature, + ILiquidityProviderFeature, + FixinCommon +{ + using LibERC20TokenV06 for IERC20TokenV06; + using LibSafeMathV06 for uint256; + using LibRichErrorsV06 for bytes; + + /// @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, 0); + + /// @dev ETH pseudo-token address. + address constant internal ETH_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + /// @dev The WETH contract address. + IEtherTokenV06 public immutable weth; + + /// @dev Store the WETH address in an immutable. + /// @param weth_ The weth token. + constructor(IEtherTokenV06 weth_) + public + FixinCommon() + { + weth = weth_; + } + + function sellToLiquidityProvider( + address makerToken, + address takerToken, + address payable recipient, + uint256 sellAmount, + uint256 minBuyAmount + ) + external + override + payable + returns (uint256 boughtAmount) + { + address providerAddress = getLiquidityProviderForMarket(makerToken, takerToken); + if (recipient == address(0)) { + recipient = msg.sender; + } + + if (takerToken == ETH_TOKEN_ADDRESS) { + // Wrap ETH. + weth.deposit{value: sellAmount}(); + weth.transfer(providerAddress, sellAmount); + } else { + ITokenSpenderFeature(address(this))._spendERC20Tokens( + IERC20TokenV06(takerToken), + msg.sender, + providerAddress, + sellAmount + ); + } + + if (makerToken == ETH_TOKEN_ADDRESS) { + uint256 balanceBefore = weth.balanceOf(address(this)); + IERC20Bridge(providerAddress).bridgeTransferFrom( + address(weth), + address(0), + address(this), + minBuyAmount, + "" + ); + boughtAmount = weth.balanceOf(address(this)).safeSub(balanceBefore); + // Unwrap wETH and send ETH to recipient. + weth.withdraw(boughtAmount); + recipient.transfer(boughtAmount); + } else { + uint256 balanceBefore = IERC20TokenV06(makerToken).balanceOf(recipient); + IERC20Bridge(providerAddress).bridgeTransferFrom( + makerToken, + address(0), + recipient, + minBuyAmount, + "" + ); + boughtAmount = IERC20TokenV06(makerToken).balanceOf(recipient).safeSub(balanceBefore); + } + if (boughtAmount < minBuyAmount) { + LibLiquidityProviderRichErrors.LiquidityProviderIncompleteSellError( + providerAddress, + makerToken, + takerToken, + sellAmount, + boughtAmount, + minBuyAmount + ).rrevert(); + } + } + + /// @dev Sets address of the liquidity provider for a market given + /// (xAsset, yAsset). + /// @param xAsset First asset managed by the liquidity provider. + /// @param yAsset Second asset managed by the liquidity provider. + /// @param providerAddress Address of the liquidity provider. + function setLiquidityProviderForMarket( + address xAsset, + address yAsset, + address providerAddress + ) + external + override + onlyOwner + { + LibLiquidityProviderStorage.getStorage() + .addressBook[xAsset][yAsset] = providerAddress; + LibLiquidityProviderStorage.getStorage() + .addressBook[yAsset][xAsset] = providerAddress; + emit LiquidityProviderForMarketUpdated( + xAsset, + yAsset, + providerAddress + ); + } + + /// @dev Returns the address of the liquidity provider for a market given + /// (xAsset, yAsset), or reverts if pool does not exist. + /// @param xAsset First asset managed by the liquidity provider. + /// @param yAsset Second asset managed by the liquidity provider. + /// @return providerAddress Address of the liquidity provider. + function getLiquidityProviderForMarket( + address xAsset, + address yAsset + ) + public + view + override + returns (address providerAddress) + { + if (xAsset == ETH_TOKEN_ADDRESS) { + providerAddress = LibLiquidityProviderStorage.getStorage() + .addressBook[address(weth)][yAsset]; + } else if (yAsset == ETH_TOKEN_ADDRESS) { + providerAddress = LibLiquidityProviderStorage.getStorage() + .addressBook[xAsset][address(weth)]; + } else { + providerAddress = LibLiquidityProviderStorage.getStorage() + .addressBook[xAsset][yAsset]; + } + if (providerAddress == address(0)) { + LibLiquidityProviderRichErrors.NoLiquidityProviderForMarketError( + xAsset, + yAsset + ).rrevert(); + } + } +} diff --git a/contracts/zero-ex/contracts/src/features/MetaTransactionsFeature.sol b/contracts/zero-ex/contracts/src/features/MetaTransactionsFeature.sol index bd6e40b1c5..6ac4475d8d 100644 --- a/contracts/zero-ex/contracts/src/features/MetaTransactionsFeature.sol +++ b/contracts/zero-ex/contracts/src/features/MetaTransactionsFeature.sol @@ -184,7 +184,7 @@ contract MetaTransactionsFeature is /// @dev Execute a meta-transaction via `sender`. Privileged variant. /// Only callable from within. - /// @param sender Who is executing the meta-transaction.. + /// @param sender Who is executing the meta-transaction. /// @param mtx The meta-transaction. /// @param signature The signature by `mtx.signer`. /// @return returnResult The ABI-encoded result of the underlying call. @@ -454,7 +454,7 @@ contract MetaTransactionsFeature is } /// @dev Make an arbitrary internal, meta-transaction call. - /// Warning: Do not let unadulerated `callData` into this function. + /// Warning: Do not let unadulterated `callData` into this function. function _callSelf(bytes32 hash, bytes memory callData, uint256 value) private returns (bytes memory returnResult) diff --git a/contracts/zero-ex/contracts/src/storage/LibLiquidityProviderStorage.sol b/contracts/zero-ex/contracts/src/storage/LibLiquidityProviderStorage.sol new file mode 100644 index 0000000000..99f0361ef0 --- /dev/null +++ b/contracts/zero-ex/contracts/src/storage/LibLiquidityProviderStorage.sol @@ -0,0 +1,45 @@ +/* + + Copyright 2020 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 "./LibStorage.sol"; + + +/// @dev Storage helpers for `LiquidityProviderFeature`. +library LibLiquidityProviderStorage { + + /// @dev Storage bucket for this feature. + struct Storage { + // Mapping of taker token -> maker token -> liquidity provider address + // Note that addressBook[x][y] == addressBook[y][x] will always hold. + mapping (address => mapping (address => address)) addressBook; + } + + /// @dev Get the storage bucket for this contract. + function getStorage() internal pure returns (Storage storage stor) { + uint256 storageSlot = LibStorage.getStorageSlot( + LibStorage.StorageId.LiquidityProvider + ); + // Dip into assembly to change the slot pointed to by the local + // variable `stor`. + // See https://solidity.readthedocs.io/en/v0.6.8/assembly.html?highlight=slot#access-to-external-variables-functions-and-libraries + assembly { stor_slot := storageSlot } + } +} diff --git a/contracts/zero-ex/contracts/src/storage/LibStorage.sol b/contracts/zero-ex/contracts/src/storage/LibStorage.sol index 809977d4a8..129254f82a 100644 --- a/contracts/zero-ex/contracts/src/storage/LibStorage.sol +++ b/contracts/zero-ex/contracts/src/storage/LibStorage.sol @@ -36,7 +36,8 @@ library LibStorage { TokenSpender, TransformERC20, MetaTransactions, - ReentrancyGuard + ReentrancyGuard, + LiquidityProvider } /// @dev Get the storage slot given a storage ID. We assign unique, well-spaced diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index 89d547f032..ee3cac9677 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -41,7 +41,7 @@ "config": { "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,SignatureValidatorFeature,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinReentrancyGuard|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|IMetaTransactionsFeature|IOwnableFeature|ISignatureValidatorFeature|ISimpleFunctionRegistryFeature|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignatureRichErrors|LibSignedCallData|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LogMetadataTransformer|MetaTransactionsFeature|MixinAdapterAddresses|MixinBalancer|MixinCurve|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|OwnableFeature|PayTakerTransformer|SignatureValidatorFeature|SimpleFunctionRegistryFeature|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx).json" + "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinReentrancyGuard|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|ILiquidityProviderFeature|IMetaTransactionsFeature|IOwnableFeature|ISignatureValidatorFeature|ISimpleFunctionRegistryFeature|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibLiquidityProviderRichErrors|LibLiquidityProviderStorage|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignatureRichErrors|LibSignedCallData|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LogMetadataTransformer|MetaTransactionsFeature|MixinAdapterAddresses|MixinBalancer|MixinCurve|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|OwnableFeature|PayTakerTransformer|SignatureValidatorFeature|SimpleFunctionRegistryFeature|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx).json" }, "repository": { "type": "git", diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index d97231955e..a2eb14facf 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -24,6 +24,7 @@ import * as IExchange from '../test/generated-artifacts/IExchange.json'; import * as IFeature from '../test/generated-artifacts/IFeature.json'; import * as IFlashWallet from '../test/generated-artifacts/IFlashWallet.json'; import * as IGasToken from '../test/generated-artifacts/IGasToken.json'; +import * as ILiquidityProviderFeature from '../test/generated-artifacts/ILiquidityProviderFeature.json'; import * as IMetaTransactionsFeature from '../test/generated-artifacts/IMetaTransactionsFeature.json'; import * as InitialMigration from '../test/generated-artifacts/InitialMigration.json'; import * as IOwnableFeature from '../test/generated-artifacts/IOwnableFeature.json'; @@ -37,6 +38,8 @@ import * as IZeroEx from '../test/generated-artifacts/IZeroEx.json'; import * as LibBootstrap from '../test/generated-artifacts/LibBootstrap.json'; import * as LibCommonRichErrors from '../test/generated-artifacts/LibCommonRichErrors.json'; import * as LibERC20Transformer from '../test/generated-artifacts/LibERC20Transformer.json'; +import * as LibLiquidityProviderRichErrors from '../test/generated-artifacts/LibLiquidityProviderRichErrors.json'; +import * as LibLiquidityProviderStorage from '../test/generated-artifacts/LibLiquidityProviderStorage.json'; import * as LibMetaTransactionsRichErrors from '../test/generated-artifacts/LibMetaTransactionsRichErrors.json'; import * as LibMetaTransactionsStorage from '../test/generated-artifacts/LibMetaTransactionsStorage.json'; import * as LibMigrate from '../test/generated-artifacts/LibMigrate.json'; @@ -55,6 +58,7 @@ import * as LibTokenSpenderStorage from '../test/generated-artifacts/LibTokenSpe import * as LibTransformERC20RichErrors from '../test/generated-artifacts/LibTransformERC20RichErrors.json'; import * as LibTransformERC20Storage from '../test/generated-artifacts/LibTransformERC20Storage.json'; import * as LibWalletRichErrors from '../test/generated-artifacts/LibWalletRichErrors.json'; +import * as LiquidityProviderFeature from '../test/generated-artifacts/LiquidityProviderFeature.json'; import * as LogMetadataTransformer from '../test/generated-artifacts/LogMetadataTransformer.json'; import * as MetaTransactionsFeature from '../test/generated-artifacts/MetaTransactionsFeature.json'; import * as MixinAdapterAddresses from '../test/generated-artifacts/MixinAdapterAddresses.json'; @@ -104,6 +108,7 @@ export const artifacts = { IZeroEx: IZeroEx as ContractArtifact, ZeroEx: ZeroEx as ContractArtifact, LibCommonRichErrors: LibCommonRichErrors as ContractArtifact, + LibLiquidityProviderRichErrors: LibLiquidityProviderRichErrors as ContractArtifact, LibMetaTransactionsRichErrors: LibMetaTransactionsRichErrors as ContractArtifact, LibOwnableRichErrors: LibOwnableRichErrors as ContractArtifact, LibProxyRichErrors: LibProxyRichErrors as ContractArtifact, @@ -120,6 +125,7 @@ export const artifacts = { BootstrapFeature: BootstrapFeature as ContractArtifact, IBootstrapFeature: IBootstrapFeature as ContractArtifact, IFeature: IFeature as ContractArtifact, + ILiquidityProviderFeature: ILiquidityProviderFeature as ContractArtifact, IMetaTransactionsFeature: IMetaTransactionsFeature as ContractArtifact, IOwnableFeature: IOwnableFeature as ContractArtifact, ISignatureValidatorFeature: ISignatureValidatorFeature as ContractArtifact, @@ -127,6 +133,7 @@ export const artifacts = { ITokenSpenderFeature: ITokenSpenderFeature as ContractArtifact, ITransformERC20Feature: ITransformERC20Feature as ContractArtifact, IUniswapFeature: IUniswapFeature as ContractArtifact, + LiquidityProviderFeature: LiquidityProviderFeature as ContractArtifact, MetaTransactionsFeature: MetaTransactionsFeature as ContractArtifact, OwnableFeature: OwnableFeature as ContractArtifact, SignatureValidatorFeature: SignatureValidatorFeature as ContractArtifact, @@ -142,6 +149,7 @@ export const artifacts = { InitialMigration: InitialMigration as ContractArtifact, LibBootstrap: LibBootstrap as ContractArtifact, LibMigrate: LibMigrate as ContractArtifact, + LibLiquidityProviderStorage: LibLiquidityProviderStorage as ContractArtifact, LibMetaTransactionsStorage: LibMetaTransactionsStorage as ContractArtifact, LibOwnableStorage: LibOwnableStorage as ContractArtifact, LibProxyStorage: LibProxyStorage as ContractArtifact, diff --git a/contracts/zero-ex/test/features/liquidity_provider_test.ts b/contracts/zero-ex/test/features/liquidity_provider_test.ts new file mode 100644 index 0000000000..4c183456df --- /dev/null +++ b/contracts/zero-ex/test/features/liquidity_provider_test.ts @@ -0,0 +1,48 @@ +import { + blockchainTests, + expect, + getRandomInteger, + randomAddress, + verifyEventsFromLogs, +} from '@0x/contracts-test-utils'; +import { BigNumber, hexUtils, StringRevertError, ZeroExRevertErrors } from '@0x/utils'; + +import { IZeroExContract, TokenSpenderFeatureContract } from '../../src/wrappers'; +import { artifacts } from '../artifacts'; +import { abis } from '../utils/abis'; +import { fullMigrateAsync } from '../utils/migration'; +import { TestTokenSpenderERC20TokenContract, TestTokenSpenderERC20TokenEvents } from '../wrappers'; + +blockchainTests.resets('LiquidityProvider feature', env => { + let zeroEx: IZeroExContract; + let feature: TokenSpenderFeatureContract; + let token: TestTokenSpenderERC20TokenContract; + let allowanceTarget: string; + + before(async () => { + const [owner] = await env.getAccountAddressesAsync(); + zeroEx = await fullMigrateAsync(owner, env.provider, env.txDefaults, { + tokenSpender: (await TokenSpenderFeatureContract.deployFrom0xArtifactAsync( + artifacts.TestTokenSpender, + env.provider, + env.txDefaults, + artifacts, + )).address, + }); + feature = new TokenSpenderFeatureContract(zeroEx.address, env.provider, env.txDefaults, abis); + token = await TestTokenSpenderERC20TokenContract.deployFrom0xArtifactAsync( + artifacts.TestTokenSpenderERC20Token, + env.provider, + env.txDefaults, + artifacts, + ); + allowanceTarget = await feature.getAllowanceTarget().callAsync(); + }); + + describe('Registry', () => { + + }); + describe('Swap', () => { + + }); +}); diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index 904e4db89a..1aa12ac0c7 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -22,6 +22,7 @@ export * from '../test/generated-wrappers/i_exchange'; export * from '../test/generated-wrappers/i_feature'; export * from '../test/generated-wrappers/i_flash_wallet'; export * from '../test/generated-wrappers/i_gas_token'; +export * from '../test/generated-wrappers/i_liquidity_provider_feature'; export * from '../test/generated-wrappers/i_meta_transactions_feature'; export * from '../test/generated-wrappers/i_ownable_feature'; export * from '../test/generated-wrappers/i_signature_validator_feature'; @@ -35,6 +36,8 @@ export * from '../test/generated-wrappers/initial_migration'; export * from '../test/generated-wrappers/lib_bootstrap'; export * from '../test/generated-wrappers/lib_common_rich_errors'; export * from '../test/generated-wrappers/lib_erc20_transformer'; +export * from '../test/generated-wrappers/lib_liquidity_provider_rich_errors'; +export * from '../test/generated-wrappers/lib_liquidity_provider_storage'; export * from '../test/generated-wrappers/lib_meta_transactions_rich_errors'; export * from '../test/generated-wrappers/lib_meta_transactions_storage'; export * from '../test/generated-wrappers/lib_migrate'; @@ -53,6 +56,7 @@ export * from '../test/generated-wrappers/lib_token_spender_storage'; export * from '../test/generated-wrappers/lib_transform_erc20_rich_errors'; export * from '../test/generated-wrappers/lib_transform_erc20_storage'; export * from '../test/generated-wrappers/lib_wallet_rich_errors'; +export * from '../test/generated-wrappers/liquidity_provider_feature'; export * from '../test/generated-wrappers/log_metadata_transformer'; export * from '../test/generated-wrappers/meta_transactions_feature'; export * from '../test/generated-wrappers/mixin_adapter_addresses'; diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index c74120dd9d..de0a013816 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -45,6 +45,7 @@ "test/generated-artifacts/IFeature.json", "test/generated-artifacts/IFlashWallet.json", "test/generated-artifacts/IGasToken.json", + "test/generated-artifacts/ILiquidityProviderFeature.json", "test/generated-artifacts/IMetaTransactionsFeature.json", "test/generated-artifacts/IOwnableFeature.json", "test/generated-artifacts/ISignatureValidatorFeature.json", @@ -58,6 +59,8 @@ "test/generated-artifacts/LibBootstrap.json", "test/generated-artifacts/LibCommonRichErrors.json", "test/generated-artifacts/LibERC20Transformer.json", + "test/generated-artifacts/LibLiquidityProviderRichErrors.json", + "test/generated-artifacts/LibLiquidityProviderStorage.json", "test/generated-artifacts/LibMetaTransactionsRichErrors.json", "test/generated-artifacts/LibMetaTransactionsStorage.json", "test/generated-artifacts/LibMigrate.json", @@ -76,6 +79,7 @@ "test/generated-artifacts/LibTransformERC20RichErrors.json", "test/generated-artifacts/LibTransformERC20Storage.json", "test/generated-artifacts/LibWalletRichErrors.json", + "test/generated-artifacts/LiquidityProviderFeature.json", "test/generated-artifacts/LogMetadataTransformer.json", "test/generated-artifacts/MetaTransactionsFeature.json", "test/generated-artifacts/MixinAdapterAddresses.json", diff --git a/packages/contract-artifacts/artifacts/IZeroEx.json b/packages/contract-artifacts/artifacts/IZeroEx.json index 4255ce4384..96f2b33437 100644 --- a/packages/contract-artifacts/artifacts/IZeroEx.json +++ b/packages/contract-artifacts/artifacts/IZeroEx.json @@ -3,6 +3,16 @@ "contractName": "IZeroEx", "compilerOutput": { "abi": [ + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "xAsset", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "yAsset", "type": "address" }, + { "indexed": false, "internalType": "address", "name": "providerAddress", "type": "address" } + ], + "name": "LiquidityProviderForMarketUpdated", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -222,6 +232,16 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { "internalType": "address", "name": "xAsset", "type": "address" }, + { "internalType": "address", "name": "yAsset", "type": "address" } + ], + "name": "getLiquidityProviderForMarket", + "outputs": [{ "internalType": "address", "name": "providerAddress", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -366,6 +386,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { "internalType": "address", "name": "makerToken", "type": "address" }, + { "internalType": "address", "name": "takerToken", "type": "address" }, + { "internalType": "address payable", "name": "recipient", "type": "address" }, + { "internalType": "uint256", "name": "sellAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "minBuyAmount", "type": "uint256" } + ], + "name": "sellToLiquidityProvider", + "outputs": [{ "internalType": "uint256", "name": "boughtAmount", "type": "uint256" }], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [ { "internalType": "contract IERC20TokenV06[]", "name": "tokens", "type": "address[]" }, @@ -378,6 +411,17 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { "internalType": "address", "name": "xAsset", "type": "address" }, + { "internalType": "address", "name": "yAsset", "type": "address" }, + { "internalType": "address", "name": "providerAddress", "type": "address" } + ], + "name": "setLiquidityProviderForMarket", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [{ "internalType": "address", "name": "quoteSigner", "type": "address" }], "name": "setQuoteSigner", @@ -492,6 +536,14 @@ "params": { "selector": "The function selector." }, "returns": { "impl": "The implementation contract address." } }, + "getLiquidityProviderForMarket(address,address)": { + "details": "Returns the address of the liquidity provider for a market given (xAsset, yAsset), or reverts if pool does not exist.", + "params": { + "xAsset": "First asset managed by the liquidity provider.", + "yAsset": "Second asset managed by the liquidity provider." + }, + "returns": { "providerAddress": "Address of the liquidity provider." } + }, "getMetaTransactionExecutedBlock((address,address,uint256,uint256,uint256,uint256,bytes,uint256,address,uint256))": { "details": "Get the block at which a meta-transaction has been executed.", "params": { "mtx": "The meta-transaction." }, @@ -574,6 +626,14 @@ }, "returns": { "buyAmount": "Amount of `tokens[-1]` bought." } }, + "setLiquidityProviderForMarket(address,address,address)": { + "details": "Sets address of the liquidity provider for a market given (xAsset, yAsset).", + "params": { + "providerAddress": "Address of the liquidity provider.", + "xAsset": "First asset managed by the liquidity provider.", + "yAsset": "Second asset managed by the liquidity provider." + } + }, "setQuoteSigner(address)": { "details": "Replace the optional signer for `transformERC20()` calldata. Only callable by the owner.", "params": { "quoteSigner": "The address of the new calldata signer." } diff --git a/packages/contract-wrappers/src/generated-wrappers/i_zero_ex.ts b/packages/contract-wrappers/src/generated-wrappers/i_zero_ex.ts index faf40e2b47..e0da11d70b 100644 --- a/packages/contract-wrappers/src/generated-wrappers/i_zero_ex.ts +++ b/packages/contract-wrappers/src/generated-wrappers/i_zero_ex.ts @@ -36,6 +36,7 @@ import * as ethers from 'ethers'; // tslint:enable:no-unused-variable export type IZeroExEventArgs = + | IZeroExLiquidityProviderForMarketUpdatedEventArgs | IZeroExMetaTransactionExecutedEventArgs | IZeroExMigratedEventArgs | IZeroExOwnershipTransferredEventArgs @@ -45,6 +46,7 @@ export type IZeroExEventArgs = | IZeroExTransformerDeployerUpdatedEventArgs; export enum IZeroExEvents { + LiquidityProviderForMarketUpdated = 'LiquidityProviderForMarketUpdated', MetaTransactionExecuted = 'MetaTransactionExecuted', Migrated = 'Migrated', OwnershipTransferred = 'OwnershipTransferred', @@ -54,6 +56,12 @@ export enum IZeroExEvents { TransformerDeployerUpdated = 'TransformerDeployerUpdated', } +export interface IZeroExLiquidityProviderForMarketUpdatedEventArgs extends DecodedLogArgs { + xAsset: string; + yAsset: string; + providerAddress: string; +} + export interface IZeroExMetaTransactionExecutedEventArgs extends DecodedLogArgs { hash: string; selector: string; @@ -211,6 +219,29 @@ export class IZeroExContract extends BaseContract { */ public static ABI(): ContractAbi { const abi = [ + { + anonymous: false, + inputs: [ + { + name: 'xAsset', + type: 'address', + indexed: true, + }, + { + name: 'yAsset', + type: 'address', + indexed: true, + }, + { + name: 'providerAddress', + type: 'address', + indexed: false, + }, + ], + name: 'LiquidityProviderForMarketUpdated', + outputs: [], + type: 'event', + }, { anonymous: false, inputs: [ @@ -697,6 +728,27 @@ export class IZeroExContract extends BaseContract { stateMutability: 'view', type: 'function', }, + { + inputs: [ + { + name: 'xAsset', + type: 'address', + }, + { + name: 'yAsset', + type: 'address', + }, + ], + name: 'getLiquidityProviderForMarket', + outputs: [ + { + name: 'providerAddress', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, { inputs: [ { @@ -1000,6 +1052,39 @@ export class IZeroExContract extends BaseContract { stateMutability: 'nonpayable', type: 'function', }, + { + inputs: [ + { + name: 'makerToken', + type: 'address', + }, + { + name: 'takerToken', + type: 'address', + }, + { + name: 'recipient', + type: 'address', + }, + { + name: 'sellAmount', + type: 'uint256', + }, + { + name: 'minBuyAmount', + type: 'uint256', + }, + ], + name: 'sellToLiquidityProvider', + outputs: [ + { + name: 'boughtAmount', + type: 'uint256', + }, + ], + stateMutability: 'payable', + type: 'function', + }, { inputs: [ { @@ -1029,6 +1114,26 @@ export class IZeroExContract extends BaseContract { stateMutability: 'payable', type: 'function', }, + { + inputs: [ + { + name: 'xAsset', + type: 'address', + }, + { + name: 'yAsset', + type: 'address', + }, + { + name: 'providerAddress', + type: 'address', + }, + ], + name: 'setLiquidityProviderForMarket', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, { inputs: [ { @@ -1743,6 +1848,60 @@ export class IZeroExContract extends BaseContract { }, }; } + /** + * Returns the address of the liquidity provider for a market given + * (xAsset, yAsset), or reverts if pool does not exist. + * @param xAsset First asset managed by the liquidity provider. + * @param yAsset Second asset managed by the liquidity provider. + */ + public getLiquidityProviderForMarket(xAsset: string, yAsset: string): ContractTxFunctionObj { + const self = (this as any) as IZeroExContract; + assert.isString('xAsset', xAsset); + assert.isString('yAsset', yAsset); + const functionSignature = 'getLiquidityProviderForMarket(address,address)'; + + return { + async sendTransactionAsync( + txData?: Partial | undefined, + opts: SendTransactionOpts = { shouldValidate: true }, + ): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync( + { data: this.getABIEncodedTransactionData(), ...txData }, + this.estimateGasAsync.bind(this), + ); + if (opts.shouldValidate !== false) { + await this.callAsync(txDataWithDefaults); + } + return self._web3Wrapper.sendTransactionAsync(txDataWithDefaults); + }, + awaitTransactionSuccessAsync( + txData?: Partial, + opts: AwaitTransactionSuccessOpts = { shouldValidate: true }, + ): PromiseWithTransactionHash { + return self._promiseWithTransactionHash(this.sendTransactionAsync(txData, opts), opts); + }, + async estimateGasAsync(txData?: Partial | undefined): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({ + data: this.getABIEncodedTransactionData(), + ...txData, + }); + return self._web3Wrapper.estimateGasAsync(txDataWithDefaults); + }, + async callAsync(callData: Partial = {}, defaultBlock?: BlockParam): Promise { + BaseContract._assertCallParams(callData, defaultBlock); + const rawCallResult = await self._performCallAsync( + { data: this.getABIEncodedTransactionData(), ...callData }, + defaultBlock, + ); + const abiEncoder = self._lookupAbiEncoder(functionSignature); + BaseContract._throwIfUnexpectedEmptyCallResult(rawCallResult, abiEncoder); + return abiEncoder.strictDecodeReturnValue(rawCallResult); + }, + getABIEncodedTransactionData(): string { + return self._strictEncodeArguments(functionSignature, [xAsset.toLowerCase(), yAsset.toLowerCase()]); + }, + }; + } /** * Get the block at which a meta-transaction has been executed. * @param mtx The meta-transaction. @@ -2447,6 +2606,69 @@ export class IZeroExContract extends BaseContract { }, }; } + public sellToLiquidityProvider( + makerToken: string, + takerToken: string, + recipient: string, + sellAmount: BigNumber, + minBuyAmount: BigNumber, + ): ContractTxFunctionObj { + const self = (this as any) as IZeroExContract; + assert.isString('makerToken', makerToken); + assert.isString('takerToken', takerToken); + assert.isString('recipient', recipient); + assert.isBigNumber('sellAmount', sellAmount); + assert.isBigNumber('minBuyAmount', minBuyAmount); + const functionSignature = 'sellToLiquidityProvider(address,address,address,uint256,uint256)'; + + return { + async sendTransactionAsync( + txData?: Partial | undefined, + opts: SendTransactionOpts = { shouldValidate: true }, + ): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync( + { data: this.getABIEncodedTransactionData(), ...txData }, + this.estimateGasAsync.bind(this), + ); + if (opts.shouldValidate !== false) { + await this.callAsync(txDataWithDefaults); + } + return self._web3Wrapper.sendTransactionAsync(txDataWithDefaults); + }, + awaitTransactionSuccessAsync( + txData?: Partial, + opts: AwaitTransactionSuccessOpts = { shouldValidate: true }, + ): PromiseWithTransactionHash { + return self._promiseWithTransactionHash(this.sendTransactionAsync(txData, opts), opts); + }, + async estimateGasAsync(txData?: Partial | undefined): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({ + data: this.getABIEncodedTransactionData(), + ...txData, + }); + return self._web3Wrapper.estimateGasAsync(txDataWithDefaults); + }, + async callAsync(callData: Partial = {}, defaultBlock?: BlockParam): Promise { + BaseContract._assertCallParams(callData, defaultBlock); + const rawCallResult = await self._performCallAsync( + { data: this.getABIEncodedTransactionData(), ...callData }, + defaultBlock, + ); + const abiEncoder = self._lookupAbiEncoder(functionSignature); + BaseContract._throwIfUnexpectedEmptyCallResult(rawCallResult, abiEncoder); + return abiEncoder.strictDecodeReturnValue(rawCallResult); + }, + getABIEncodedTransactionData(): string { + return self._strictEncodeArguments(functionSignature, [ + makerToken.toLowerCase(), + takerToken.toLowerCase(), + recipient.toLowerCase(), + sellAmount, + minBuyAmount, + ]); + }, + }; + } /** * Efficiently sell directly to uniswap/sushiswap. * @param tokens Sell path. @@ -2509,6 +2731,70 @@ export class IZeroExContract extends BaseContract { }, }; } + /** + * Sets address of the liquidity provider for a market given + * (xAsset, yAsset). + * @param xAsset First asset managed by the liquidity provider. + * @param yAsset Second asset managed by the liquidity provider. + * @param providerAddress Address of the liquidity provider. + */ + public setLiquidityProviderForMarket( + xAsset: string, + yAsset: string, + providerAddress: string, + ): ContractTxFunctionObj { + const self = (this as any) as IZeroExContract; + assert.isString('xAsset', xAsset); + assert.isString('yAsset', yAsset); + assert.isString('providerAddress', providerAddress); + const functionSignature = 'setLiquidityProviderForMarket(address,address,address)'; + + return { + async sendTransactionAsync( + txData?: Partial | undefined, + opts: SendTransactionOpts = { shouldValidate: true }, + ): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync( + { data: this.getABIEncodedTransactionData(), ...txData }, + this.estimateGasAsync.bind(this), + ); + if (opts.shouldValidate !== false) { + await this.callAsync(txDataWithDefaults); + } + return self._web3Wrapper.sendTransactionAsync(txDataWithDefaults); + }, + awaitTransactionSuccessAsync( + txData?: Partial, + opts: AwaitTransactionSuccessOpts = { shouldValidate: true }, + ): PromiseWithTransactionHash { + return self._promiseWithTransactionHash(this.sendTransactionAsync(txData, opts), opts); + }, + async estimateGasAsync(txData?: Partial | undefined): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({ + data: this.getABIEncodedTransactionData(), + ...txData, + }); + return self._web3Wrapper.estimateGasAsync(txDataWithDefaults); + }, + async callAsync(callData: Partial = {}, defaultBlock?: BlockParam): Promise { + BaseContract._assertCallParams(callData, defaultBlock); + const rawCallResult = await self._performCallAsync( + { data: this.getABIEncodedTransactionData(), ...callData }, + defaultBlock, + ); + const abiEncoder = self._lookupAbiEncoder(functionSignature); + BaseContract._throwIfUnexpectedEmptyCallResult(rawCallResult, abiEncoder); + return abiEncoder.strictDecodeReturnValue(rawCallResult); + }, + getABIEncodedTransactionData(): string { + return self._strictEncodeArguments(functionSignature, [ + xAsset.toLowerCase(), + yAsset.toLowerCase(), + providerAddress.toLowerCase(), + ]); + }, + }; + } /** * Replace the optional signer for `transformERC20()` calldata. * Only callable by the owner.