diff --git a/contracts/zero-ex/contracts/src/errors/LibTransformERC20RichErrors.sol b/contracts/zero-ex/contracts/src/errors/LibTransformERC20RichErrors.sol index 2c69bcb980..73a692c843 100644 --- a/contracts/zero-ex/contracts/src/errors/LibTransformERC20RichErrors.sol +++ b/contracts/zero-ex/contracts/src/errors/LibTransformERC20RichErrors.sol @@ -187,6 +187,32 @@ library LibTransformERC20RichErrors { ); } + function InvalidTokensReceivedError( + address[] memory tokens + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("InvalidTokensReceivedError(address[])")), + tokens + ); + } + + function InvalidTakerFeeTokenError( + address token + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("InvalidTakerFeeTokenError(address)")), + token + ); + } + // WethTransformer rors //////////////////////////////////////////////////// function WrongNumberOfTokensReceivedError( 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..110057ecd2 --- /dev/null +++ b/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol @@ -0,0 +1,442 @@ +/* + + 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 "@0x/contracts-utils/contracts/src/v06/ReentrancyGuardV06.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, + ReentrancyGuardV06 +{ + // solhint-disable indent,no-empty-blocks,no-unused-vars + + /// @dev Data to encode and pass to `transform()`. + struct FillQuoteTransformData { + // 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. + // 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 ERC20Proxy ID. + bytes4 constant private ERC20_ASSET_PROXY_ID = 0xf47261b0; + /// @dev Received tokens index of the sell token. + uint256 constant private SELL_TOKEN_IDX = 0; + /// @dev Received tokens index of the ETH "token" (protocol fees). + uint256 constant private ETH_TOKEN_IDX = 1; + + /// @dev The Exchange contract. + IExchange public immutable exchange; + /// @dev The ERC20Proxy address. + address public immutable erc20Proxy; + + using LibERC20TokenV06 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 `FillQuoteTransformData`. + /// @return success `TRANSFORMER_SUCCESS` on success. + function transform( + bytes32, // callDataHash, + address payable, // taker, + IERC20TokenV06[] calldata tokens, + uint256[] calldata amounts, + bytes calldata data_ + ) + external + override + payable + nonReentrant + returns (bytes4 success) + { + FillQuoteTransformData memory data = + abi.decode(data_, (FillQuoteTransformData)); + + // We expect to receive two tokens: The sell token and ETH for the protocol fee. + if (tokens.length != 2 || + tokens[SELL_TOKEN_IDX] != data.sellToken || + !LibERC20Transformer.isTokenETH(tokens[ETH_TOKEN_IDX])) + { + LibTransformERC20RichErrors + .InvalidTokensReceivedError(_asAddressArray(tokens)) + .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 known in advance, like when unwrapping + // Chai/cUSDC/cDAI. + if (data.sellAmount == uint256(-1) && data.buyAmount == 0) { + data.sellAmount = amounts[SELL_TOKEN_IDX]; + } + + // Approve the ERC20 proxy to spend `sellToken`. + data.sellToken.approveIfBelow(erc20Proxy, data.sellAmount); + + // Fill the orders. + uint256 singleProtocolFee = exchange.protocolFeeMultiplier().safeMul(tx.gasprice); + uint256 boughtAmount = 0; + uint256 soldAmount = 0; + uint256 protocolFeesPaid = 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. + uint256 remainingETH = amounts[ETH_TOKEN_IDX].safeSub(protocolFeesPaid); + if (remainingETH < singleProtocolFee) { + LibTransformERC20RichErrors + .InsufficientProtocolFeeError(remainingETH, 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); + protocolFeesPaid = protocolFeesPaid.safeAdd(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(); + } + } + + // Transfer buy tokens. + data.buyToken.compatTransfer(msg.sender, boughtAmount); + { + // Return unused sell tokens. + uint256 remainingSellToken = amounts[SELL_TOKEN_IDX].safeSub(soldAmount); + if (remainingSellToken != 0) { + data.sellToken.compatTransfer(msg.sender, remainingSellToken); + } + } + { + // Return unused ETH. + uint256 remainingETH = amounts[ETH_TOKEN_IDX].safeSub(protocolFeesPaid); + if (remainingETH != 0) { + msg.sender.transfer(remainingETH); + } + } + return LibERC20Transformer.TRANSFORMER_SUCCESS; + } + + // solhint-disable + /// @dev Allow this contract to receive protocol fee refunds. + receive() external payable {} + // solhint-enable + + // Try to sell up to `sellAmount` from an 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. + 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 Fill an order. + 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. + 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)); + } + + /// @dev Cast an array of tokens to an array of addresses. + function _asAddressArray(IERC20TokenV06[] memory tokens) + private + pure + returns (address[] memory addrs) + { + assembly { addrs := tokens } + } +} 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..aa57469816 --- /dev/null +++ b/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol @@ -0,0 +1,64 @@ +/* + + 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 any tokens it receives to the taker. +contract PayTakerTransformer is + IERC20Transformer +{ + using LibRichErrorsV06 for bytes; + using LibSafeMathV06 for uint256; + using LibERC20Transformer for IERC20TokenV06; + + /// @dev Forwards any tokens transffered to the taker. + /// @param taker The taker address (caller of `TransformERC20.transformERC20()`). + /// @param tokens The tokens that were transferred to this contract. ETH may + /// be included as 0xeee... + /// @param amounts The amount of each token in `tokens` that were transferred + /// to this contract. + /// @return success `TRANSFORMER_SUCCESS` on success. + function transform( + bytes32, // callDataHash, + address payable taker, + IERC20TokenV06[] calldata tokens, + uint256[] calldata amounts, + bytes calldata // data_ + ) + external + override + payable + returns (bytes4 success) + { + for (uint256 i = 0; i < amounts.length; ++i) { + // Transfer tokens directly to the taker. + tokens[i].transformerTransfer(taker, amounts[i]); + } + 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..922aa793e1 --- /dev/null +++ b/contracts/zero-ex/contracts/src/transformers/WethTransformer.sol @@ -0,0 +1,92 @@ +/* + + 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 +{ + // solhint-disable indent + + /// @dev The WETH contract address. + IEtherTokenV06 public immutable weth; + + using LibRichErrorsV06 for bytes; + using LibSafeMathV06 for uint256; + + constructor(IEtherTokenV06 weth_) public { + weth = weth_; + } + + /// @dev Wraps and unwraps WETH, depending on the token transferred. + /// If WETH is transferred, it will be unwrapped to ETH. + /// If ETH is transferred, it will be wrapped to WETH. + /// @param tokens The tokens that were transferred to this contract. ETH may + /// be included as 0xeee... + /// @param amounts The amount of each token in `tokens` that were transferred + /// to this contract. + /// @return success `TRANSFORMER_SUCCESS` on success. + function transform( + bytes32, // callDataHash, + address payable, // taker, + IERC20TokenV06[] calldata tokens, + uint256[] calldata amounts, + bytes calldata // data + ) + external + override + payable + returns (bytes4 success) + { + if (tokens.length != 1) { + LibTransformERC20RichErrors + .WrongNumberOfTokensReceivedError(tokens.length, 1) + .rrevert(); + } + + uint256 amount = amounts[0]; + + if (address(tokens[0]) == LibERC20Transformer.ETH_TOKEN_ADDRESS) { + // Wrap ETH. + weth.deposit{value: amount}(); + // Transfer WETH to sender. + weth.transfer(msg.sender, amount); + } else if (address(tokens[0]) == address(weth)) { + // Unwrap WETH. + weth.withdraw(amount); + // Transfer ETH to sender. + msg.sender.transfer(amount); + } else { + // Token must be either WETH or ETH. + LibTransformERC20RichErrors + .InvalidTokenReceivedError(address(tokens[0])) + .rrevert(); + } + 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..6bc848d7ff --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestFillQuoteTransformerExchange.sol @@ -0,0 +1,146 @@ +/* + + 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; + address private constant ASSET_PROXY_ADDRESS = address(uint160(uint256(keccak256("FAKE_ASSET_PROXY")))); + + 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, ASSET_PROXY_ADDRESS) >= 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, ASSET_PROXY_ADDRESS) >= 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 + pure + returns (address) + { + return ASSET_PROXY_ADDRESS; + } + + function _getTokenFromAssetData(bytes memory assetData) + private + pure + returns (TestMintableERC20Token token) + { + return TestMintableERC20Token(LibBytesV06.readAddress(assetData, 16)); + } +} diff --git a/contracts/zero-ex/contracts/test/TestFillQuoteTransformerHelper.sol b/contracts/zero-ex/contracts/test/TestFillQuoteTransformerHelper.sol new file mode 100644 index 0000000000..ad15f39d6f --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestFillQuoteTransformerHelper.sol @@ -0,0 +1,36 @@ +/* + + 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/FillQuoteTransformer.sol"; + + +abstract contract TestFillQuoteTransformerHelper { + + function encodeTransformData( + FillQuoteTransformer.FillQuoteTransformData calldata data + ) + external + pure + returns (bytes memory encoded) + { + return abi.encode(data); + } +} diff --git a/contracts/zero-ex/contracts/test/TestFillQuoteTransformerTaker.sol b/contracts/zero-ex/contracts/test/TestFillQuoteTransformerTaker.sol new file mode 100644 index 0000000000..195f34ae96 --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestFillQuoteTransformerTaker.sol @@ -0,0 +1,67 @@ +/* + + Copyright 2020 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6.5; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; +import "../src/transformers/FillQuoteTransformer.sol"; +import "../src/transformers/LibERC20Transformer.sol"; + + +contract TestFillQuoteTransformerTaker { + + FillQuoteTransformer private _transformer; + + constructor(FillQuoteTransformer transformer) public { + _transformer = transformer; + } + + function transform( + IERC20TokenV06[] calldata tokens, + uint256[] calldata amounts, + bytes calldata data + ) + external + payable + { + for (uint256 i = 0; i < tokens.length; ++i) { + if (amounts.length > i) { + if (!LibERC20Transformer.isTokenETH(tokens[i])) { + tokens[i].transfer(address(_transformer), amounts[i]); + } + } + } + + bytes4 success = _transformer.transform{value: msg.value}( + bytes32(0), + address(0), + tokens, + amounts, + data + ); + require( + success == LibERC20Transformer.TRANSFORMER_SUCCESS, + "TestFillQuoteTransformerTaker/UNSUCCESSFUL_RESULT" + ); + } + + // solhint-disable + receive() external payable {} + // solhint-enable +} diff --git a/contracts/zero-ex/contracts/test/TestMintableERC20Token.sol b/contracts/zero-ex/contracts/test/TestMintableERC20Token.sol index 34f666789e..060d454902 100644 --- a/contracts/zero-ex/contracts/test/TestMintableERC20Token.sol +++ b/contracts/zero-ex/contracts/test/TestMintableERC20Token.sol @@ -59,4 +59,13 @@ contract TestMintableERC20Token { 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/package.json b/contracts/zero-ex/package.json index 4e1a0db4d5..78007ea335 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -40,7 +40,7 @@ "config": { "publicInterfaceContracts": "ZeroEx,FullMigration,InitialMigration,Puppet,IERC20Transformer,IOwnable,ISimpleFunctionRegistry,ITokenSpender,IPuppetPool,ITransformERC20", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(Bootstrap|FixinCommon|FullMigration|IBootstrap|IERC20Transformer|IFeature|IOwnable|IPuppet|IPuppetPool|ISimpleFunctionRegistry|ITestSimpleFunctionRegistryFeature|ITokenSpender|ITokenSpenderPuppet|ITransformERC20|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibPuppetPoolStorage|LibPuppetRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|Ownable|Puppet|PuppetPool|SimpleFunctionRegistry|TestInitialMigration|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestPuppetPool|TestPuppetTarget|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestZeroExFeature|TokenSpender|TokenSpenderPuppet|TransformERC20|ZeroEx).json" + "abis": "./test/generated-artifacts/@(Bootstrap|FillQuoteTransformer|FixinCommon|FullMigration|IBootstrap|IERC20Transformer|IExchange|IFeature|IOwnable|IPuppet|IPuppetPool|ISimpleFunctionRegistry|ITestSimpleFunctionRegistryFeature|ITokenSpender|ITokenSpenderPuppet|ITransformERC20|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibPuppetPoolStorage|LibPuppetRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|Ownable|PayTakerTransformer|Puppet|PuppetPool|SimpleFunctionRegistry|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHelper|TestFillQuoteTransformerTaker|TestInitialMigration|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestPuppetPool|TestPuppetTarget|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestZeroExFeature|TokenSpender|TokenSpenderPuppet|TransformERC20|WethTransformer|ZeroEx).json" }, "repository": { "type": "git", diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index 89e59c343f..18effa034a 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -6,10 +6,12 @@ import { ContractArtifact } from 'ethereum-types'; 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 FullMigration from '../test/generated-artifacts/FullMigration.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 InitialMigration from '../test/generated-artifacts/InitialMigration.json'; import * as IOwnable from '../test/generated-artifacts/IOwnable.json'; @@ -37,9 +39,13 @@ 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 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 PuppetPool from '../test/generated-artifacts/PuppetPool.json'; import * as SimpleFunctionRegistry from '../test/generated-artifacts/SimpleFunctionRegistry.json'; +import * as TestFillQuoteTransformerExchange from '../test/generated-artifacts/TestFillQuoteTransformerExchange.json'; +import * as TestFillQuoteTransformerHelper from '../test/generated-artifacts/TestFillQuoteTransformerHelper.json'; +import * as TestFillQuoteTransformerTaker from '../test/generated-artifacts/TestFillQuoteTransformerTaker.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'; @@ -55,6 +61,7 @@ import * as TestZeroExFeature from '../test/generated-artifacts/TestZeroExFeatur import * as TokenSpender from '../test/generated-artifacts/TokenSpender.json'; import * as TokenSpenderPuppet from '../test/generated-artifacts/TokenSpenderPuppet.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, @@ -93,9 +100,16 @@ export const artifacts = { LibSimpleFunctionRegistryStorage: LibSimpleFunctionRegistryStorage as ContractArtifact, LibStorage: LibStorage as ContractArtifact, LibTokenSpenderStorage: LibTokenSpenderStorage 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, + TestFillQuoteTransformerExchange: TestFillQuoteTransformerExchange as ContractArtifact, + TestFillQuoteTransformerHelper: TestFillQuoteTransformerHelper as ContractArtifact, + TestFillQuoteTransformerTaker: TestFillQuoteTransformerTaker as ContractArtifact, TestInitialMigration: TestInitialMigration as ContractArtifact, TestMigrator: TestMigrator as ContractArtifact, TestMintTokenERC20Transformer: TestMintTokenERC20Transformer as ContractArtifact, diff --git a/contracts/zero-ex/test/transformers/fill_quote_test.ts b/contracts/zero-ex/test/transformers/fill_quote_test.ts new file mode 100644 index 0000000000..c6f70427f9 --- /dev/null +++ b/contracts/zero-ex/test/transformers/fill_quote_test.ts @@ -0,0 +1,566 @@ +import { + assertIntegerRoughlyEquals, + blockchainTests, + constants, + expect, + getRandomInteger, + getRandomPortion, + Numberish, + verifyEventsFromLogs, +} from '@0x/contracts-test-utils'; +import { assetDataUtils } from '@0x/order-utils'; +import { BigNumber, hexUtils, ZeroExRevertErrors } from '@0x/utils'; +import { ERC20AssetData, Order } from '@0x/types'; +import * as _ from 'lodash'; + +import { artifacts } from '../artifacts'; +import { + FillQuoteTransformerContract, + TestMintableERC20TokenContract, + TestFillQuoteTransformerExchangeContract, + TestFillQuoteTransformerHelperContract, + TestFillQuoteTransformerTakerContract, +} from '../wrappers'; + +const { NULL_ADDRESS, NULL_BYTES, MAX_UINT256, ZERO_AMOUNT } = constants; + +blockchainTests.resets.only('TransformERC20 feature', env => { + let maker: string; + let feeRecipient: string; + let exchange: TestFillQuoteTransformerExchangeContract; + let helper: TestFillQuoteTransformerHelperContract; + let transformer: FillQuoteTransformerContract; + let taker: TestFillQuoteTransformerTakerContract; + let makerToken: TestMintableERC20TokenContract; + let takerToken: TestMintableERC20TokenContract; + let takerFeeToken: TestMintableERC20TokenContract; + let singleProtocolFee: BigNumber; + + 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, + ); + taker = await TestFillQuoteTransformerTakerContract.deployFrom0xArtifactAsync( + artifacts.TestFillQuoteTransformerTaker, + env.provider, + { + ...env.txDefaults, + gasPrice: 1, + }, + artifacts, + transformer.address, + ); + helper = new TestFillQuoteTransformerHelperContract( + // No need to actually deploy this contract. + NULL_ADDRESS, + env.provider, + env.txDefaults, + ); + [makerToken, takerToken, takerFeeToken] = await Promise.all(_.times(3, + async () => TestMintableERC20TokenContract.deployFrom0xArtifactAsync( + artifacts.TestMintableERC20Token, + env.provider, + env.txDefaults, + artifacts, + ), + )); + singleProtocolFee = await exchange.protocolFeeMultiplier().callAsync(); + }); + + 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 = fillRatio + .times(order.takerAssetAmount) + .integerValue(BigNumber.ROUND_DOWN), + qfr.protocolFeePaid = qfr.protocolFeePaid.plus(singleProtocolFee); + qfr.makerAssetBought = qfr.makerAssetBought.plus(singleFillAmount); + 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; + } + + async function mintTakerTokens(owner: string, amount: BigNumber): Promise { + await takerToken.mint(owner, amount).awaitTransactionSuccessAsync(); + } + + 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, 1, 'makerAssetBalance'); + assertIntegerRoughlyEquals(actual.takerAssetBalance, expected.takerAssetBalance, 1, 'takerAssetBalance'); + assertIntegerRoughlyEquals(actual.takerFeeBalance, expected.takerFeeBalance, 1, 'takerFeeBalance'); + assertIntegerRoughlyEquals(actual.protocolFeeBalance, expected.protocolFeeBalance, 1, 'protocolFeeBalance'); + } + + interface TransformData { + sellToken: string; + buyToken: string; + orders: Order[]; + signatures: string[]; + maxOrderFillAmounts: BigNumber[]; + sellAmount: BigNumber; + buyAmount: BigNumber; + } + + function encodeTransformData(fields: Partial = {}): string { + const data = { + sellToken: takerToken.address, + buyToken: makerToken.address, + orders: [], + signatures: [], + maxOrderFillAmounts: [], + sellAmount: MAX_UINT256, + buyAmount: ZERO_AMOUNT, + ...fields, + }; + return hexUtils.slice(helper.encodeTransformData(data).getABIEncodedTransactionData(), 4); + } + + 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 ETH_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; + + 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 mintTakerTokens(taker.address, qfr.takerAssetSpent); + await taker.transform( + [takerToken.address, ETH_TOKEN_ADDRESS], + [qfr.takerAssetSpent, qfr.protocolFeePaid], + encodeTransformData({ + orders, + signatures, + }), + ).awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances( + await getBalancesAsync(taker.address), + { ...ZERO_BALANCES, makerAssetBalance: qfr.makerAssetBought }, + ); + expect(await getBalancesAsync(transformer.address)).to.deep.eq(ZERO_BALANCES); + }); + + it('can fully sell to multi order quote', async () => { + const orders = _.times(3, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders); + await mintTakerTokens(taker.address, qfr.takerAssetSpent); + await taker.transform( + [takerToken.address, ETH_TOKEN_ADDRESS], + [qfr.takerAssetSpent, qfr.protocolFeePaid], + encodeTransformData({ + orders, + signatures, + }), + ).awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances( + await getBalancesAsync(taker.address), + { ...ZERO_BALANCES, makerAssetBalance: qfr.makerAssetBought }, + ); + expect(await getBalancesAsync(transformer.address)).to.deep.eq(ZERO_BALANCES); + }); + + 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 mintTakerTokens(taker.address, qfr.takerAssetSpent); + await taker.transform( + [takerToken.address, ETH_TOKEN_ADDRESS], + [qfr.takerAssetSpent, qfr.protocolFeePaid], + encodeTransformData({ + orders, + signatures, + }), + ).awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances( + await getBalancesAsync(taker.address), + { ...ZERO_BALANCES, makerAssetBalance: qfr.makerAssetBought }, + ); + expect(await getBalancesAsync(transformer.address)).to.deep.eq(ZERO_BALANCES); + }); + + 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 mintTakerTokens(taker.address, qfr.takerAssetSpent); + await taker.transform( + [takerToken.address, ETH_TOKEN_ADDRESS], + [qfr.takerAssetSpent, maxProtocolFees], + encodeTransformData({ + orders, + signatures, + }), + ).awaitTransactionSuccessAsync({ value: maxProtocolFees }); + assertBalances( + await getBalancesAsync(taker.address), + { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + protocolFeeBalance: singleProtocolFee, + }, + ); + expect(await getBalancesAsync(transformer.address)).to.deep.eq(ZERO_BALANCES); + }); + + 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 mintTakerTokens(taker.address, qfr.takerAssetSpent); + await taker.transform( + [takerToken.address, ETH_TOKEN_ADDRESS], + [qfr.takerAssetSpent, qfr.protocolFeePaid], + encodeTransformData({ + orders, + signatures, + }), + ).awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances( + await getBalancesAsync(taker.address), + { ...ZERO_BALANCES, makerAssetBalance: qfr.makerAssetBought }, + ); + expect(await getBalancesAsync(transformer.address)).to.deep.eq(ZERO_BALANCES); + }); + + 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 mintTakerTokens(taker.address, qfr.takerAssetSpent); + await taker.transform( + [takerToken.address, ETH_TOKEN_ADDRESS], + [qfr.takerAssetSpent, qfr.protocolFeePaid], + encodeTransformData({ + orders, + signatures, + }), + ).awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances( + await getBalancesAsync(taker.address), + { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought + .minus(orders[0].makerAssetAmount.times(1 - mintScale)) + .integerValue(BigNumber.ROUND_DOWN), + }, + ); + expect(await getBalancesAsync(transformer.address)).to.deep.eq(ZERO_BALANCES); + }); + + 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); + await mintTakerTokens(taker.address, qfr.takerAssetSpent); + const tx = taker.transform( + [takerToken.address, ETH_TOKEN_ADDRESS], + [qfr.takerAssetSpent, qfr.protocolFeePaid], + 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); + await mintTakerTokens(taker.address, qfr.takerAssetSpent); + const tx = taker.transform( + [takerToken.address, ETH_TOKEN_ADDRESS], + [qfr.takerAssetSpent, qfr.protocolFeePaid.minus(1)], + encodeTransformData({ + orders, + signatures, + }), + ).awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + 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 mintTakerTokens(taker.address, takerTokenBalance); + await taker.transform( + [takerToken.address, ETH_TOKEN_ADDRESS], + [takerTokenBalance, qfr.protocolFeePaid], + encodeTransformData({ + orders, + signatures, + sellAmount: qfr.takerAssetSpent, + }), + ).awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances( + await getBalancesAsync(taker.address), + { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + takerAssetBalance: qfr.takerAssetSpent.times(0.01).integerValue(), + }, + ); + expect(await getBalancesAsync(transformer.address)).to.deep.eq(ZERO_BALANCES); + }); + + 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(); + await mintTakerTokens(taker.address, takerTokenBalance); + const tx = taker.transform( + [takerToken.address, ETH_TOKEN_ADDRESS], + [takerTokenBalance, qfr.protocolFeePaid], + 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, + ), + ); + }); + }); + + describe('buy quotes', () => { + it.only('can fully buy from a single order quote', async () => { + const orders = _.times(1, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedBuyQuoteFillResults(orders); + await mintTakerTokens(taker.address, qfr.takerAssetSpent); + await taker.transform( + [takerToken.address, ETH_TOKEN_ADDRESS], + [qfr.takerAssetSpent, qfr.protocolFeePaid], + encodeTransformData({ + orders, + signatures, + buyAmount: qfr.makerAssetBought, + }), + ).awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances( + await getBalancesAsync(taker.address), + { ...ZERO_BALANCES, makerAssetBalance: qfr.makerAssetBought }, + ); + expect(await getBalancesAsync(transformer.address)).to.deep.eq(ZERO_BALANCES); + }); + + 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).plus(1); + console.log(mintScale); + const signatures = [ + encodeExchangeBehavior(0, mintScale), + ...orders.slice(1).map(() => encodeExchangeBehavior()), + ]; + const qfr = getExpectedBuyQuoteFillResults(orders); + await mintTakerTokens(taker.address, qfr.takerAssetSpent); + await taker.transform( + [takerToken.address, ETH_TOKEN_ADDRESS], + [qfr.takerAssetSpent, qfr.protocolFeePaid], + encodeTransformData({ + orders, + signatures, + buyAmount: qfr.makerAssetBought, + }), + ).awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances( + await getBalancesAsync(taker.address), + { + ...ZERO_BALANCES, + makerAssetBalance: orders[0].makerAssetAmount + .times(mintScale) + .integerValue(BigNumber.ROUND_DOWN), + takerAssetBalance: orders[1].takerAssetAmount + .plus(orders[1].takerFee), + }, + ); + expect(await getBalancesAsync(transformer.address)).to.deep.eq(ZERO_BALANCES); + }); + }); +}); diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index 150f3376a4..ae7ab0473f 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -4,10 +4,12 @@ * ----------------------------------------------------------------------------- */ 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/full_migration'; 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_ownable'; export * from '../test/generated-wrappers/i_puppet'; @@ -35,9 +37,13 @@ export * from '../test/generated-wrappers/lib_storage'; export * from '../test/generated-wrappers/lib_token_spender_storage'; export * from '../test/generated-wrappers/lib_transform_erc20_rich_errors'; export * from '../test/generated-wrappers/ownable'; +export * from '../test/generated-wrappers/pay_taker_transformer'; export * from '../test/generated-wrappers/puppet'; export * from '../test/generated-wrappers/puppet_pool'; export * from '../test/generated-wrappers/simple_function_registry'; +export * from '../test/generated-wrappers/test_fill_quote_transformer_exchange'; +export * from '../test/generated-wrappers/test_fill_quote_transformer_helper'; +export * from '../test/generated-wrappers/test_fill_quote_transformer_taker'; export * from '../test/generated-wrappers/test_initial_migration'; export * from '../test/generated-wrappers/test_migrator'; export * from '../test/generated-wrappers/test_mint_token_erc20_transformer'; @@ -53,4 +59,5 @@ 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 e69ff98339..c4cfb4f1dd 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -14,10 +14,12 @@ "generated-artifacts/Puppet.json", "generated-artifacts/ZeroEx.json", "test/generated-artifacts/Bootstrap.json", + "test/generated-artifacts/FillQuoteTransformer.json", "test/generated-artifacts/FixinCommon.json", "test/generated-artifacts/FullMigration.json", "test/generated-artifacts/IBootstrap.json", "test/generated-artifacts/IERC20Transformer.json", + "test/generated-artifacts/IExchange.json", "test/generated-artifacts/IFeature.json", "test/generated-artifacts/IOwnable.json", "test/generated-artifacts/IPuppet.json", @@ -45,9 +47,13 @@ "test/generated-artifacts/LibTokenSpenderStorage.json", "test/generated-artifacts/LibTransformERC20RichErrors.json", "test/generated-artifacts/Ownable.json", + "test/generated-artifacts/PayTakerTransformer.json", "test/generated-artifacts/Puppet.json", "test/generated-artifacts/PuppetPool.json", "test/generated-artifacts/SimpleFunctionRegistry.json", + "test/generated-artifacts/TestFillQuoteTransformerExchange.json", + "test/generated-artifacts/TestFillQuoteTransformerHelper.json", + "test/generated-artifacts/TestFillQuoteTransformerTaker.json", "test/generated-artifacts/TestInitialMigration.json", "test/generated-artifacts/TestMigrator.json", "test/generated-artifacts/TestMintTokenERC20Transformer.json", @@ -63,6 +69,7 @@ "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"]