diff --git a/contracts/zero-ex/contracts/src/errors/LibTransformERC20RichErrors.sol b/contracts/zero-ex/contracts/src/errors/LibTransformERC20RichErrors.sol index f35179edd5..4ce8f16687 100644 --- a/contracts/zero-ex/contracts/src/errors/LibTransformERC20RichErrors.sol +++ b/contracts/zero-ex/contracts/src/errors/LibTransformERC20RichErrors.sol @@ -98,6 +98,21 @@ library LibTransformERC20RichErrors { ); } + // Common Transformer errors /////////////////////////////////////////////// + + function InvalidTransformDataError( + bytes memory transformData + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("InvalidTransformDataError(bytes)")), + transformData + ); + } + // FillQuoteTransformer errors ///////////////////////////////////////////// function IncompleteFillSellQuoteError( @@ -177,24 +192,7 @@ library LibTransformERC20RichErrors { ); } - // WethTransformer errors //////////////////////////////////////////////////// - - function WrongNumberOfTokensReceivedError( - uint256 actual, - uint256 expected - ) - internal - pure - returns (bytes memory) - { - return abi.encodeWithSelector( - bytes4(keccak256("WrongNumberOfTokensReceivedError(uint256,uint256)")), - actual, - expected - ); - } - - function InvalidTokenReceivedError( + function InvalidTakerFeeTokenError( address token ) internal @@ -202,8 +200,9 @@ library LibTransformERC20RichErrors { returns (bytes memory) { return abi.encodeWithSelector( - bytes4(keccak256("InvalidTokenReceivedError(address)")), + bytes4(keccak256("InvalidTakerFeeTokenError(address)")), token ); } + } diff --git a/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol b/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol new file mode 100644 index 0000000000..f004222ea8 --- /dev/null +++ b/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol @@ -0,0 +1,418 @@ +/* + + 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-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; +import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibBytesV06.sol"; +import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibMathV06.sol"; +import "../errors/LibTransformERC20RichErrors.sol"; +import "../vendor/v3/IExchange.sol"; +import "./IERC20Transformer.sol"; +import "./LibERC20Transformer.sol"; + + +/// @dev A transformer that fills an ERC20 market sell/buy quote. +contract FillQuoteTransformer is + IERC20Transformer +{ + // solhint-disable indent,no-empty-blocks,no-unused-vars + + /// @dev Transform data to ABI-encode and pass into `transform()`. + struct TransformData { + // The token being sold. + // This should be an actual token, not the ETH pseudo-token. + IERC20TokenV06 sellToken; + // The token being bought. + // This should be an actual token, not the ETH pseudo-token. + IERC20TokenV06 buyToken; + // The orders to fill. + IExchange.Order[] orders; + // Signatures for each respective order in `orders`. + bytes[] signatures; + // Maximum fill amount for each order. This may be shorter than the + // number of orders, where missing entries will be treated as `uint256(-1)`. + // For sells, this will be the maximum sell amount (taker asset). + // For buys, this will be the maximum buy amount (maker asset). + uint256[] maxOrderFillAmounts; + // Amount of `sellToken` to sell. May be `uint256(-1)` to sell entire + // amount of `sellToken` received. Zero if performing a market buy. + uint256 sellAmount; + // Amount of `buyToken` to buy. Zero if performing a market sell. + uint256 buyAmount; + } + + /// @dev Results of a call to `_fillOrder()`. + struct FillOrderResults { + // The amount of taker tokens sold, according to balance checks. + uint256 takerTokenSoldAmount; + // The amount of maker tokens sold, according to balance checks. + uint256 makerTokenBoughtAmount; + // The amount of protocol fee paid. + uint256 protocolFeePaid; + } + + /// @dev The Exchange ERC20Proxy ID. + bytes4 constant private ERC20_ASSET_PROXY_ID = 0xf47261b0; + + /// @dev The Exchange contract. + IExchange public immutable exchange; + /// @dev The ERC20Proxy address. + address public immutable erc20Proxy; + + using LibERC20TokenV06 for IERC20TokenV06; + using LibERC20Transformer for IERC20TokenV06; + using LibSafeMathV06 for uint256; + using LibRichErrorsV06 for bytes; + + constructor(IExchange exchange_) public { + exchange = exchange_; + erc20Proxy = exchange_.getAssetProxy(ERC20_ASSET_PROXY_ID); + } + + /// @dev Sell this contract's entire balance of of `sellToken` in exchange + /// for `buyToken` by filling `orders`. Protocol fees should be attached + /// to this call. `buyToken` and excess ETH will be transferred back to the caller. + /// This function cannot be re-entered. + /// @param data_ ABI-encoded `TransformData`. + /// @return success `TRANSFORMER_SUCCESS` on success. + function transform( + bytes32, // callDataHash, + address payable, // taker, + bytes calldata data_ + ) + external + override + returns (bytes4 success) + { + TransformData memory data = abi.decode(data_, (TransformData)); + + // Validate data fields. + if (data.sellToken.isTokenETH() || + data.buyToken.isTokenETH() || + data.orders.length != data.signatures.length) + { + LibTransformERC20RichErrors.InvalidTransformDataError(data_).rrevert(); + } + + // If `sellAmount == -1` and `buyAmount == 0` then we are selling + // the entire balance of `sellToken`. This is useful in cases where + // the exact sell amount is not exactly known in advance, like when + // unwrapping Chai/cUSDC/cDAI. + if (data.sellAmount == uint256(-1) && data.buyAmount == 0) { + data.sellAmount = data.sellToken.getTokenBalanceOf(address(this)); + } + + // Approve the ERC20 proxy to spend `sellToken`. + data.sellToken.approveIfBelow(erc20Proxy, data.sellAmount); + + // Fill the orders. + uint256 singleProtocolFee = exchange.protocolFeeMultiplier().safeMul(tx.gasprice); + uint256 ethRemaining = address(this).balance; + uint256 boughtAmount = 0; + uint256 soldAmount = 0; + for (uint256 i = 0; i < data.orders.length; ++i) { + // Check if we've hit our targets. + if (data.buyAmount == 0) { + // Market sell check. + if (soldAmount >= data.sellAmount) { + break; + } + } else { + // Market buy check. + if (boughtAmount >= data.buyAmount) { + break; + } + } + + // Ensure we have enough ETH to cover the protocol fee. + if (ethRemaining < singleProtocolFee) { + LibTransformERC20RichErrors + .InsufficientProtocolFeeError(ethRemaining, singleProtocolFee) + .rrevert(); + } + + // Fill the order. + FillOrderResults memory results; + if (data.buyAmount == 0) { + // Market sell. + results = _sellToOrder( + data.buyToken, + data.sellToken, + data.orders[i], + data.signatures[i], + data.sellAmount.safeSub(soldAmount).min256( + data.maxOrderFillAmounts.length > i + ? data.maxOrderFillAmounts[i] + : uint256(-1) + ), + singleProtocolFee + ); + } else { + // Market buy. + results = _buyFromOrder( + data.buyToken, + data.sellToken, + data.orders[i], + data.signatures[i], + data.buyAmount.safeSub(boughtAmount).min256( + data.maxOrderFillAmounts.length > i + ? data.maxOrderFillAmounts[i] + : uint256(-1) + ), + singleProtocolFee + ); + } + + // Accumulate totals. + soldAmount = soldAmount.safeAdd(results.takerTokenSoldAmount); + boughtAmount = boughtAmount.safeAdd(results.makerTokenBoughtAmount); + ethRemaining = ethRemaining.safeSub(results.protocolFeePaid); + } + + // Ensure we hit our targets. + if (data.buyAmount == 0) { + // Market sell check. + if (soldAmount < data.sellAmount) { + LibTransformERC20RichErrors + .IncompleteFillSellQuoteError( + address(data.sellToken), + soldAmount, + data.sellAmount + ).rrevert(); + } + } else { + // Market buy check. + if (boughtAmount < data.buyAmount) { + LibTransformERC20RichErrors + .IncompleteFillBuyQuoteError( + address(data.buyToken), + boughtAmount, + data.buyAmount + ).rrevert(); + } + } + return LibERC20Transformer.TRANSFORMER_SUCCESS; + } + + /// @dev Try to sell up to `sellAmount` from an order. + /// @param makerToken The maker/buy token. + /// @param takerToken The taker/sell token. + /// @param order The order to fill. + /// @param signature The signature for `order`. + /// @param sellAmount Amount of taker token to sell. + /// @param protocolFee The protocol fee needed to fill `order`. + function _sellToOrder( + IERC20TokenV06 makerToken, + IERC20TokenV06 takerToken, + IExchange.Order memory order, + bytes memory signature, + uint256 sellAmount, + uint256 protocolFee + ) + private + returns (FillOrderResults memory results) + { + IERC20TokenV06 takerFeeToken = order.takerFeeAssetData.length == 0 + ? IERC20TokenV06(address(0)) + : _getTokenFromERC20AssetData(order.takerFeeAssetData); + + uint256 takerTokenFillAmount = sellAmount; + + if (order.takerFee != 0) { + if (takerFeeToken == makerToken) { + // Taker fee is payable in the maker token, so we need to + // approve the proxy to spend the maker token. + // It isn't worth computing the actual taker fee + // since `approveIfBelow()` will set the allowance to infinite. We + // just need a reasonable upper bound to avoid unnecessarily re-approving. + takerFeeToken.approveIfBelow(erc20Proxy, order.takerFee); + } else if (takerFeeToken == takerToken){ + // Taker fee is payable in the taker token, so we need to + // reduce the fill amount to cover the fee. + // takerTokenFillAmount' = + // (takerTokenFillAmount * order.takerAssetAmount) / + // (order.takerAssetAmount + order.takerFee) + takerTokenFillAmount = LibMathV06.getPartialAmountCeil( + order.takerAssetAmount, + order.takerAssetAmount.safeAdd(order.takerFee), + takerTokenFillAmount + ); + } else { + // Only support taker or maker asset denominated taker fees. + LibTransformERC20RichErrors.InvalidTakerFeeTokenError( + address(takerFeeToken) + ).rrevert(); + } + } + + // Clamp fill amount to order size. + takerTokenFillAmount = LibSafeMathV06.min256( + takerTokenFillAmount, + order.takerAssetAmount + ); + + // Perform the fill. + return _fillOrder( + order, + signature, + takerTokenFillAmount, + protocolFee, + makerToken, + takerFeeToken == takerToken + ); + } + + /// @dev Try to buy up to `buyAmount` from an order. + /// @param makerToken The maker/buy token. + /// @param takerToken The taker/sell token. + /// @param order The order to fill. + /// @param signature The signature for `order`. + /// @param buyAmount Amount of maker token to buy. + /// @param protocolFee The protocol fee needed to fill `order`. + function _buyFromOrder( + IERC20TokenV06 makerToken, + IERC20TokenV06 takerToken, + IExchange.Order memory order, + bytes memory signature, + uint256 buyAmount, + uint256 protocolFee + ) + private + returns (FillOrderResults memory results) + { + IERC20TokenV06 takerFeeToken = order.takerFeeAssetData.length == 0 + ? IERC20TokenV06(address(0)) + : _getTokenFromERC20AssetData(order.takerFeeAssetData); + + uint256 makerTokenFillAmount = buyAmount; + + if (order.takerFee != 0) { + if (takerFeeToken == makerToken) { + // Taker fee is payable in the maker token. + // Increase the fill amount to account for maker tokens being + // lost to the taker fee. + // makerTokenFillAmount' = + // (order.makerAssetAmount * makerTokenFillAmount) / + // (order.makerAssetAmount - order.takerFee) + makerTokenFillAmount = LibMathV06.getPartialAmountCeil( + order.makerAssetAmount, + order.makerAssetAmount.safeSub(order.takerFee), + makerTokenFillAmount + ); + // Approve the proxy to spend the maker token. + // It isn't worth computing the actual taker fee + // since `approveIfBelow()` will set the allowance to infinite. We + // just need a reasonable upper bound to avoid unnecessarily re-approving. + takerFeeToken.approveIfBelow(erc20Proxy, order.takerFee); + } else if (takerFeeToken != takerToken) { + // Only support taker or maker asset denominated taker fees. + LibTransformERC20RichErrors.InvalidTakerFeeTokenError( + address(takerFeeToken) + ).rrevert(); + } + } + + // Convert maker fill amount to taker fill amount. + uint256 takerTokenFillAmount = LibSafeMathV06.min256( + order.takerAssetAmount, + LibMathV06.getPartialAmountCeil( + makerTokenFillAmount, + order.makerAssetAmount, + order.takerAssetAmount + ) + ); + + // Perform the fill. + return _fillOrder( + order, + signature, + takerTokenFillAmount, + protocolFee, + makerToken, + takerFeeToken == takerToken + ); + } + + /// @dev Attempt to fill an order. If the fill reverts, the revert will be + /// swallowed and `results` will be zeroed out. + /// @param order The order to fill. + /// @param signature The order signature. + /// @param takerAssetFillAmount How much taker asset to fill. + /// @param protocolFee The protocol fee needed to fill this order. + /// @param makerToken The maker token. + /// @param isTakerFeeInTakerToken Whether the taker fee token is the same as the + /// taker token. + function _fillOrder( + IExchange.Order memory order, + bytes memory signature, + uint256 takerAssetFillAmount, + uint256 protocolFee, + IERC20TokenV06 makerToken, + bool isTakerFeeInTakerToken + ) + private + returns (FillOrderResults memory results) + { + // Track changes in the maker token balance. + results.makerTokenBoughtAmount = makerToken.balanceOf(address(this)); + try + exchange.fillOrder + {value: protocolFee} + (order, takerAssetFillAmount, signature) + returns (IExchange.FillResults memory fillResults) + { + // Update maker quantity based on changes in token balances. + results.makerTokenBoughtAmount = makerToken.balanceOf(address(this)) + .safeSub(results.makerTokenBoughtAmount); + // We can trust the other fill result quantities. + results.protocolFeePaid = fillResults.protocolFeePaid; + results.takerTokenSoldAmount = fillResults.takerAssetFilledAmount; + // If the taker fee is payable in the taker asset, include the + // taker fee in the total amount sold. + if (isTakerFeeInTakerToken) { + results.takerTokenSoldAmount = + results.takerTokenSoldAmount.safeAdd(fillResults.takerFeePaid); + } + } catch (bytes memory) { + // If the fill fails, zero out fill quantities. + results.makerTokenBoughtAmount = 0; + } + } + + /// @dev Extract the token from plain ERC20 asset data. + /// @param assetData The order asset data. + function _getTokenFromERC20AssetData(bytes memory assetData) + private + pure + returns (IERC20TokenV06 token) + { + if (assetData.length != 36 || + LibBytesV06.readBytes4(assetData, 0) != ERC20_ASSET_PROXY_ID) + { + LibTransformERC20RichErrors + .InvalidERC20AssetDataError(assetData) + .rrevert(); + } + return IERC20TokenV06(LibBytesV06.readAddress(assetData, 16)); + } +} diff --git a/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol b/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol new file mode 100644 index 0000000000..4b668862c1 --- /dev/null +++ b/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol @@ -0,0 +1,77 @@ +/* + + 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-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 "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; +import "../errors/LibTransformERC20RichErrors.sol"; +import "./IERC20Transformer.sol"; +import "./LibERC20Transformer.sol"; + + +/// @dev A transformer that transfers tokens to the taker. +contract PayTakerTransformer is + IERC20Transformer +{ + /// @dev Transform data to ABI-encode and pass into `transform()`. + struct TransformData { + // The tokens to transfer to the taker. + IERC20TokenV06[] tokens; + // Amount of each token in `tokens` to transfer to the taker. + // `uint(-1)` will transfer the entire balance. + uint256[] amounts; + } + + using LibRichErrorsV06 for bytes; + using LibSafeMathV06 for uint256; + using LibERC20Transformer for IERC20TokenV06; + + /// @dev Forwards tokens to the taker. + /// @param taker The taker address (caller of `TransformERC20.transformERC20()`). + /// @param data_ ABI-encoded `TransformData`, indicating which tokens to transfer. + /// @return success `TRANSFORMER_SUCCESS` on success. + function transform( + bytes32, // callDataHash, + address payable taker, + bytes calldata data_ + ) + external + override + returns (bytes4 success) + { + TransformData memory data = abi.decode(data_, (TransformData)); + + // Transfer tokens directly to the taker. + for (uint256 i = 0; i < data.tokens.length; ++i) { + // The `amounts` array can be shorter than the `tokens` array. + // Missing elements are treated as `uint256(-1)`. + uint256 amount = data.amounts.length > i ? data.amounts[i] : uint256(-1); + if (amount == uint256(-1)) { + amount = data.tokens[i].getTokenBalanceOf(address(this)); + } + if (amount != 0) { + data.tokens[i].transformerTransfer(taker, amount); + } + } + return LibERC20Transformer.TRANSFORMER_SUCCESS; + } +} diff --git a/contracts/zero-ex/contracts/src/transformers/WethTransformer.sol b/contracts/zero-ex/contracts/src/transformers/WethTransformer.sol new file mode 100644 index 0000000000..003da666e5 --- /dev/null +++ b/contracts/zero-ex/contracts/src/transformers/WethTransformer.sol @@ -0,0 +1,91 @@ +/* + + 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-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; +import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol"; +import "../errors/LibTransformERC20RichErrors.sol"; +import "./IERC20Transformer.sol"; +import "./LibERC20Transformer.sol"; + + +/// @dev A transformer that wraps or unwraps WETH. +contract WethTransformer is + IERC20Transformer +{ + /// @dev Transform data to ABI-encode and pass into `transform()`. + struct TransformData { + // The token to wrap/unwrap. Must be either ETH or WETH. + IERC20TokenV06 token; + // Amount of `token` to wrap or unwrap. + // `uint(-1)` will unwrap the entire balance. + uint256 amount; + } + + // solhint-disable + /// @dev The WETH contract address. + IEtherTokenV06 public immutable weth; + // solhint-enable + + using LibRichErrorsV06 for bytes; + using LibSafeMathV06 for uint256; + using LibERC20Transformer for IERC20TokenV06; + + /// @dev Construct the transformer and store the WETH address in an immutable. + /// @param weth_ The weth token. + constructor(IEtherTokenV06 weth_) public { + weth = weth_; + } + + /// @dev Wraps and unwraps WETH. + /// @param data_ ABI-encoded `TransformData`, indicating which token to wrap/umwrap. + /// @return success `TRANSFORMER_SUCCESS` on success. + function transform( + bytes32, // callDataHash, + address payable, // taker, + bytes calldata data_ + ) + external + override + returns (bytes4 success) + { + TransformData memory data = abi.decode(data_, (TransformData)); + if (!data.token.isTokenETH() && data.token != weth) { + LibTransformERC20RichErrors.InvalidTransformDataError(data_).rrevert(); + } + + uint256 amount = data.amount; + if (amount == uint256(-1)) { + amount = data.token.getTokenBalanceOf(address(this)); + } + + if (amount != 0) { + if (data.token.isTokenETH()) { + // Wrap ETH. + weth.deposit{value: amount}(); + } else { + // Unwrap WETH. + weth.withdraw(amount); + } + } + return LibERC20Transformer.TRANSFORMER_SUCCESS; + } +} diff --git a/contracts/zero-ex/contracts/src/vendor/v3/IExchange.sol b/contracts/zero-ex/contracts/src/vendor/v3/IExchange.sol new file mode 100644 index 0000000000..7d98b61b87 --- /dev/null +++ b/contracts/zero-ex/contracts/src/vendor/v3/IExchange.sol @@ -0,0 +1,107 @@ +/* + + 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 Interface to the V3 Exchange. +interface IExchange { + + /// @dev V3 Order structure. + struct Order { + // Address that created the order. + address makerAddress; + // Address that is allowed to fill the order. + // If set to 0, any address is allowed to fill the order. + address takerAddress; + // Address that will recieve fees when order is filled. + address feeRecipientAddress; + // Address that is allowed to call Exchange contract methods that affect this order. + // If set to 0, any address is allowed to call these methods. + address senderAddress; + // Amount of makerAsset being offered by maker. Must be greater than 0. + uint256 makerAssetAmount; + // Amount of takerAsset being bid on by maker. Must be greater than 0. + uint256 takerAssetAmount; + // Fee paid to feeRecipient by maker when order is filled. + uint256 makerFee; + // Fee paid to feeRecipient by taker when order is filled. + uint256 takerFee; + // Timestamp in seconds at which order expires. + uint256 expirationTimeSeconds; + // Arbitrary number to facilitate uniqueness of the order's hash. + uint256 salt; + // Encoded data that can be decoded by a specified proxy contract when transferring makerAsset. + // The leading bytes4 references the id of the asset proxy. + bytes makerAssetData; + // Encoded data that can be decoded by a specified proxy contract when transferring takerAsset. + // The leading bytes4 references the id of the asset proxy. + bytes takerAssetData; + // Encoded data that can be decoded by a specified proxy contract when transferring makerFeeAsset. + // The leading bytes4 references the id of the asset proxy. + bytes makerFeeAssetData; + // Encoded data that can be decoded by a specified proxy contract when transferring takerFeeAsset. + // The leading bytes4 references the id of the asset proxy. + bytes takerFeeAssetData; + } + + /// @dev V3 `fillOrder()` results.` + struct FillResults { + // Total amount of makerAsset(s) filled. + uint256 makerAssetFilledAmount; + // Total amount of takerAsset(s) filled. + uint256 takerAssetFilledAmount; + // Total amount of fees paid by maker(s) to feeRecipient(s). + uint256 makerFeePaid; + // Total amount of fees paid by taker to feeRecipients(s). + uint256 takerFeePaid; + // Total amount of fees paid by taker to the staking contract. + uint256 protocolFeePaid; + } + + /// @dev Fills the input order. + /// @param order Order struct containing order specifications. + /// @param takerAssetFillAmount Desired amount of takerAsset to sell. + /// @param signature Proof that order has been created by maker. + /// @return fillResults Amounts filled and fees paid by maker and taker. + function fillOrder( + Order calldata order, + uint256 takerAssetFillAmount, + bytes calldata signature + ) + external + payable + returns (FillResults memory fillResults); + + /// @dev Returns the protocolFeeMultiplier + /// @return multiplier The multiplier for protocol fees. + function protocolFeeMultiplier() + external + view + returns (uint256 multiplier); + + /// @dev Gets an asset proxy. + /// @param assetProxyId Id of the asset proxy. + /// @return proxyAddress The asset proxy registered to assetProxyId. + /// Returns 0x0 if no proxy is registered. + function getAssetProxy(bytes4 assetProxyId) + external + view + returns (address proxyAddress); +} diff --git a/contracts/zero-ex/contracts/test/TestFillQuoteTransformerExchange.sol b/contracts/zero-ex/contracts/test/TestFillQuoteTransformerExchange.sol new file mode 100644 index 0000000000..698ff8c364 --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestFillQuoteTransformerExchange.sol @@ -0,0 +1,145 @@ +/* + + 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-utils/contracts/src/v06/LibBytesV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibMathV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; +import "../src/vendor/v3/IExchange.sol"; +import "./TestMintableERC20Token.sol"; + + +contract TestFillQuoteTransformerExchange { + + struct FillBehavior { + // How much of the order is filled, in taker asset amount. + uint256 filledTakerAssetAmount; + // Scaling for maker assets minted, in 1e18. + uint256 makerAssetMintRatio; + } + + uint256 private constant PROTOCOL_FEE_MULTIPLIER = 1337; + + using LibSafeMathV06 for uint256; + + function fillOrder( + IExchange.Order calldata order, + uint256 takerAssetFillAmount, + bytes calldata signature + ) + external + payable + returns (IExchange.FillResults memory fillResults) + { + require( + signature.length != 0, + "TestFillQuoteTransformerExchange/INVALID_SIGNATURE" + ); + // The signature is the ABI-encoded FillBehavior data. + FillBehavior memory behavior = abi.decode(signature, (FillBehavior)); + + uint256 protocolFee = PROTOCOL_FEE_MULTIPLIER * tx.gasprice; + require( + msg.value == protocolFee, + "TestFillQuoteTransformerExchange/INSUFFICIENT_PROTOCOL_FEE" + ); + // Return excess protocol fee. + msg.sender.transfer(msg.value - protocolFee); + + // Take taker tokens. + TestMintableERC20Token takerToken = _getTokenFromAssetData(order.takerAssetData); + takerAssetFillAmount = LibSafeMathV06.min256( + order.takerAssetAmount.safeSub(behavior.filledTakerAssetAmount), + takerAssetFillAmount + ); + require( + takerToken.getSpendableAmount(msg.sender, address(this)) >= takerAssetFillAmount, + "TestFillQuoteTransformerExchange/INSUFFICIENT_TAKER_FUNDS" + ); + takerToken.transferFrom(msg.sender, order.makerAddress, takerAssetFillAmount); + + // Mint maker tokens. + uint256 makerAssetFilledAmount = LibMathV06.getPartialAmountFloor( + takerAssetFillAmount, + order.takerAssetAmount, + order.makerAssetAmount + ); + TestMintableERC20Token makerToken = _getTokenFromAssetData(order.makerAssetData); + makerToken.mint( + msg.sender, + LibMathV06.getPartialAmountFloor( + behavior.makerAssetMintRatio, + 1e18, + makerAssetFilledAmount + ) + ); + + // Take taker fee. + TestMintableERC20Token takerFeeToken = _getTokenFromAssetData(order.takerFeeAssetData); + uint256 takerFee = LibMathV06.getPartialAmountFloor( + takerAssetFillAmount, + order.takerAssetAmount, + order.takerFee + ); + require( + takerFeeToken.getSpendableAmount(msg.sender, address(this)) >= takerFee, + "TestFillQuoteTransformerExchange/INSUFFICIENT_TAKER_FEE_FUNDS" + ); + takerFeeToken.transferFrom(msg.sender, order.feeRecipientAddress, takerFee); + + fillResults.makerAssetFilledAmount = makerAssetFilledAmount; + fillResults.takerAssetFilledAmount = takerAssetFillAmount; + fillResults.makerFeePaid = uint256(-1); + fillResults.takerFeePaid = takerFee; + fillResults.protocolFeePaid = protocolFee; + } + + function encodeBehaviorData(FillBehavior calldata behavior) + external + pure + returns (bytes memory encoded) + { + return abi.encode(behavior); + } + + function protocolFeeMultiplier() + external + pure + returns (uint256) + { + return PROTOCOL_FEE_MULTIPLIER; + } + + function getAssetProxy(bytes4) + external + view + returns (address) + { + return address(this); + } + + function _getTokenFromAssetData(bytes memory assetData) + private + pure + returns (TestMintableERC20Token token) + { + return TestMintableERC20Token(LibBytesV06.readAddress(assetData, 16)); + } +} diff --git a/contracts/zero-ex/contracts/test/TestFillQuoteTransformerHost.sol b/contracts/zero-ex/contracts/test/TestFillQuoteTransformerHost.sol new file mode 100644 index 0000000000..6b85acd2f5 --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestFillQuoteTransformerHost.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 "../src/transformers/IERC20Transformer.sol"; +import "./TestMintableERC20Token.sol"; +import "./TestTransformerHost.sol"; + + +contract TestFillQuoteTransformerHost is + TestTransformerHost +{ + function executeTransform( + IERC20Transformer transformer, + TestMintableERC20Token inputToken, + uint256 inputTokenAmount, + bytes calldata data + ) + external + payable + { + if (inputTokenAmount != 0) { + inputToken.mint(address(this), inputTokenAmount); + } + // Have to make this call externally because transformers aren't payable. + this.rawExecuteTransform(transformer, bytes32(0), msg.sender, data); + } +} diff --git a/contracts/zero-ex/contracts/test/TestMintableERC20Token.sol b/contracts/zero-ex/contracts/test/TestMintableERC20Token.sol index 27ef1209fd..f2015c6b70 100644 --- a/contracts/zero-ex/contracts/test/TestMintableERC20Token.sol +++ b/contracts/zero-ex/contracts/test/TestMintableERC20Token.sol @@ -74,4 +74,14 @@ contract TestMintableERC20Token { balanceOf[to] += amount; return true; } + + function getSpendableAmount(address owner, address spender) + external + view + returns (uint256) + { + return balanceOf[owner] < allowance[owner][spender] + ? balanceOf[owner] + : allowance[owner][spender]; + } } diff --git a/contracts/zero-ex/contracts/test/TestTransformerHost.sol b/contracts/zero-ex/contracts/test/TestTransformerHost.sol new file mode 100644 index 0000000000..23f2fff764 --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestTransformerHost.sol @@ -0,0 +1,60 @@ +/* + + 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-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; +import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; +import "../src/transformers/IERC20Transformer.sol"; +import "../src/transformers/LibERC20Transformer.sol"; + + +contract TestTransformerHost { + + using LibERC20Transformer for IERC20TokenV06; + using LibRichErrorsV06 for bytes; + + function rawExecuteTransform( + IERC20Transformer transformer, + bytes32 callDataHash, + address taker, + bytes calldata data + ) + external + { + (bool success, bytes memory resultData) = + address(transformer).delegatecall(abi.encodeWithSelector( + transformer.transform.selector, + callDataHash, + taker, + data + )); + if (!success) { + resultData.rrevert(); + } + require( + abi.decode(resultData, (bytes4)) == LibERC20Transformer.TRANSFORMER_SUCCESS, + "TestFillQuoteTransformerTaker/UNSUCCESSFUL_RESULT" + ); + } + + // solhint-disable + receive() external payable {} + // solhint-enable +} diff --git a/contracts/zero-ex/contracts/test/TestWeth.sol b/contracts/zero-ex/contracts/test/TestWeth.sol new file mode 100644 index 0000000000..5a4f622bb5 --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestWeth.sol @@ -0,0 +1,42 @@ +/* + + 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 "./TestMintableERC20Token.sol"; + + +contract TestWeth is + TestMintableERC20Token +{ + function deposit() + external + payable + { + this.mint(msg.sender, msg.value); + } + + function withdraw(uint256 amount) + external + { + require(balanceOf[msg.sender] >= amount, "TestWeth/INSUFFICIENT_FUNDS"); + balanceOf[msg.sender] -= amount; + msg.sender.transfer(amount); + } +} diff --git a/contracts/zero-ex/contracts/test/TestWethTransformerHost.sol b/contracts/zero-ex/contracts/test/TestWethTransformerHost.sol new file mode 100644 index 0000000000..3c0fd83999 --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestWethTransformerHost.sol @@ -0,0 +1,53 @@ +/* + + 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 "../src/transformers/IERC20Transformer.sol"; +import "./TestMintableERC20Token.sol"; +import "./TestTransformerHost.sol"; +import "./TestWeth.sol"; + + +contract TestWethTransformerHost is + TestTransformerHost +{ + // solhint-disable + TestWeth private immutable _weth; + // solhint-enable + + constructor(TestWeth weth) public { + _weth = weth; + } + + function executeTransform( + uint256 wethAmount, + IERC20Transformer transformer, + bytes calldata data + ) + external + payable + { + if (wethAmount != 0) { + _weth.deposit{value: wethAmount}(); + } + // Have to make this call externally because transformers aren't payable. + this.rawExecuteTransform(transformer, bytes32(0), msg.sender, data); + } +} diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index d1f05183e8..6a69f8eb38 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -38,7 +38,7 @@ "docs:json": "typedoc --excludePrivate --excludeExternals --excludeProtected --ignoreCompilerErrors --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES" }, "config": { - "publicInterfaceContracts": "ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnable,ISimpleFunctionRegistry,ITokenSpender,ITransformERC20", + "publicInterfaceContracts": "ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnable,ISimpleFunctionRegistry,ITokenSpender,ITransformERC20,FillQuoteTransformer,PayTakerTransformer,WethTransformer", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", "abis": "./test/generated-artifacts/@(AllowanceTarget|Bootstrap|FixinCommon|FlashWallet|FullMigration|IAllowanceTarget|IBootstrap|IERC20Transformer|IFeature|IFlashWallet|IOwnable|ISimpleFunctionRegistry|ITestSimpleFunctionRegistryFeature|ITokenSpender|ITransformERC20|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|Ownable|SimpleFunctionRegistry|TestCallTarget|TestFullMigration|TestInitialMigration|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestZeroExFeature|TokenSpender|TransformERC20|ZeroEx).json" }, @@ -56,6 +56,7 @@ "@0x/contracts-gen": "^2.0.8", "@0x/contracts-test-utils": "^5.3.2", "@0x/dev-utils": "^3.2.1", + "@0x/order-utils": "^10.2.4", "@0x/sol-compiler": "^4.0.8", "@0x/subproviders": "^6.0.8", "@0x/ts-doc-gen": "^0.0.22", diff --git a/contracts/zero-ex/src/artifacts.ts b/contracts/zero-ex/src/artifacts.ts index fea6a4beb1..f68d20e56b 100644 --- a/contracts/zero-ex/src/artifacts.ts +++ b/contracts/zero-ex/src/artifacts.ts @@ -5,25 +5,31 @@ */ import { ContractArtifact } from 'ethereum-types'; +import * as FillQuoteTransformer from '../generated-artifacts/FillQuoteTransformer.json'; import * as FullMigration from '../generated-artifacts/FullMigration.json'; -import * as IAllowanceTarget from '../generated-artifacts/IAllowanceTarget.json'; import * as IERC20Transformer from '../generated-artifacts/IERC20Transformer.json'; -import * as IFlashWallet from '../generated-artifacts/IFlashWallet.json'; import * as InitialMigration from '../generated-artifacts/InitialMigration.json'; import * as IOwnable from '../generated-artifacts/IOwnable.json'; import * as ISimpleFunctionRegistry from '../generated-artifacts/ISimpleFunctionRegistry.json'; import * as ITokenSpender from '../generated-artifacts/ITokenSpender.json'; import * as ITransformERC20 from '../generated-artifacts/ITransformERC20.json'; +import * as LibERC20Transformer from '../generated-artifacts/LibERC20Transformer.json'; +import * as PayTakerTransformer from '../generated-artifacts/PayTakerTransformer.json'; +import * as Puppet from '../generated-artifacts/Puppet.json'; +import * as WethTransformer from '../generated-artifacts/WethTransformer.json'; import * as ZeroEx from '../generated-artifacts/ZeroEx.json'; export const artifacts = { ZeroEx: ZeroEx as ContractArtifact, FullMigration: FullMigration as ContractArtifact, InitialMigration: InitialMigration as ContractArtifact, - IFlashWallet: IFlashWallet as ContractArtifact, - IAllowanceTarget: IAllowanceTarget as ContractArtifact, + Puppet: Puppet as ContractArtifact, IERC20Transformer: IERC20Transformer as ContractArtifact, IOwnable: IOwnable as ContractArtifact, ISimpleFunctionRegistry: ISimpleFunctionRegistry as ContractArtifact, ITokenSpender: ITokenSpender as ContractArtifact, ITransformERC20: ITransformERC20 as ContractArtifact, + LibERC20Transformer: LibERC20Transformer as ContractArtifact, + PayTakerTransformer: PayTakerTransformer as ContractArtifact, + WethTransformer: WethTransformer as ContractArtifact, + FillQuoteTransformer: FillQuoteTransformer as ContractArtifact, }; diff --git a/contracts/zero-ex/src/constants.ts b/contracts/zero-ex/src/constants.ts new file mode 100644 index 0000000000..2c49072817 --- /dev/null +++ b/contracts/zero-ex/src/constants.ts @@ -0,0 +1,4 @@ +/* + * The pseudo-token address for ETH used by `tranformERC20()`. + */ +export const ETH_TOKEN_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; diff --git a/contracts/zero-ex/src/index.ts b/contracts/zero-ex/src/index.ts index b9e4913d08..c2c9cebc48 100644 --- a/contracts/zero-ex/src/index.ts +++ b/contracts/zero-ex/src/index.ts @@ -1,9 +1,14 @@ export { artifacts } from './artifacts'; export { + FillQuoteTransformerContract, IOwnableContract, IOwnableEvents, ISimpleFunctionRegistryContract, ISimpleFunctionRegistryEvents, + ITokenSpenderContract, + ITransformERC20Contract, + PayTakerTransformerContract, + WethTransformerContract, ZeroExContract, } from './wrappers'; export { ZeroExRevertErrors } from '@0x/utils'; @@ -36,4 +41,6 @@ export { TupleDataItem, StateMutability, } from 'ethereum-types'; -export { rlpEncodeNonce } from './nonce_utils'; + +export * from './constants'; +export * from './transformer_data_encoders'; diff --git a/contracts/zero-ex/src/transformer_data_encoders.ts b/contracts/zero-ex/src/transformer_data_encoders.ts new file mode 100644 index 0000000000..b44bea7631 --- /dev/null +++ b/contracts/zero-ex/src/transformer_data_encoders.ts @@ -0,0 +1,114 @@ +import { Order } from '@0x/types'; +import { AbiEncoder, BigNumber } from '@0x/utils'; + +const ORDER_ABI_COMPONENTS = [ + { name: 'makerAddress', type: 'address' }, + { name: 'takerAddress', type: 'address' }, + { name: 'feeRecipientAddress', type: 'address' }, + { name: 'senderAddress', type: 'address' }, + { name: 'makerAssetAmount', type: 'uint256' }, + { name: 'takerAssetAmount', type: 'uint256' }, + { name: 'makerFee', type: 'uint256' }, + { name: 'takerFee', type: 'uint256' }, + { name: 'expirationTimeSeconds', type: 'uint256' }, + { name: 'salt', type: 'uint256' }, + { name: 'makerAssetData', type: 'bytes' }, + { name: 'takerAssetData', type: 'bytes' }, + { name: 'makerFeeAssetData', type: 'bytes' }, + { name: 'takerFeeAssetData', type: 'bytes' }, +]; + +/** + * ABI encoder for `FillQuoteTransformer.TransformData` + */ +export const fillQuoteTransformerDataEncoder = AbiEncoder.create([ + { + name: 'data', + type: 'tuple', + components: [ + { name: 'sellToken', type: 'address' }, + { name: 'buyToken', type: 'address' }, + { + name: 'orders', + type: 'tuple[]', + components: ORDER_ABI_COMPONENTS, + }, + { name: 'signatures', type: 'bytes[]' }, + { name: 'maxOrderFillAmounts', type: 'uint256[]' }, + { name: 'sellAmount', type: 'uint256' }, + { name: 'buyAmount', type: 'uint256' }, + ], + }, +]); + +/** + * `FillQuoteTransformer.TransformData` + */ +export interface FillQuoteTransformerData { + sellToken: string; + buyToken: string; + orders: Array>; + signatures: string[]; + maxOrderFillAmounts: BigNumber[]; + sellAmount: BigNumber; + buyAmount: BigNumber; +} + +/** + * ABI-encode a `FillQuoteTransformer.TransformData` type. + */ +export function encodeFillQuoteTransformerData(data: FillQuoteTransformerData): string { + return fillQuoteTransformerDataEncoder.encode([data]); +} + +/** + * ABI encoder for `WethTransformer.TransformData` + */ +export const wethTransformerDataEncoder = AbiEncoder.create([ + { + name: 'data', + type: 'tuple', + components: [{ name: 'token', type: 'address' }, { name: 'amount', type: 'uint256' }], + }, +]); + +/** + * `WethTransformer.TransformData` + */ +export interface WethTransformerData { + token: string; + amount: BigNumber; +} + +/** + * ABI-encode a `WethTransformer.TransformData` type. + */ +export function encodeWethTransformerData(data: WethTransformerData): string { + return wethTransformerDataEncoder.encode([data]); +} + +/** + * ABI encoder for `PayTakerTransformer.TransformData` + */ +export const payTakerTransformerDataEncoder = AbiEncoder.create([ + { + name: 'data', + type: 'tuple', + components: [{ name: 'tokens', type: 'address[]' }, { name: 'amounts', type: 'uint256[]' }], + }, +]); + +/** + * `PayTakerTransformer.TransformData` + */ +export interface PayTakerTransformerData { + tokens: string[]; + amounts: BigNumber[]; +} + +/** + * ABI-encode a `PayTakerTransformer.TransformData` type. + */ +export function encodePayTakerTransformerData(data: PayTakerTransformerData): string { + return payTakerTransformerDataEncoder.encode([data]); +} diff --git a/contracts/zero-ex/src/wrappers.ts b/contracts/zero-ex/src/wrappers.ts index 7be4d42f1e..fdbf2d49c0 100644 --- a/contracts/zero-ex/src/wrappers.ts +++ b/contracts/zero-ex/src/wrappers.ts @@ -3,13 +3,16 @@ * Warning: This file is auto-generated by contracts-gen. Don't edit manually. * ----------------------------------------------------------------------------- */ +export * from '../generated-wrappers/fill_quote_transformer'; export * from '../generated-wrappers/full_migration'; -export * from '../generated-wrappers/i_allowance_target'; export * from '../generated-wrappers/i_erc20_transformer'; -export * from '../generated-wrappers/i_flash_wallet'; export * from '../generated-wrappers/i_ownable'; export * from '../generated-wrappers/i_simple_function_registry'; export * from '../generated-wrappers/i_token_spender'; export * from '../generated-wrappers/i_transform_erc20'; export * from '../generated-wrappers/initial_migration'; +export * from '../generated-wrappers/lib_erc20_transformer'; +export * from '../generated-wrappers/pay_taker_transformer'; +export * from '../generated-wrappers/puppet'; +export * from '../generated-wrappers/weth_transformer'; export * from '../generated-wrappers/zero_ex'; diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index cf9c020daa..26a642efb7 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -7,16 +7,17 @@ import { ContractArtifact } from 'ethereum-types'; import * as AllowanceTarget from '../test/generated-artifacts/AllowanceTarget.json'; import * as Bootstrap from '../test/generated-artifacts/Bootstrap.json'; +import * as FillQuoteTransformer from '../test/generated-artifacts/FillQuoteTransformer.json'; import * as FixinCommon from '../test/generated-artifacts/FixinCommon.json'; -import * as FlashWallet from '../test/generated-artifacts/FlashWallet.json'; import * as FullMigration from '../test/generated-artifacts/FullMigration.json'; import * as IAllowanceTarget from '../test/generated-artifacts/IAllowanceTarget.json'; import * as IBootstrap from '../test/generated-artifacts/IBootstrap.json'; import * as IERC20Transformer from '../test/generated-artifacts/IERC20Transformer.json'; +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 InitialMigration from '../test/generated-artifacts/InitialMigration.json'; import * as IOwnable from '../test/generated-artifacts/IOwnable.json'; +import * as IPuppet from '../test/generated-artifacts/IPuppet.json'; import * as ISimpleFunctionRegistry from '../test/generated-artifacts/ISimpleFunctionRegistry.json'; import * as ITestSimpleFunctionRegistryFeature from '../test/generated-artifacts/ITestSimpleFunctionRegistryFeature.json'; import * as ITokenSpender from '../test/generated-artifacts/ITokenSpender.json'; @@ -29,6 +30,7 @@ import * as LibOwnableRichErrors from '../test/generated-artifacts/LibOwnableRic import * as LibOwnableStorage from '../test/generated-artifacts/LibOwnableStorage.json'; import * as LibProxyRichErrors from '../test/generated-artifacts/LibProxyRichErrors.json'; import * as LibProxyStorage from '../test/generated-artifacts/LibProxyStorage.json'; +import * as LibPuppetRichErrors from '../test/generated-artifacts/LibPuppetRichErrors.json'; import * as LibSimpleFunctionRegistryRichErrors from '../test/generated-artifacts/LibSimpleFunctionRegistryRichErrors.json'; import * as LibSimpleFunctionRegistryStorage from '../test/generated-artifacts/LibSimpleFunctionRegistryStorage.json'; import * as LibSpenderRichErrors from '../test/generated-artifacts/LibSpenderRichErrors.json'; @@ -36,37 +38,44 @@ import * as LibStorage from '../test/generated-artifacts/LibStorage.json'; import * as LibTokenSpenderStorage from '../test/generated-artifacts/LibTokenSpenderStorage.json'; 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 Ownable from '../test/generated-artifacts/Ownable.json'; +import * as PayTakerTransformer from '../test/generated-artifacts/PayTakerTransformer.json'; +import * as Puppet from '../test/generated-artifacts/Puppet.json'; import * as SimpleFunctionRegistry from '../test/generated-artifacts/SimpleFunctionRegistry.json'; -import * as TestCallTarget from '../test/generated-artifacts/TestCallTarget.json'; +import * as TestFillQuoteTransformerExchange from '../test/generated-artifacts/TestFillQuoteTransformerExchange.json'; +import * as TestFillQuoteTransformerHost from '../test/generated-artifacts/TestFillQuoteTransformerHost.json'; import * as TestFullMigration from '../test/generated-artifacts/TestFullMigration.json'; import * as TestInitialMigration from '../test/generated-artifacts/TestInitialMigration.json'; import * as TestMigrator from '../test/generated-artifacts/TestMigrator.json'; import * as TestMintableERC20Token from '../test/generated-artifacts/TestMintableERC20Token.json'; import * as TestMintTokenERC20Transformer from '../test/generated-artifacts/TestMintTokenERC20Transformer.json'; +import * as TestPuppetTarget from '../test/generated-artifacts/TestPuppetTarget.json'; import * as TestSimpleFunctionRegistryFeatureImpl1 from '../test/generated-artifacts/TestSimpleFunctionRegistryFeatureImpl1.json'; import * as TestSimpleFunctionRegistryFeatureImpl2 from '../test/generated-artifacts/TestSimpleFunctionRegistryFeatureImpl2.json'; import * as TestTokenSpender from '../test/generated-artifacts/TestTokenSpender.json'; import * as TestTokenSpenderERC20Token from '../test/generated-artifacts/TestTokenSpenderERC20Token.json'; import * as TestTransformERC20 from '../test/generated-artifacts/TestTransformERC20.json'; +import * as TestTransformerHost from '../test/generated-artifacts/TestTransformerHost.json'; +import * as TestWeth from '../test/generated-artifacts/TestWeth.json'; +import * as TestWethTransformerHost from '../test/generated-artifacts/TestWethTransformerHost.json'; import * as TestZeroExFeature from '../test/generated-artifacts/TestZeroExFeature.json'; import * as TokenSpender from '../test/generated-artifacts/TokenSpender.json'; import * as TransformERC20 from '../test/generated-artifacts/TransformERC20.json'; +import * as WethTransformer from '../test/generated-artifacts/WethTransformer.json'; import * as ZeroEx from '../test/generated-artifacts/ZeroEx.json'; export const artifacts = { ZeroEx: ZeroEx as ContractArtifact, LibCommonRichErrors: LibCommonRichErrors as ContractArtifact, LibOwnableRichErrors: LibOwnableRichErrors as ContractArtifact, LibProxyRichErrors: LibProxyRichErrors as ContractArtifact, + LibPuppetRichErrors: LibPuppetRichErrors as ContractArtifact, LibSimpleFunctionRegistryRichErrors: LibSimpleFunctionRegistryRichErrors as ContractArtifact, LibSpenderRichErrors: LibSpenderRichErrors as ContractArtifact, LibTransformERC20RichErrors: LibTransformERC20RichErrors as ContractArtifact, - LibWalletRichErrors: LibWalletRichErrors as ContractArtifact, AllowanceTarget: AllowanceTarget as ContractArtifact, - FlashWallet: FlashWallet as ContractArtifact, IAllowanceTarget: IAllowanceTarget as ContractArtifact, - IFlashWallet: IFlashWallet as ContractArtifact, + IPuppet: IPuppet as ContractArtifact, + Puppet: Puppet as ContractArtifact, Bootstrap: Bootstrap as ContractArtifact, IBootstrap: IBootstrap as ContractArtifact, IFeature: IFeature as ContractArtifact, @@ -89,19 +98,28 @@ export const artifacts = { LibStorage: LibStorage as ContractArtifact, LibTokenSpenderStorage: LibTokenSpenderStorage as ContractArtifact, LibTransformERC20Storage: LibTransformERC20Storage as ContractArtifact, + FillQuoteTransformer: FillQuoteTransformer as ContractArtifact, IERC20Transformer: IERC20Transformer as ContractArtifact, LibERC20Transformer: LibERC20Transformer as ContractArtifact, + PayTakerTransformer: PayTakerTransformer as ContractArtifact, + WethTransformer: WethTransformer as ContractArtifact, + IExchange: IExchange as ContractArtifact, ITestSimpleFunctionRegistryFeature: ITestSimpleFunctionRegistryFeature as ContractArtifact, - TestCallTarget: TestCallTarget as ContractArtifact, + TestFillQuoteTransformerExchange: TestFillQuoteTransformerExchange as ContractArtifact, + TestFillQuoteTransformerHost: TestFillQuoteTransformerHost as ContractArtifact, TestFullMigration: TestFullMigration as ContractArtifact, TestInitialMigration: TestInitialMigration as ContractArtifact, TestMigrator: TestMigrator as ContractArtifact, TestMintTokenERC20Transformer: TestMintTokenERC20Transformer as ContractArtifact, TestMintableERC20Token: TestMintableERC20Token as ContractArtifact, + TestPuppetTarget: TestPuppetTarget as ContractArtifact, TestSimpleFunctionRegistryFeatureImpl1: TestSimpleFunctionRegistryFeatureImpl1 as ContractArtifact, TestSimpleFunctionRegistryFeatureImpl2: TestSimpleFunctionRegistryFeatureImpl2 as ContractArtifact, TestTokenSpender: TestTokenSpender as ContractArtifact, TestTokenSpenderERC20Token: TestTokenSpenderERC20Token as ContractArtifact, TestTransformERC20: TestTransformERC20 as ContractArtifact, + TestTransformerHost: TestTransformerHost as ContractArtifact, + TestWeth: TestWeth as ContractArtifact, + TestWethTransformerHost: TestWethTransformerHost as ContractArtifact, TestZeroExFeature: TestZeroExFeature as ContractArtifact, }; diff --git a/contracts/zero-ex/test/features/transform_erc20_test.ts b/contracts/zero-ex/test/features/transform_erc20_test.ts index 28daa667f0..82bb3aee90 100644 --- a/contracts/zero-ex/test/features/transform_erc20_test.ts +++ b/contracts/zero-ex/test/features/transform_erc20_test.ts @@ -10,6 +10,7 @@ import { } from '@0x/contracts-test-utils'; import { AbiEncoder, hexUtils, ZeroExRevertErrors } from '@0x/utils'; +import { ETH_TOKEN_ADDRESS } from '../../src/constants'; import { getRLPEncodedAccountNonceAsync } from '../../src/nonce_utils'; import { artifacts } from '../artifacts'; import { abis } from '../utils/abis'; @@ -206,8 +207,6 @@ blockchainTests.resets('TransformERC20 feature', env => { ); }); - const ETH_TOKEN_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; - it("succeeds if taker's output token balance increases by exactly minOutputTokenAmount, with ETH", async () => { const startingInputTokenBalance = getRandomInteger(0, '100e18'); await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync(); diff --git a/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts b/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts new file mode 100644 index 0000000000..05e920a6b0 --- /dev/null +++ b/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts @@ -0,0 +1,849 @@ +import { + assertIntegerRoughlyEquals, + blockchainTests, + constants, + expect, + getRandomInteger, + Numberish, + randomAddress, +} from '@0x/contracts-test-utils'; +import { assetDataUtils } from '@0x/order-utils'; +import { Order } from '@0x/types'; +import { BigNumber, hexUtils, ZeroExRevertErrors } from '@0x/utils'; +import * as _ from 'lodash'; + +import { encodeFillQuoteTransformerData, FillQuoteTransformerData } from '../../src/transformer_data_encoders'; +import { artifacts } from '../artifacts'; +import { + FillQuoteTransformerContract, + TestFillQuoteTransformerExchangeContract, + TestFillQuoteTransformerHostContract, + TestMintableERC20TokenContract, +} from '../wrappers'; + +const { NULL_ADDRESS, NULL_BYTES, MAX_UINT256, ZERO_AMOUNT } = constants; + +blockchainTests.resets('FillQuoteTransformer', env => { + let maker: string; + let feeRecipient: string; + let exchange: TestFillQuoteTransformerExchangeContract; + let transformer: FillQuoteTransformerContract; + let host: TestFillQuoteTransformerHostContract; + let makerToken: TestMintableERC20TokenContract; + let takerToken: TestMintableERC20TokenContract; + let takerFeeToken: TestMintableERC20TokenContract; + let singleProtocolFee: BigNumber; + + const GAS_PRICE = 1337; + + before(async () => { + [maker, feeRecipient] = await env.getAccountAddressesAsync(); + exchange = await TestFillQuoteTransformerExchangeContract.deployFrom0xArtifactAsync( + artifacts.TestFillQuoteTransformerExchange, + env.provider, + env.txDefaults, + artifacts, + ); + transformer = await FillQuoteTransformerContract.deployFrom0xArtifactAsync( + artifacts.FillQuoteTransformer, + env.provider, + env.txDefaults, + artifacts, + exchange.address, + ); + host = await TestFillQuoteTransformerHostContract.deployFrom0xArtifactAsync( + artifacts.TestFillQuoteTransformerHost, + env.provider, + { + ...env.txDefaults, + gasPrice: GAS_PRICE, + }, + artifacts, + ); + [makerToken, takerToken, takerFeeToken] = await Promise.all( + _.times(3, async () => + TestMintableERC20TokenContract.deployFrom0xArtifactAsync( + artifacts.TestMintableERC20Token, + env.provider, + env.txDefaults, + artifacts, + ), + ), + ); + singleProtocolFee = (await exchange.protocolFeeMultiplier().callAsync()).times(GAS_PRICE); + }); + + type FilledOrder = Order & { filledTakerAssetAmount: BigNumber }; + + function createOrder(fields: Partial = {}): FilledOrder { + return { + chainId: 1, + exchangeAddress: exchange.address, + expirationTimeSeconds: ZERO_AMOUNT, + salt: ZERO_AMOUNT, + senderAddress: NULL_ADDRESS, + takerAddress: NULL_ADDRESS, + makerAddress: maker, + feeRecipientAddress: feeRecipient, + makerAssetAmount: getRandomInteger('0.1e18', '1e18'), + takerAssetAmount: getRandomInteger('0.1e18', '1e18'), + makerFee: ZERO_AMOUNT, + takerFee: getRandomInteger('0.001e18', '0.1e18'), + makerAssetData: assetDataUtils.encodeERC20AssetData(makerToken.address), + takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken.address), + makerFeeAssetData: NULL_BYTES, + takerFeeAssetData: assetDataUtils.encodeERC20AssetData(takerToken.address), + filledTakerAssetAmount: ZERO_AMOUNT, + ...fields, + }; + } + + interface QuoteFillResults { + makerAssetBought: BigNumber; + takerAssetSpent: BigNumber; + protocolFeePaid: BigNumber; + } + + const ZERO_QUOTE_FILL_RESULTS = { + makerAssetBought: ZERO_AMOUNT, + takerAssetSpent: ZERO_AMOUNT, + protocolFeePaid: ZERO_AMOUNT, + }; + + function getExpectedSellQuoteFillResults( + orders: FilledOrder[], + takerAssetFillAmount: BigNumber = constants.MAX_UINT256, + ): QuoteFillResults { + const qfr = { ...ZERO_QUOTE_FILL_RESULTS }; + for (const order of orders) { + if (qfr.takerAssetSpent.gte(takerAssetFillAmount)) { + break; + } + const singleFillAmount = BigNumber.min( + takerAssetFillAmount.minus(qfr.takerAssetSpent), + order.takerAssetAmount.minus(order.filledTakerAssetAmount), + ); + const fillRatio = singleFillAmount.div(order.takerAssetAmount); + qfr.takerAssetSpent = qfr.takerAssetSpent.plus(singleFillAmount); + qfr.protocolFeePaid = qfr.protocolFeePaid.plus(singleProtocolFee); + qfr.makerAssetBought = qfr.makerAssetBought.plus( + fillRatio.times(order.makerAssetAmount).integerValue(BigNumber.ROUND_DOWN), + ); + const takerFee = fillRatio.times(order.takerFee).integerValue(BigNumber.ROUND_DOWN); + if (order.takerAssetData === order.takerFeeAssetData) { + // Taker fee is in taker asset. + qfr.takerAssetSpent = qfr.takerAssetSpent.plus(takerFee); + } else if (order.makerAssetData === order.takerFeeAssetData) { + // Taker fee is in maker asset. + qfr.makerAssetBought = qfr.makerAssetBought.minus(takerFee); + } + } + return qfr; + } + + function getExpectedBuyQuoteFillResults( + orders: FilledOrder[], + makerAssetFillAmount: BigNumber = constants.MAX_UINT256, + ): QuoteFillResults { + const qfr = { ...ZERO_QUOTE_FILL_RESULTS }; + for (const order of orders) { + if (qfr.makerAssetBought.gte(makerAssetFillAmount)) { + break; + } + const filledMakerAssetAmount = order.filledTakerAssetAmount + .times(order.makerAssetAmount.div(order.takerAssetAmount)) + .integerValue(BigNumber.ROUND_DOWN); + const singleFillAmount = BigNumber.min( + makerAssetFillAmount.minus(qfr.makerAssetBought), + order.makerAssetAmount.minus(filledMakerAssetAmount), + ); + const fillRatio = singleFillAmount.div(order.makerAssetAmount); + qfr.takerAssetSpent = qfr.takerAssetSpent.plus( + fillRatio.times(order.takerAssetAmount).integerValue(BigNumber.ROUND_UP), + ); + qfr.protocolFeePaid = qfr.protocolFeePaid.plus(singleProtocolFee); + qfr.makerAssetBought = qfr.makerAssetBought.plus(singleFillAmount); + const takerFee = fillRatio.times(order.takerFee).integerValue(BigNumber.ROUND_UP); + if (order.takerAssetData === order.takerFeeAssetData) { + // Taker fee is in taker asset. + qfr.takerAssetSpent = qfr.takerAssetSpent.plus(takerFee); + } else if (order.makerAssetData === order.takerFeeAssetData) { + // Taker fee is in maker asset. + qfr.makerAssetBought = qfr.makerAssetBought.minus(takerFee); + } + } + return qfr; + } + + interface Balances { + makerAssetBalance: BigNumber; + takerAssetBalance: BigNumber; + takerFeeBalance: BigNumber; + protocolFeeBalance: BigNumber; + } + + const ZERO_BALANCES = { + makerAssetBalance: ZERO_AMOUNT, + takerAssetBalance: ZERO_AMOUNT, + takerFeeBalance: ZERO_AMOUNT, + protocolFeeBalance: ZERO_AMOUNT, + }; + + async function getBalancesAsync(owner: string): Promise { + const balances = { ...ZERO_BALANCES }; + [ + balances.makerAssetBalance, + balances.takerAssetBalance, + balances.takerFeeBalance, + balances.protocolFeeBalance, + ] = await Promise.all([ + makerToken.balanceOf(owner).callAsync(), + takerToken.balanceOf(owner).callAsync(), + takerFeeToken.balanceOf(owner).callAsync(), + env.web3Wrapper.getBalanceInWeiAsync(owner), + ]); + return balances; + } + + function assertBalances(actual: Balances, expected: Balances): void { + assertIntegerRoughlyEquals(actual.makerAssetBalance, expected.makerAssetBalance, 10, 'makerAssetBalance'); + assertIntegerRoughlyEquals(actual.takerAssetBalance, expected.takerAssetBalance, 10, 'takerAssetBalance'); + assertIntegerRoughlyEquals(actual.takerFeeBalance, expected.takerFeeBalance, 10, 'takerFeeBalance'); + assertIntegerRoughlyEquals(actual.protocolFeeBalance, expected.protocolFeeBalance, 10, 'protocolFeeBalance'); + } + + function encodeTransformData(fields: Partial = {}): string { + return encodeFillQuoteTransformerData({ + sellToken: takerToken.address, + buyToken: makerToken.address, + orders: [], + signatures: [], + maxOrderFillAmounts: [], + sellAmount: MAX_UINT256, + buyAmount: ZERO_AMOUNT, + ...fields, + }); + } + + function encodeExchangeBehavior( + filledTakerAssetAmount: Numberish = 0, + makerAssetMintRatio: Numberish = 1.0, + ): string { + return hexUtils.slice( + exchange + .encodeBehaviorData({ + filledTakerAssetAmount: new BigNumber(filledTakerAssetAmount), + makerAssetMintRatio: new BigNumber(makerAssetMintRatio).times('1e18').integerValue(), + }) + .getABIEncodedTransactionData(), + 4, + ); + } + + const ERC20_ASSET_PROXY_ID = '0xf47261b0'; + + describe('sell quotes', () => { + it('can fully sell to a single order quote', async () => { + const orders = _.times(1, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + + it('can fully sell to multi order quote', async () => { + const orders = _.times(3, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + + it('can partially sell to single order quote', async () => { + const orders = _.times(1, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults( + orders, + getExpectedSellQuoteFillResults(orders).takerAssetSpent.dividedToIntegerBy(2), + ); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + + it('can partially sell to multi order quote and refund unused protocol fees', async () => { + const orders = _.times(3, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders.slice(0, 2)); + const maxProtocolFees = singleProtocolFee.times(orders.length); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + .awaitTransactionSuccessAsync({ value: maxProtocolFees }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + protocolFeeBalance: singleProtocolFee, + }); + }); + + it('can sell to multi order quote with a failing order', async () => { + const orders = _.times(3, () => createOrder()); + // First order will fail. + const validOrders = orders.slice(1); + const signatures = [NULL_BYTES, ...validOrders.map(() => encodeExchangeBehavior())]; + const qfr = getExpectedSellQuoteFillResults(validOrders); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + + it('succeeds if an order transfers too few maker tokens', async () => { + const mintScale = 0.5; + const orders = _.times(3, () => createOrder()); + // First order mints less than expected. + const signatures = [ + encodeExchangeBehavior(0, mintScale), + ...orders.slice(1).map(() => encodeExchangeBehavior()), + ]; + const qfr = getExpectedSellQuoteFillResults(orders); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought + .minus(orders[0].makerAssetAmount.times(1 - mintScale)) + .integerValue(BigNumber.ROUND_DOWN), + }); + }); + + it('can fail if an order is partially filled', async () => { + const orders = _.times(3, () => createOrder()); + // First order is partially filled. + const filledOrder = { + ...orders[0], + filledTakerAssetAmount: orders[0].takerAssetAmount.dividedToIntegerBy(2), + }; + // First order is partially filled. + const signatures = [ + encodeExchangeBehavior(filledOrder.filledTakerAssetAmount), + ...orders.slice(1).map(() => encodeExchangeBehavior()), + ]; + const qfr = getExpectedSellQuoteFillResults(orders); + const tx = host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.TransformERC20.IncompleteFillSellQuoteError( + takerToken.address, + getExpectedSellQuoteFillResults([filledOrder, ...orders.slice(1)]).takerAssetSpent, + qfr.takerAssetSpent, + ), + ); + }); + + it('fails if not enough protocol fee provided', async () => { + const orders = _.times(3, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders); + const tx = host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid.minus(1) }); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.TransformERC20.InsufficientProtocolFeeError( + singleProtocolFee.minus(1), + singleProtocolFee, + ), + ); + }); + + it('can sell less than the taker token balance', async () => { + const orders = _.times(3, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders); + const takerTokenBalance = qfr.takerAssetSpent.times(1.01).integerValue(); + await host + .executeTransform( + transformer.address, + takerToken.address, + takerTokenBalance, + encodeTransformData({ + orders, + signatures, + sellAmount: qfr.takerAssetSpent, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + takerAssetBalance: qfr.takerAssetSpent.times(0.01).integerValue(), + }); + }); + + it('fails to sell more than the taker token balance', async () => { + const orders = _.times(3, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders); + const takerTokenBalance = qfr.takerAssetSpent.times(0.99).integerValue(); + const tx = host + .executeTransform( + transformer.address, + takerToken.address, + takerTokenBalance, + encodeTransformData({ + orders, + signatures, + sellAmount: qfr.takerAssetSpent, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.TransformERC20.IncompleteFillSellQuoteError( + takerToken.address, + getExpectedSellQuoteFillResults(orders.slice(0, 2)).takerAssetSpent, + qfr.takerAssetSpent, + ), + ); + }); + + it('can fully sell to a single order with maker asset taker fees', async () => { + const orders = _.times(1, () => + createOrder({ + takerFeeAssetData: assetDataUtils.encodeERC20AssetData(makerToken.address), + }), + ); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + + it('fails if an order has a non-standard taker fee asset', async () => { + const BAD_ASSET_DATA = hexUtils.random(36); + const orders = _.times(1, () => createOrder({ takerFeeAssetData: BAD_ASSET_DATA })); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders); + const tx = host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.TransformERC20.InvalidERC20AssetDataError(BAD_ASSET_DATA), + ); + }); + + it('fails if an order has a fee asset that is neither maker or taker asset', async () => { + const badToken = randomAddress(); + const BAD_ASSET_DATA = hexUtils.concat(ERC20_ASSET_PROXY_ID, hexUtils.leftPad(badToken)); + const orders = _.times(1, () => createOrder({ takerFeeAssetData: BAD_ASSET_DATA })); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders); + const tx = host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + return expect(tx).to.revertWith(new ZeroExRevertErrors.TransformERC20.InvalidTakerFeeTokenError(badToken)); + }); + + it('respects `maxOrderFillAmounts`', async () => { + const orders = _.times(2, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders.slice(1)); + const protocolFee = singleProtocolFee.times(2); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + // Skip the first order. + maxOrderFillAmounts: [ZERO_AMOUNT], + }), + ) + .awaitTransactionSuccessAsync({ value: protocolFee }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + }); + + describe('buy quotes', () => { + it('can fully buy from a single order quote', async () => { + const orders = _.times(1, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedBuyQuoteFillResults(orders); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + buyAmount: qfr.makerAssetBought, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + + it('can fully buy from a multi order quote', async () => { + const orders = _.times(3, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedBuyQuoteFillResults(orders); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + buyAmount: qfr.makerAssetBought, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + + it('can partially buy from a single order quote', async () => { + const orders = _.times(1, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedBuyQuoteFillResults( + orders, + getExpectedBuyQuoteFillResults(orders).makerAssetBought.dividedToIntegerBy(2), + ); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + buyAmount: qfr.makerAssetBought, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + + it('can partially buy from multi order quote and refund unused protocol fees', async () => { + const orders = _.times(3, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedBuyQuoteFillResults(orders.slice(0, 2)); + const maxProtocolFees = singleProtocolFee.times(orders.length); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + buyAmount: qfr.makerAssetBought, + }), + ) + .awaitTransactionSuccessAsync({ value: maxProtocolFees }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + protocolFeeBalance: singleProtocolFee, + }); + }); + + it('can buy from multi order quote with a failing order', async () => { + const orders = _.times(3, () => createOrder()); + // First order will fail. + const validOrders = orders.slice(1); + const signatures = [NULL_BYTES, ...validOrders.map(() => encodeExchangeBehavior())]; + const qfr = getExpectedBuyQuoteFillResults(validOrders); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + buyAmount: qfr.makerAssetBought, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + + it('succeeds if an order transfers too many maker tokens', async () => { + const orders = _.times(2, () => createOrder()); + // First order will mint its tokens + the maker tokens of the second. + const mintScale = orders[1].makerAssetAmount.div(orders[0].makerAssetAmount.minus(1)).plus(1); + const signatures = [ + encodeExchangeBehavior(0, mintScale), + ...orders.slice(1).map(() => encodeExchangeBehavior()), + ]; + const qfr = getExpectedBuyQuoteFillResults(orders); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + buyAmount: qfr.makerAssetBought, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: orders[0].makerAssetAmount.times(mintScale).integerValue(BigNumber.ROUND_DOWN), + takerAssetBalance: orders[1].takerAssetAmount.plus(orders[1].takerFee), + protocolFeeBalance: singleProtocolFee, + }); + }); + + it('fails to buy more than available in orders', async () => { + const orders = _.times(3, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedBuyQuoteFillResults(orders); + const tx = host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + buyAmount: qfr.makerAssetBought.plus(1), + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.TransformERC20.IncompleteFillBuyQuoteError( + makerToken.address, + qfr.makerAssetBought, + qfr.makerAssetBought.plus(1), + ), + ); + }); + + it('can fully buy from a single order with maker asset taker fees', async () => { + const orders = _.times(1, () => + createOrder({ + takerFeeAssetData: assetDataUtils.encodeERC20AssetData(makerToken.address), + }), + ); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedBuyQuoteFillResults(orders); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + buyAmount: qfr.makerAssetBought, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + + it('fails if an order has a non-standard taker fee asset', async () => { + const BAD_ASSET_DATA = hexUtils.random(36); + const orders = _.times(1, () => createOrder({ takerFeeAssetData: BAD_ASSET_DATA })); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders); + const tx = host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + buyAmount: qfr.makerAssetBought, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.TransformERC20.InvalidERC20AssetDataError(BAD_ASSET_DATA), + ); + }); + + it('fails if an order has a fee asset that is neither maker or taker asset', async () => { + const badToken = randomAddress(); + const BAD_ASSET_DATA = hexUtils.concat(ERC20_ASSET_PROXY_ID, hexUtils.leftPad(badToken)); + const orders = _.times(1, () => createOrder({ takerFeeAssetData: BAD_ASSET_DATA })); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders); + const tx = host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + buyAmount: qfr.makerAssetBought, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + return expect(tx).to.revertWith(new ZeroExRevertErrors.TransformERC20.InvalidTakerFeeTokenError(badToken)); + }); + + it('respects `maxOrderFillAmounts`', async () => { + const orders = _.times(2, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders.slice(1)); + const protocolFee = singleProtocolFee.times(2); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + buyAmount: qfr.makerAssetBought, + // Skip the first order. + maxOrderFillAmounts: [ZERO_AMOUNT], + }), + ) + .awaitTransactionSuccessAsync({ value: protocolFee }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + }); +}); diff --git a/contracts/zero-ex/test/transformers/pay_taker_transformer.ts b/contracts/zero-ex/test/transformers/pay_taker_transformer.ts new file mode 100644 index 0000000000..215544bc87 --- /dev/null +++ b/contracts/zero-ex/test/transformers/pay_taker_transformer.ts @@ -0,0 +1,147 @@ +import { blockchainTests, constants, expect, getRandomInteger, randomAddress } from '@0x/contracts-test-utils'; +import { BigNumber, hexUtils } from '@0x/utils'; +import * as _ from 'lodash'; + +import { ETH_TOKEN_ADDRESS } from '../../src/constants'; +import { encodePayTakerTransformerData } from '../../src/transformer_data_encoders'; +import { artifacts } from '../artifacts'; +import { PayTakerTransformerContract, TestMintableERC20TokenContract, TestTransformerHostContract } from '../wrappers'; + +const { MAX_UINT256, ZERO_AMOUNT } = constants; + +blockchainTests.resets('PayTakerTransformer', env => { + let caller: string; + const taker = randomAddress(); + let token: TestMintableERC20TokenContract; + let transformer: PayTakerTransformerContract; + let host: TestTransformerHostContract; + + before(async () => { + [caller] = await env.getAccountAddressesAsync(); + token = await TestMintableERC20TokenContract.deployFrom0xArtifactAsync( + artifacts.TestMintableERC20Token, + env.provider, + env.txDefaults, + artifacts, + ); + transformer = await PayTakerTransformerContract.deployFrom0xArtifactAsync( + artifacts.PayTakerTransformer, + env.provider, + env.txDefaults, + artifacts, + ); + host = await TestTransformerHostContract.deployFrom0xArtifactAsync( + artifacts.TestTransformerHost, + env.provider, + { ...env.txDefaults, from: caller }, + artifacts, + ); + }); + + interface Balances { + ethBalance: BigNumber; + tokenBalance: BigNumber; + } + + const ZERO_BALANCES = { + ethBalance: ZERO_AMOUNT, + tokenBalance: ZERO_AMOUNT, + }; + + async function getBalancesAsync(owner: string): Promise { + return { + ethBalance: await env.web3Wrapper.getBalanceInWeiAsync(owner), + tokenBalance: await token.balanceOf(owner).callAsync(), + }; + } + + async function mintHostTokensAsync(amount: BigNumber): Promise { + await token.mint(host.address, amount).awaitTransactionSuccessAsync(); + } + + async function sendEtherAsync(to: string, amount: BigNumber): Promise { + await env.web3Wrapper.awaitTransactionSuccessAsync( + await env.web3Wrapper.sendTransactionAsync({ + ...env.txDefaults, + to, + from: caller, + value: amount, + }), + ); + } + + it('can transfer a token and ETH', async () => { + const amounts = _.times(2, () => getRandomInteger(1, '1e18')); + const data = encodePayTakerTransformerData({ + amounts, + tokens: [token.address, ETH_TOKEN_ADDRESS], + }); + await mintHostTokensAsync(amounts[0]); + await sendEtherAsync(host.address, amounts[1]); + await host + .rawExecuteTransform(transformer.address, hexUtils.random(), taker, data) + .awaitTransactionSuccessAsync(); + expect(await getBalancesAsync(host.address)).to.deep.eq(ZERO_BALANCES); + expect(await getBalancesAsync(taker)).to.deep.eq({ + tokenBalance: amounts[0], + ethBalance: amounts[1], + }); + }); + + it('can transfer all of a token and ETH', async () => { + const amounts = _.times(2, () => getRandomInteger(1, '1e18')); + const data = encodePayTakerTransformerData({ + amounts: [MAX_UINT256, MAX_UINT256], + tokens: [token.address, ETH_TOKEN_ADDRESS], + }); + await mintHostTokensAsync(amounts[0]); + await sendEtherAsync(host.address, amounts[1]); + await host + .rawExecuteTransform(transformer.address, hexUtils.random(), taker, data) + .awaitTransactionSuccessAsync(); + expect(await getBalancesAsync(host.address)).to.deep.eq(ZERO_BALANCES); + expect(await getBalancesAsync(taker)).to.deep.eq({ + tokenBalance: amounts[0], + ethBalance: amounts[1], + }); + }); + + it('can transfer all of a token and ETH (empty amounts)', async () => { + const amounts = _.times(2, () => getRandomInteger(1, '1e18')); + const data = encodePayTakerTransformerData({ + amounts: [], + tokens: [token.address, ETH_TOKEN_ADDRESS], + }); + await mintHostTokensAsync(amounts[0]); + await sendEtherAsync(host.address, amounts[1]); + await host + .rawExecuteTransform(transformer.address, hexUtils.random(), taker, data) + .awaitTransactionSuccessAsync(); + expect(await getBalancesAsync(host.address)).to.deep.eq(ZERO_BALANCES); + expect(await getBalancesAsync(taker)).to.deep.eq({ + tokenBalance: amounts[0], + ethBalance: amounts[1], + }); + }); + + it('can transfer less than the balance of a token and ETH', async () => { + const amounts = _.times(2, () => getRandomInteger(1, '1e18')); + const data = encodePayTakerTransformerData({ + amounts: amounts.map(a => a.dividedToIntegerBy(2)), + tokens: [token.address, ETH_TOKEN_ADDRESS], + }); + await mintHostTokensAsync(amounts[0]); + await sendEtherAsync(host.address, amounts[1]); + await host + .rawExecuteTransform(transformer.address, hexUtils.random(), taker, data) + .awaitTransactionSuccessAsync(); + expect(await getBalancesAsync(host.address)).to.deep.eq({ + tokenBalance: amounts[0].minus(amounts[0].dividedToIntegerBy(2)), + ethBalance: amounts[1].minus(amounts[1].dividedToIntegerBy(2)), + }); + expect(await getBalancesAsync(taker)).to.deep.eq({ + tokenBalance: amounts[0].dividedToIntegerBy(2), + ethBalance: amounts[1].dividedToIntegerBy(2), + }); + }); +}); diff --git a/contracts/zero-ex/test/transformers/weth_transformer_test.ts b/contracts/zero-ex/test/transformers/weth_transformer_test.ts new file mode 100644 index 0000000000..c4da09447d --- /dev/null +++ b/contracts/zero-ex/test/transformers/weth_transformer_test.ts @@ -0,0 +1,147 @@ +import { blockchainTests, constants, expect, getRandomInteger, randomAddress } from '@0x/contracts-test-utils'; +import { BigNumber, ZeroExRevertErrors } from '@0x/utils'; +import * as _ from 'lodash'; + +import { ETH_TOKEN_ADDRESS } from '../../src/constants'; +import { encodeWethTransformerData } from '../../src/transformer_data_encoders'; +import { artifacts } from '../artifacts'; +import { TestWethContract, TestWethTransformerHostContract, WethTransformerContract } from '../wrappers'; + +const { MAX_UINT256, ZERO_AMOUNT } = constants; + +blockchainTests.resets('WethTransformer', env => { + let weth: TestWethContract; + let transformer: WethTransformerContract; + let host: TestWethTransformerHostContract; + + before(async () => { + weth = await TestWethContract.deployFrom0xArtifactAsync( + artifacts.TestWeth, + env.provider, + env.txDefaults, + artifacts, + ); + transformer = await WethTransformerContract.deployFrom0xArtifactAsync( + artifacts.WethTransformer, + env.provider, + env.txDefaults, + artifacts, + weth.address, + ); + host = await TestWethTransformerHostContract.deployFrom0xArtifactAsync( + artifacts.TestWethTransformerHost, + env.provider, + env.txDefaults, + artifacts, + weth.address, + ); + }); + + interface Balances { + ethBalance: BigNumber; + wethBalance: BigNumber; + } + + async function getHostBalancesAsync(): Promise { + return { + ethBalance: await env.web3Wrapper.getBalanceInWeiAsync(host.address), + wethBalance: await weth.balanceOf(host.address).callAsync(), + }; + } + + it('fails if the token is neither ETH or WETH', async () => { + const amount = getRandomInteger(1, '1e18'); + const data = encodeWethTransformerData({ + amount, + token: randomAddress(), + }); + const tx = host + .executeTransform(amount, transformer.address, data) + .awaitTransactionSuccessAsync({ value: amount }); + return expect(tx).to.revertWith(new ZeroExRevertErrors.TransformERC20.InvalidTransformDataError(data)); + }); + + it('can unwrap WETH', async () => { + const amount = getRandomInteger(1, '1e18'); + const data = encodeWethTransformerData({ + amount, + token: weth.address, + }); + await host.executeTransform(amount, transformer.address, data).awaitTransactionSuccessAsync({ value: amount }); + expect(await getHostBalancesAsync()).to.deep.eq({ + ethBalance: amount, + wethBalance: ZERO_AMOUNT, + }); + }); + + it('can unwrap all WETH', async () => { + const amount = getRandomInteger(1, '1e18'); + const data = encodeWethTransformerData({ + amount: MAX_UINT256, + token: weth.address, + }); + await host.executeTransform(amount, transformer.address, data).awaitTransactionSuccessAsync({ value: amount }); + expect(await getHostBalancesAsync()).to.deep.eq({ + ethBalance: amount, + wethBalance: ZERO_AMOUNT, + }); + }); + + it('can unwrap some WETH', async () => { + const amount = getRandomInteger(1, '1e18'); + const data = encodeWethTransformerData({ + amount: amount.dividedToIntegerBy(2), + token: weth.address, + }); + await host.executeTransform(amount, transformer.address, data).awaitTransactionSuccessAsync({ value: amount }); + expect(await getHostBalancesAsync()).to.deep.eq({ + ethBalance: amount.dividedToIntegerBy(2), + wethBalance: amount.minus(amount.dividedToIntegerBy(2)), + }); + }); + + it('can wrap ETH', async () => { + const amount = getRandomInteger(1, '1e18'); + const data = encodeWethTransformerData({ + amount, + token: ETH_TOKEN_ADDRESS, + }); + await host + .executeTransform(ZERO_AMOUNT, transformer.address, data) + .awaitTransactionSuccessAsync({ value: amount }); + expect(await getHostBalancesAsync()).to.deep.eq({ + ethBalance: ZERO_AMOUNT, + wethBalance: amount, + }); + }); + + it('can wrap all ETH', async () => { + const amount = getRandomInteger(1, '1e18'); + const data = encodeWethTransformerData({ + amount: MAX_UINT256, + token: ETH_TOKEN_ADDRESS, + }); + await host + .executeTransform(ZERO_AMOUNT, transformer.address, data) + .awaitTransactionSuccessAsync({ value: amount }); + expect(await getHostBalancesAsync()).to.deep.eq({ + ethBalance: ZERO_AMOUNT, + wethBalance: amount, + }); + }); + + it('can wrap some ETH', async () => { + const amount = getRandomInteger(1, '1e18'); + const data = encodeWethTransformerData({ + amount: amount.dividedToIntegerBy(2), + token: ETH_TOKEN_ADDRESS, + }); + await host + .executeTransform(ZERO_AMOUNT, transformer.address, data) + .awaitTransactionSuccessAsync({ value: amount }); + expect(await getHostBalancesAsync()).to.deep.eq({ + ethBalance: amount.minus(amount.dividedToIntegerBy(2)), + wethBalance: amount.dividedToIntegerBy(2), + }); + }); +}); diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index 303c54a517..b674f9519c 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -5,12 +5,14 @@ */ export * from '../test/generated-wrappers/allowance_target'; export * from '../test/generated-wrappers/bootstrap'; +export * from '../test/generated-wrappers/fill_quote_transformer'; export * from '../test/generated-wrappers/fixin_common'; export * from '../test/generated-wrappers/flash_wallet'; export * from '../test/generated-wrappers/full_migration'; export * from '../test/generated-wrappers/i_allowance_target'; export * from '../test/generated-wrappers/i_bootstrap'; export * from '../test/generated-wrappers/i_erc20_transformer'; +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_ownable'; @@ -49,8 +51,12 @@ export * from '../test/generated-wrappers/test_simple_function_registry_feature_ export * from '../test/generated-wrappers/test_token_spender'; export * from '../test/generated-wrappers/test_token_spender_erc20_token'; export * from '../test/generated-wrappers/test_transform_erc20'; +export * from '../test/generated-wrappers/test_transformer_host'; +export * from '../test/generated-wrappers/test_weth'; +export * from '../test/generated-wrappers/test_weth_transformer_host'; export * from '../test/generated-wrappers/test_zero_ex_feature'; export * from '../test/generated-wrappers/token_spender'; export * from '../test/generated-wrappers/token_spender_puppet'; export * from '../test/generated-wrappers/transform_erc20'; +export * from '../test/generated-wrappers/weth_transformer'; export * from '../test/generated-wrappers/zero_ex'; diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index a06888b4e8..07a67468db 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "outDir": "lib", "rootDir": ".", "resolveJsonModule": true }, "include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"], "files": [ + "generated-artifacts/FillQuoteTransformer.json", "generated-artifacts/FullMigration.json", "generated-artifacts/IAllowanceTarget.json", "generated-artifacts/IERC20Transformer.json", @@ -15,12 +16,14 @@ "generated-artifacts/ZeroEx.json", "test/generated-artifacts/AllowanceTarget.json", "test/generated-artifacts/Bootstrap.json", + "test/generated-artifacts/FillQuoteTransformer.json", "test/generated-artifacts/FixinCommon.json", "test/generated-artifacts/FlashWallet.json", "test/generated-artifacts/FullMigration.json", "test/generated-artifacts/IAllowanceTarget.json", "test/generated-artifacts/IBootstrap.json", "test/generated-artifacts/IERC20Transformer.json", + "test/generated-artifacts/IExchange.json", "test/generated-artifacts/IFeature.json", "test/generated-artifacts/IFlashWallet.json", "test/generated-artifacts/IOwnable.json", @@ -59,10 +62,14 @@ "test/generated-artifacts/TestTokenSpender.json", "test/generated-artifacts/TestTokenSpenderERC20Token.json", "test/generated-artifacts/TestTransformERC20.json", + "test/generated-artifacts/TestTransformerHost.json", + "test/generated-artifacts/TestWeth.json", + "test/generated-artifacts/TestWethTransformerHost.json", "test/generated-artifacts/TestZeroExFeature.json", "test/generated-artifacts/TokenSpender.json", "test/generated-artifacts/TokenSpenderPuppet.json", "test/generated-artifacts/TransformERC20.json", + "test/generated-artifacts/WethTransformer.json", "test/generated-artifacts/ZeroEx.json" ], "exclude": ["./deploy/solc/solc_bin"]