diff --git a/contracts/zero-ex/CHANGELOG.json b/contracts/zero-ex/CHANGELOG.json index 5724860100..d567edd0fc 100644 --- a/contracts/zero-ex/CHANGELOG.json +++ b/contracts/zero-ex/CHANGELOG.json @@ -31,6 +31,10 @@ { "note": "Introduce fill `TransformERC20` feature.", "pr": 2545 + }, + { + "note": "Fill Bridges directly in `FillQuoteTransformer`.", + "pr": 2608 } ] } diff --git a/contracts/zero-ex/contracts/src/fixins/FixinGasToken.sol b/contracts/zero-ex/contracts/src/fixins/FixinGasToken.sol new file mode 100644 index 0000000000..96cac324c0 --- /dev/null +++ b/contracts/zero-ex/contracts/src/fixins/FixinGasToken.sol @@ -0,0 +1,46 @@ +/* + + 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; + +import "../vendor/v3/IGasToken.sol"; + +contract FixinGasToken +{ + /// @dev Mainnet address of the GST2 contract + address constant private GST_ADDRESS = 0x0000000000b3F879cb30FE243b4Dfee438691c04; + /// @dev Mainnet address of the GST Collector + address constant private GST_COLLECTOR_ADDRESS = 0x000000D3b08566BE75A6DB803C03C85C0c1c5B96; + + /// @dev Frees gas tokens using the balance of `from`. Amount freed is based + /// on the gas consumed in the function + modifier freesGasTokensFromCollector() { + uint256 gasBefore = gasleft(); + _; + // (gasUsed + FREE_BASE) / (2 * REIMBURSE - FREE_TOKEN) + // 14154 24000 6870 + uint256 value = (gasBefore - gasleft() + 14154) / 41130; + GST_ADDRESS.call( + abi.encodeWithSelector( + IGasToken(address(0)).freeFromUpTo.selector, + GST_COLLECTOR_ADDRESS, + value + ) + ); + } +} diff --git a/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol b/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol index 383b0e2d02..3adbb48036 100644 --- a/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol +++ b/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol @@ -27,18 +27,23 @@ 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 "../vendor/v3/IERC20Bridge.sol"; import "./Transformer.sol"; import "./LibERC20Transformer.sol"; +import "../fixins/FixinGasToken.sol"; /// @dev A transformer that fills an ERC20 market sell/buy quote. +/// This transformer shortcuts bridge orders and fills them directly contract FillQuoteTransformer is - Transformer + Transformer, + FixinGasToken { using LibERC20TokenV06 for IERC20TokenV06; using LibERC20Transformer for IERC20TokenV06; using LibSafeMathV06 for uint256; using LibRichErrorsV06 for bytes; + using LibBytesV06 for bytes; /// @dev Whether we are performing a market sell or buy. enum Side { @@ -81,8 +86,29 @@ contract FillQuoteTransformer is uint256 protocolFeePaid; } + /// @dev Intermediate state variables to get around stack limits. + struct FillState { + uint256 ethRemaining; + uint256 boughtAmount; + uint256 soldAmount; + uint256 protocolFee; + uint256 takerTokenBalanceRemaining; + } + + /// @dev Emitted when a trade is skipped due to a lack of funds + /// to pay the 0x Protocol fee. + /// @param ethBalance The current eth balance. + /// @param ethNeeded The current eth balance required to pay + /// the protocol fee. + event ProtocolFeeUnfunded( + uint256 ethBalance, + uint256 ethNeeded + ); + /// @dev The Exchange ERC20Proxy ID. bytes4 private constant ERC20_ASSET_PROXY_ID = 0xf47261b0; + /// @dev The Exchange ERC20BridgeProxy ID. + bytes4 private constant ERC20_BRIDGE_PROXY_ID = 0xdc1600f3; /// @dev Maximum uint256 value. uint256 private constant MAX_UINT256 = uint256(-1); @@ -113,9 +139,11 @@ contract FillQuoteTransformer is ) external override + freesGasTokensFromCollector returns (bytes4 success) { TransformData memory data = abi.decode(data_, (TransformData)); + FillState memory state; // Validate data fields. if (data.sellToken.isTokenETH() || data.buyToken.isTokenETH()) { @@ -131,43 +159,35 @@ contract FillQuoteTransformer is ).rrevert(); } + state.takerTokenBalanceRemaining = data.sellToken.getTokenBalanceOf(address(this)); if (data.side == Side.Sell && data.fillAmount == MAX_UINT256) { // If `sellAmount == -1 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. - data.fillAmount = data.sellToken.getTokenBalanceOf(address(this)); + data.fillAmount = state.takerTokenBalanceRemaining; } // Approve the ERC20 proxy to spend `sellToken`. data.sellToken.approveIfBelow(erc20Proxy, data.fillAmount); // Fill the orders. - uint256 singleProtocolFee = exchange.protocolFeeMultiplier().safeMul(tx.gasprice); - uint256 ethRemaining = address(this).balance; - uint256 boughtAmount = 0; - uint256 soldAmount = 0; + state.protocolFee = exchange.protocolFeeMultiplier().safeMul(tx.gasprice); + state.ethRemaining = address(this).balance; for (uint256 i = 0; i < data.orders.length; ++i) { // Check if we've hit our targets. if (data.side == Side.Sell) { // Market sell check. - if (soldAmount >= data.fillAmount) { + if (state.soldAmount >= data.fillAmount) { break; } } else { // Market buy check. - if (boughtAmount >= data.fillAmount) { + if (state.boughtAmount >= data.fillAmount) { 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.side == Side.Sell) { @@ -177,12 +197,12 @@ contract FillQuoteTransformer is data.sellToken, data.orders[i], data.signatures[i], - data.fillAmount.safeSub(soldAmount).min256( + data.fillAmount.safeSub(state.soldAmount).min256( data.maxOrderFillAmounts.length > i ? data.maxOrderFillAmounts[i] : MAX_UINT256 ), - singleProtocolFee + state ); } else { // Market buy. @@ -191,39 +211,40 @@ contract FillQuoteTransformer is data.sellToken, data.orders[i], data.signatures[i], - data.fillAmount.safeSub(boughtAmount).min256( + data.fillAmount.safeSub(state.boughtAmount).min256( data.maxOrderFillAmounts.length > i ? data.maxOrderFillAmounts[i] : MAX_UINT256 ), - singleProtocolFee + state ); } // Accumulate totals. - soldAmount = soldAmount.safeAdd(results.takerTokenSoldAmount); - boughtAmount = boughtAmount.safeAdd(results.makerTokenBoughtAmount); - ethRemaining = ethRemaining.safeSub(results.protocolFeePaid); + state.soldAmount = state.soldAmount.safeAdd(results.takerTokenSoldAmount); + state.boughtAmount = state.boughtAmount.safeAdd(results.makerTokenBoughtAmount); + state.ethRemaining = state.ethRemaining.safeSub(results.protocolFeePaid); + state.takerTokenBalanceRemaining = state.takerTokenBalanceRemaining.safeSub(results.takerTokenSoldAmount); } // Ensure we hit our targets. if (data.side == Side.Sell) { // Market sell check. - if (soldAmount < data.fillAmount) { + if (state.soldAmount < data.fillAmount) { LibTransformERC20RichErrors .IncompleteFillSellQuoteError( address(data.sellToken), - soldAmount, + state.soldAmount, data.fillAmount ).rrevert(); } } else { // Market buy check. - if (boughtAmount < data.fillAmount) { + if (state.boughtAmount < data.fillAmount) { LibTransformERC20RichErrors .IncompleteFillBuyQuoteError( address(data.buyToken), - boughtAmount, + state.boughtAmount, data.fillAmount ).rrevert(); } @@ -237,14 +258,14 @@ contract FillQuoteTransformer is /// @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`. + /// @param state Intermediate state variables to get around stack limits. function _sellToOrder( IERC20TokenV06 makerToken, IERC20TokenV06 takerToken, IExchange.Order memory order, bytes memory signature, uint256 sellAmount, - uint256 protocolFee + FillState memory state ) private returns (FillOrderResults memory results) @@ -281,18 +302,12 @@ contract FillQuoteTransformer is } } - // Clamp fill amount to order size. - takerTokenFillAmount = LibSafeMathV06.min256( - takerTokenFillAmount, - order.takerAssetAmount - ); - // Perform the fill. return _fillOrder( order, signature, takerTokenFillAmount, - protocolFee, + state, makerToken, takerFeeToken == takerToken ); @@ -304,14 +319,14 @@ contract FillQuoteTransformer is /// @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`. + /// @param state Intermediate state variables to get around stack limits. function _buyFromOrder( IERC20TokenV06 makerToken, IERC20TokenV06 takerToken, IExchange.Order memory order, bytes memory signature, uint256 buyAmount, - uint256 protocolFee + FillState memory state ) private returns (FillOrderResults memory results) @@ -351,18 +366,12 @@ contract FillQuoteTransformer is } } - // Clamp to order size. - takerTokenFillAmount = LibSafeMathV06.min256( - order.takerAssetAmount, - takerTokenFillAmount - ); - // Perform the fill. return _fillOrder( order, signature, takerTokenFillAmount, - protocolFee, + state, makerToken, takerFeeToken == takerToken ); @@ -373,7 +382,7 @@ contract FillQuoteTransformer is /// @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 state Intermediate state variables to get around stack limits. /// @param makerToken The maker token. /// @param isTakerFeeInTakerToken Whether the taker fee token is the same as the /// taker token. @@ -381,38 +390,116 @@ contract FillQuoteTransformer is IExchange.Order memory order, bytes memory signature, uint256 takerAssetFillAmount, - uint256 protocolFee, + FillState memory state, IERC20TokenV06 makerToken, bool isTakerFeeInTakerToken ) private returns (FillOrderResults memory results) { - // Track changes in the maker token balance. - uint256 initialMakerTokenBalance = 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(initialMakerTokenBalance); - // 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) { + // Clamp to remaining taker asset amount or order size. + uint256 availableTakerAssetFillAmount = + takerAssetFillAmount.min256(order.takerAssetAmount); + availableTakerAssetFillAmount = + availableTakerAssetFillAmount.min256(state.takerTokenBalanceRemaining); + // If it is a Bridge order we fill this directly + // rather than filling via 0x Exchange + if (order.makerAssetData.readBytes4(0) == ERC20_BRIDGE_PROXY_ID) { + // Calculate the amount (in maker token) we expect to receive + // from the bridge + uint256 outputTokenAmount = LibMathV06.getPartialAmountFloor( + availableTakerAssetFillAmount, + order.takerAssetAmount, + order.makerAssetAmount + ); + (bool success, bytes memory data) = address(_implementation).delegatecall( + abi.encodeWithSelector( + this.fillBridgeOrder.selector, + order.makerAddress, + order.makerAssetData, + order.takerAssetData, + availableTakerAssetFillAmount, + outputTokenAmount + ) + ); // Swallow failures, leaving all results as zero. + // TransformERC20 asserts the overall price is as expected. It is possible + // a subsequent fill can net out at the expected price so we do not assert + // the trade balance + if (success) { + results.makerTokenBoughtAmount = makerToken + .balanceOf(address(this)) + .safeSub(state.boughtAmount); + results.takerTokenSoldAmount = availableTakerAssetFillAmount; + // protocol fee paid remains 0 + } + } else { + // Emit an event if we do not have sufficient ETH to cover the protocol fee. + if (state.ethRemaining < state.protocolFee) { + emit ProtocolFeeUnfunded(state.ethRemaining, state.protocolFee); + return results; + } + try + exchange.fillOrder + {value: state.protocolFee} + (order, availableTakerAssetFillAmount, signature) + returns (IExchange.FillResults memory fillResults) + { + results.makerTokenBoughtAmount = fillResults.makerAssetFilledAmount; + results.takerTokenSoldAmount = fillResults.takerAssetFilledAmount; + results.protocolFeePaid = fillResults.protocolFeePaid; + // 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) { + // Swallow failures, leaving all results as zero. + } } } + /// @dev Attempt to fill an ERC20 Bridge order. If the fill reverts, + /// or the amount filled was not sufficient this reverts. + /// @param makerAddress The address of the maker. + /// @param makerAssetData The encoded ERC20BridgeProxy asset data. + /// @param takerAssetData The encoded ERC20 asset data. + /// @param inputTokenAmount How much taker asset to fill clamped to the available balance. + /// @param outputTokenAmount How much maker asset to receive. + function fillBridgeOrder( + address makerAddress, + bytes calldata makerAssetData, + bytes calldata takerAssetData, + uint256 inputTokenAmount, + uint256 outputTokenAmount + ) + external + { + // Track changes in the maker token balance. + ( + address tokenAddress, + address bridgeAddress, + bytes memory bridgeData + ) = abi.decode( + makerAssetData.sliceDestructive(4, makerAssetData.length), + (address, address, bytes) + ); + require(bridgeAddress != address(this), "INVALID_BRIDGE_ADDRESS"); + // Transfer the tokens to the bridge to perform the work + _getTokenFromERC20AssetData(takerAssetData).compatTransfer( + bridgeAddress, + inputTokenAmount + ); + IERC20Bridge(bridgeAddress).bridgeTransferFrom( + tokenAddress, + makerAddress, + address(this), + outputTokenAmount, // amount to transfer back from the bridge + bridgeData + ); + } + /// @dev Extract the token from plain ERC20 asset data. /// If the asset-data is empty, a zero token address will be returned. /// @param assetData The order asset data. diff --git a/contracts/zero-ex/contracts/src/transformers/Transformer.sol b/contracts/zero-ex/contracts/src/transformers/Transformer.sol index 855745e32b..919a387465 100644 --- a/contracts/zero-ex/contracts/src/transformers/Transformer.sol +++ b/contracts/zero-ex/contracts/src/transformers/Transformer.sol @@ -33,7 +33,7 @@ abstract contract Transformer is /// @dev The address of the deployer. address public immutable deployer; /// @dev The original address of this contract. - address private immutable _implementation; + address internal immutable _implementation; /// @dev Create this contract. constructor() public { diff --git a/contracts/zero-ex/contracts/src/vendor/v3/IERC20Bridge.sol b/contracts/zero-ex/contracts/src/vendor/v3/IERC20Bridge.sol new file mode 100644 index 0000000000..b690ac65e4 --- /dev/null +++ b/contracts/zero-ex/contracts/src/vendor/v3/IERC20Bridge.sol @@ -0,0 +1,55 @@ +/* + + 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; + +interface IERC20Bridge { + + /// @dev Emitted when a trade occurs. + /// @param inputToken The token the bridge is converting from. + /// @param outputToken The token the bridge is converting to. + /// @param inputTokenAmount Amount of input token. + /// @param outputTokenAmount Amount of output token. + /// @param from The `from` address in `bridgeTransferFrom()` + /// @param to The `to` address in `bridgeTransferFrom()` + event ERC20BridgeTransfer( + address inputToken, + address outputToken, + uint256 inputTokenAmount, + uint256 outputTokenAmount, + address from, + address to + ); + + /// @dev Transfers `amount` of the ERC20 `tokenAddress` from `from` to `to`. + /// @param tokenAddress The address of the ERC20 token to transfer. + /// @param from Address to transfer asset from. + /// @param to Address to transfer asset to. + /// @param amount Amount of asset to transfer. + /// @param bridgeData Arbitrary asset data needed by the bridge contract. + /// @return success The magic bytes `0xdc1600f3` if successful. + function bridgeTransferFrom( + address tokenAddress, + address from, + address to, + uint256 amount, + bytes calldata bridgeData + ) + external + returns (bytes4 success); +} diff --git a/contracts/zero-ex/contracts/src/vendor/v3/IGasToken.sol b/contracts/zero-ex/contracts/src/vendor/v3/IGasToken.sol new file mode 100644 index 0000000000..4526c5abf7 --- /dev/null +++ b/contracts/zero-ex/contracts/src/vendor/v3/IGasToken.sol @@ -0,0 +1,37 @@ +/* + + 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; + +interface IGasToken { + + /// @dev Frees up to `value` sub-tokens + /// @param value The amount of tokens to free + /// @return freed How many tokens were freed + function freeUpTo(uint256 value) external returns (uint256 freed); + + /// @dev Frees up to `value` sub-tokens owned by `from` + /// @param from The owner of tokens to spend + /// @param value The amount of tokens to free + /// @return freed How many tokens were freed + function freeFromUpTo(address from, uint256 value) external returns (uint256 freed); + + /// @dev Mints `value` amount of tokens + /// @param value The amount of tokens to mint + function mint(uint256 value) external; +} diff --git a/contracts/zero-ex/contracts/test/TestFillQuoteTransformerBridge.sol b/contracts/zero-ex/contracts/test/TestFillQuoteTransformerBridge.sol new file mode 100644 index 0000000000..8bf6fbdcc7 --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestFillQuoteTransformerBridge.sol @@ -0,0 +1,65 @@ +/* + + 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/LibMathV06.sol"; +import "../src/vendor/v3/IERC20Bridge.sol"; +import "./TestMintableERC20Token.sol"; + + +contract TestFillQuoteTransformerBridge { + + struct FillBehavior { + // Scaling for maker assets minted, in 1e18. + uint256 makerAssetMintRatio; + } + + bytes4 private constant ERC20_BRIDGE_PROXY_ID = 0xdc1600f3; + + function bridgeTransferFrom( + address tokenAddress, + address from, + address to, + uint256 amount, + bytes calldata bridgeData + ) + external + returns (bytes4 success) + { + FillBehavior memory behavior = abi.decode(bridgeData, (FillBehavior)); + TestMintableERC20Token(tokenAddress).mint( + to, + LibMathV06.getPartialAmountFloor( + behavior.makerAssetMintRatio, + 1e18, + amount + ) + ); + return ERC20_BRIDGE_PROXY_ID; + } + + function encodeBehaviorData(FillBehavior calldata behavior) + external + pure + returns (bytes memory encoded) + { + return abi.encode(behavior); + } +} diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index a69fd38488..1a51a20b20 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -35,12 +35,13 @@ "lint-contracts": "#solhint -c ../.solhint.json contracts/**/**/**/**/*.sol", "compile:truffle": "truffle compile", "docs:md": "ts-doc-gen --sourceDir='$PROJECT_FILES' --output=$MD_FILE_DIR --fileExtension=mdx --tsconfig=./typedoc-tsconfig.json", - "docs:json": "typedoc --excludePrivate --excludeExternals --excludeProtected --ignoreCompilerErrors --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES" + "docs:json": "typedoc --excludePrivate --excludeExternals --excludeProtected --ignoreCompilerErrors --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES", + "publish:private": "yarn build && gitpkg publish" }, "config": { "publicInterfaceContracts": "ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnable,ISimpleFunctionRegistry,ITokenSpender,ITransformERC20,FillQuoteTransformer,PayTakerTransformer,WethTransformer,Ownable,SimpleFunctionRegistry,TransformERC20,TokenSpender,AffiliateFeeTransformer", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|Bootstrap|FillQuoteTransformer|FixinCommon|FlashWallet|FullMigration|IAllowanceTarget|IBootstrap|IERC20Transformer|IExchange|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|PayTakerTransformer|SimpleFunctionRegistry|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpender|TransformERC20|Transformer|TransformerDeployer|WethTransformer|ZeroEx).json" + "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|Bootstrap|FillQuoteTransformer|FixinCommon|FixinGasToken|FlashWallet|FullMigration|IAllowanceTarget|IBootstrap|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|IOwnable|ISimpleFunctionRegistry|ITestSimpleFunctionRegistryFeature|ITokenSpender|ITransformERC20|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|Ownable|PayTakerTransformer|SimpleFunctionRegistry|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpender|TransformERC20|Transformer|TransformerDeployer|WethTransformer|ZeroEx).json" }, "repository": { "type": "git", diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index e1fb0476b2..eee23af238 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -10,14 +10,17 @@ import * as AllowanceTarget from '../test/generated-artifacts/AllowanceTarget.js 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 FixinGasToken from '../test/generated-artifacts/FixinGasToken.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 IERC20Bridge from '../test/generated-artifacts/IERC20Bridge.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 IGasToken from '../test/generated-artifacts/IGasToken.json'; import * as InitialMigration from '../test/generated-artifacts/InitialMigration.json'; import * as IOwnable from '../test/generated-artifacts/IOwnable.json'; import * as ISimpleFunctionRegistry from '../test/generated-artifacts/ISimpleFunctionRegistry.json'; @@ -45,6 +48,7 @@ import * as PayTakerTransformer from '../test/generated-artifacts/PayTakerTransf import * as SimpleFunctionRegistry from '../test/generated-artifacts/SimpleFunctionRegistry.json'; import * as TestCallTarget from '../test/generated-artifacts/TestCallTarget.json'; import * as TestDelegateCaller from '../test/generated-artifacts/TestDelegateCaller.json'; +import * as TestFillQuoteTransformerBridge from '../test/generated-artifacts/TestFillQuoteTransformerBridge.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'; @@ -95,6 +99,7 @@ export const artifacts = { TokenSpender: TokenSpender as ContractArtifact, TransformERC20: TransformERC20 as ContractArtifact, FixinCommon: FixinCommon as ContractArtifact, + FixinGasToken: FixinGasToken as ContractArtifact, FullMigration: FullMigration as ContractArtifact, InitialMigration: InitialMigration as ContractArtifact, LibBootstrap: LibBootstrap as ContractArtifact, @@ -112,10 +117,13 @@ export const artifacts = { PayTakerTransformer: PayTakerTransformer as ContractArtifact, Transformer: Transformer as ContractArtifact, WethTransformer: WethTransformer as ContractArtifact, + IERC20Bridge: IERC20Bridge as ContractArtifact, IExchange: IExchange as ContractArtifact, + IGasToken: IGasToken as ContractArtifact, ITestSimpleFunctionRegistryFeature: ITestSimpleFunctionRegistryFeature as ContractArtifact, TestCallTarget: TestCallTarget as ContractArtifact, TestDelegateCaller: TestDelegateCaller as ContractArtifact, + TestFillQuoteTransformerBridge: TestFillQuoteTransformerBridge as ContractArtifact, TestFillQuoteTransformerExchange: TestFillQuoteTransformerExchange as ContractArtifact, TestFillQuoteTransformerHost: TestFillQuoteTransformerHost as ContractArtifact, TestFullMigration: TestFullMigration as ContractArtifact, diff --git a/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts b/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts index dd842e79ee..92bd69d2ef 100644 --- a/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts +++ b/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts @@ -18,6 +18,7 @@ import { BigNumber, hexUtils, ZeroExRevertErrors } from '@0x/utils'; import * as _ from 'lodash'; import { artifacts } from '../artifacts'; +import { TestFillQuoteTransformerBridgeContract } from '../generated-wrappers/test_fill_quote_transformer_bridge'; import { FillQuoteTransformerContract, TestFillQuoteTransformerExchangeContract, @@ -31,6 +32,7 @@ blockchainTests.resets('FillQuoteTransformer', env => { let maker: string; let feeRecipient: string; let exchange: TestFillQuoteTransformerExchangeContract; + let bridge: TestFillQuoteTransformerBridgeContract; let transformer: FillQuoteTransformerContract; let host: TestFillQuoteTransformerHostContract; let makerToken: TestMintableERC20TokenContract; @@ -64,6 +66,12 @@ blockchainTests.resets('FillQuoteTransformer', env => { }, artifacts, ); + bridge = await TestFillQuoteTransformerBridgeContract.deployFrom0xArtifactAsync( + artifacts.TestFillQuoteTransformerBridge, + env.provider, + env.txDefaults, + artifacts, + ); [makerToken, takerToken, takerFeeToken] = await Promise.all( _.times(3, async () => TestMintableERC20TokenContract.deployFrom0xArtifactAsync( @@ -102,6 +110,19 @@ blockchainTests.resets('FillQuoteTransformer', env => { }; } + function createBridgeOrder(fields: Partial = {}, bridgeData: string = encodeBridgeBehavior()): FilledOrder { + const order = createOrder(fields); + return { + ...order, + makerAddress: bridge.address, + makerAssetData: assetDataUtils.encodeERC20BridgeAssetData(makerToken.address, bridge.address, bridgeData), + makerFeeAssetData: NULL_BYTES, + takerFeeAssetData: NULL_BYTES, + makerFee: ZERO_AMOUNT, + takerFee: ZERO_AMOUNT, + }; + } + interface QuoteFillResults { makerAssetBought: BigNumber; takerAssetSpent: BigNumber; @@ -244,6 +265,17 @@ blockchainTests.resets('FillQuoteTransformer', env => { ); } + function encodeBridgeBehavior(makerAssetMintRatio: Numberish = 1.0): string { + return hexUtils.slice( + bridge + .encodeBehaviorData({ + makerAssetMintRatio: new BigNumber(makerAssetMintRatio).times('1e18').integerValue(), + }) + .getABIEncodedTransactionData(), + 4, + ); + } + const ERC20_ASSET_PROXY_ID = '0xf47261b0'; describe('sell quotes', () => { @@ -436,9 +468,10 @@ blockchainTests.resets('FillQuoteTransformer', env => { ) .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid.minus(1) }); return expect(tx).to.revertWith( - new ZeroExRevertErrors.TransformERC20.InsufficientProtocolFeeError( - singleProtocolFee.minus(1), - singleProtocolFee, + new ZeroExRevertErrors.TransformERC20.IncompleteFillSellQuoteError( + takerToken.address, + getExpectedSellQuoteFillResults([...orders.slice(0, 2)]).takerAssetSpent, + qfr.takerAssetSpent, ), ); }); @@ -707,36 +740,6 @@ blockchainTests.resets('FillQuoteTransformer', env => { }); }); - 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, - side: FillQuoteTransformerSide.Buy, - fillAmount: 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()); @@ -861,4 +864,109 @@ blockchainTests.resets('FillQuoteTransformer', env => { }); }); }); + + describe('bridge orders', () => { + it('can fully sell to a single bridge order quote', async () => { + const orders = _.times(1, () => createBridgeOrder()); + const signatures = orders.map(() => NULL_BYTES); + const qfr = getExpectedSellQuoteFillResults(orders); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + .awaitTransactionSuccessAsync({ value: ZERO_AMOUNT }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + + it('can sell to a mix of order quote', async () => { + const nativeOrders = [createOrder()]; + const bridgeOrders = [createBridgeOrder()]; + const orders = [...nativeOrders, ...bridgeOrders]; + const signatures = [ + ...nativeOrders.map(() => encodeExchangeBehavior()), // Valid Signatures + ...bridgeOrders.map(() => NULL_BYTES), // Valid Signatures + ]; + const qfr = getExpectedSellQuoteFillResults(orders); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + .awaitTransactionSuccessAsync({ value: singleProtocolFee.times(nativeOrders.length) }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + + it('can attempt to sell to a mix of order quote handling reverts', async () => { + const nativeOrders = _.times(3, () => createOrder()); + const bridgeOrders = [createBridgeOrder()]; + const orders = [...nativeOrders, ...bridgeOrders]; + const signatures = [ + ...nativeOrders.map(() => NULL_BYTES), // Invalid Signatures + ...bridgeOrders.map(() => NULL_BYTES), // Valid Signatures + ]; + const qfr = getExpectedSellQuoteFillResults(bridgeOrders); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + // Single protocol fee as all Native orders will fail + .awaitTransactionSuccessAsync({ value: singleProtocolFee }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + protocolFeeBalance: singleProtocolFee, + }); + }); + + it('can continue to the bridge order if the native order reverts', async () => { + const nativeOrders = [createOrder()]; + const bridgeOrders = [createBridgeOrder()]; + const orders = [...nativeOrders, ...bridgeOrders]; + const signatures = [ + ...nativeOrders.map(() => encodeExchangeBehavior()), // Valid Signatures + ...bridgeOrders.map(() => NULL_BYTES), // Valid Signatures + ]; + const qfr = getExpectedSellQuoteFillResults(bridgeOrders); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + // Insufficient single protocol fee + .awaitTransactionSuccessAsync({ value: singleProtocolFee.minus(1) }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + protocolFeeBalance: singleProtocolFee, + }); + }); + }); }); diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index 00bdcffb29..803fdf09c9 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -8,14 +8,17 @@ 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/fixin_gas_token'; 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_bridge'; 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_gas_token'; export * from '../test/generated-wrappers/i_ownable'; export * from '../test/generated-wrappers/i_simple_function_registry'; export * from '../test/generated-wrappers/i_test_simple_function_registry_feature'; @@ -43,6 +46,7 @@ export * from '../test/generated-wrappers/pay_taker_transformer'; export * from '../test/generated-wrappers/simple_function_registry'; export * from '../test/generated-wrappers/test_call_target'; export * from '../test/generated-wrappers/test_delegate_caller'; +export * from '../test/generated-wrappers/test_fill_quote_transformer_bridge'; export * from '../test/generated-wrappers/test_fill_quote_transformer_exchange'; export * from '../test/generated-wrappers/test_fill_quote_transformer_host'; export * from '../test/generated-wrappers/test_full_migration'; diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index 2e0d70c2d5..c161d5ff74 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -26,14 +26,17 @@ "test/generated-artifacts/Bootstrap.json", "test/generated-artifacts/FillQuoteTransformer.json", "test/generated-artifacts/FixinCommon.json", + "test/generated-artifacts/FixinGasToken.json", "test/generated-artifacts/FlashWallet.json", "test/generated-artifacts/FullMigration.json", "test/generated-artifacts/IAllowanceTarget.json", "test/generated-artifacts/IBootstrap.json", + "test/generated-artifacts/IERC20Bridge.json", "test/generated-artifacts/IERC20Transformer.json", "test/generated-artifacts/IExchange.json", "test/generated-artifacts/IFeature.json", "test/generated-artifacts/IFlashWallet.json", + "test/generated-artifacts/IGasToken.json", "test/generated-artifacts/IOwnable.json", "test/generated-artifacts/ISimpleFunctionRegistry.json", "test/generated-artifacts/ITestSimpleFunctionRegistryFeature.json", @@ -61,6 +64,7 @@ "test/generated-artifacts/SimpleFunctionRegistry.json", "test/generated-artifacts/TestCallTarget.json", "test/generated-artifacts/TestDelegateCaller.json", + "test/generated-artifacts/TestFillQuoteTransformerBridge.json", "test/generated-artifacts/TestFillQuoteTransformerExchange.json", "test/generated-artifacts/TestFillQuoteTransformerHost.json", "test/generated-artifacts/TestFullMigration.json",