feat: opt-in positive slippage fee for integrators (#101)
* feat: Positive Slippage Fee * fix: rename ethToTakerAssetRate to takerAssetPriceForOneEth * fix: rename takerAssetPriceForOneEth to takerAssetsPerEth * fix: export AffiliateFeeType * rebased off development * Add a gasOverhead for non-deterministic operations * CHANGELOGs * rename outputTokens to outputAmount * Confirm transformer addresses on Mainnet and Ropsten * fix import Co-authored-by: Jacob Evans <jacob@dekz.net>
This commit is contained in:
		| @@ -21,6 +21,10 @@ | |||||||
|             { |             { | ||||||
|                 "note": "refund ETH with no gas limit in FQT", |                 "note": "refund ETH with no gas limit in FQT", | ||||||
|                 "pr": 155 |                 "pr": 155 | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 "note": "Added an opt-in `PositiveSlippageAffiliateFee`", | ||||||
|  |                 "pr": 101 | ||||||
|             } |             } | ||||||
|         ] |         ] | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -0,0 +1,68 @@ | |||||||
|  | // SPDX-License-Identifier: Apache-2.0 | ||||||
|  | /* | ||||||
|  |  | ||||||
|  |   Copyright 2021 ZeroEx Intl. | ||||||
|  |  | ||||||
|  |   Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  |   you may not use this file except in compliance with the License. | ||||||
|  |   You may obtain a copy of the License at | ||||||
|  |  | ||||||
|  |     http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  | ||||||
|  |   Unless required by applicable law or agreed to in writing, software | ||||||
|  |   distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  |   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  |   See the License for the specific language governing permissions and | ||||||
|  |   limitations under the License. | ||||||
|  |  | ||||||
|  | */ | ||||||
|  |  | ||||||
|  | pragma solidity ^0.6.5; | ||||||
|  | pragma experimental ABIEncoderV2; | ||||||
|  |  | ||||||
|  | import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; | ||||||
|  | import "@0x/contracts-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 "./Transformer.sol"; | ||||||
|  | import "./LibERC20Transformer.sol"; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /// @dev A transformer that transfers tokens to arbitrary addresses. | ||||||
|  | contract PositiveSlippageFeeTransformer is | ||||||
|  |     Transformer | ||||||
|  | { | ||||||
|  |     using LibRichErrorsV06 for bytes; | ||||||
|  |     using LibSafeMathV06 for uint256; | ||||||
|  |     using LibERC20Transformer for IERC20TokenV06; | ||||||
|  |  | ||||||
|  |     /// @dev Information for a single fee. | ||||||
|  |     struct TokenFee { | ||||||
|  |         // The token to transfer to `recipient`. | ||||||
|  |         IERC20TokenV06 token; | ||||||
|  |         // Amount of each `token` to transfer to `recipient`. | ||||||
|  |         uint256 bestCaseAmount; | ||||||
|  |         // Recipient of `token`. | ||||||
|  |         address payable recipient; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// @dev Transfers tokens to recipients. | ||||||
|  |     /// @param context Context information. | ||||||
|  |     /// @return success The success bytes (`LibERC20Transformer.TRANSFORMER_SUCCESS`). | ||||||
|  |     function transform(TransformContext calldata context) | ||||||
|  |         external | ||||||
|  |         override | ||||||
|  |     returns (bytes4 success) | ||||||
|  |     { | ||||||
|  |         TokenFee memory fee = abi.decode(context.data, (TokenFee)); | ||||||
|  |  | ||||||
|  |         uint256 transformerAmount = LibERC20Transformer.getTokenBalanceOf(fee.token, address(this)); | ||||||
|  |         if (transformerAmount > fee.bestCaseAmount) { | ||||||
|  |             uint256 positiveSlippageAmount = transformerAmount - fee.bestCaseAmount; | ||||||
|  |             fee.token.transformerTransfer(fee.recipient, positiveSlippageAmount); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return LibERC20Transformer.TRANSFORMER_SUCCESS; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -41,9 +41,9 @@ | |||||||
|         "rollback": "node ./lib/scripts/rollback.js" |         "rollback": "node ./lib/scripts/rollback.js" | ||||||
|     }, |     }, | ||||||
|     "config": { |     "config": { | ||||||
|         "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature,ILiquidityProviderFeature,NativeOrdersFeature,INativeOrdersFeature,FeeCollectorController,FeeCollector,CurveLiquidityProvider", |         "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,PositiveSlippageFeeTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature,ILiquidityProviderFeature,NativeOrdersFeature,INativeOrdersFeature,FeeCollectorController,FeeCollector,CurveLiquidityProvider", | ||||||
|         "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", |         "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", | ||||||
|         "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|BridgeSource|CurveLiquidityProvider|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IFeature|IFlashWallet|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|INativeOrdersFeature|IOwnableFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinBalancer|MixinBancor|MixinCoFiX|MixinCryptoCom|MixinCurve|MixinDodo|MixinDodoV2|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinShell|MixinSushiswap|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|NativeOrdersFeature|OwnableFeature|PayTakerTransformer|PermissionlessTransformerDeployer|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestCurve|TestDelegateCaller|TestFeeCollectorController|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFixinProtocolFees|TestFixinTokenSpender|TestFullMigration|TestInitialMigration|TestLibNativeOrder|TestLibSignature|TestLiquidityProvider|TestMetaTransactionsNativeOrdersFeature|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestNativeOrdersFeature|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx|ZeroExOptimized).json" |         "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|BridgeSource|CurveLiquidityProvider|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IFeature|IFlashWallet|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|INativeOrdersFeature|IOwnableFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinBalancer|MixinBancor|MixinCoFiX|MixinCryptoCom|MixinCurve|MixinDodo|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinShell|MixinSushiswap|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|NativeOrdersFeature|OwnableFeature|PayTakerTransformer|PermissionlessTransformerDeployer|PositiveSlippageFeeTransformer|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestCurve|TestDelegateCaller|TestFeeCollectorController|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFixinProtocolFees|TestFixinTokenSpender|TestFullMigration|TestInitialMigration|TestLibNativeOrder|TestLibSignature|TestLiquidityProvider|TestMetaTransactionsNativeOrdersFeature|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestNativeOrdersFeature|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx|ZeroExOptimized).json" | ||||||
|     }, |     }, | ||||||
|     "repository": { |     "repository": { | ||||||
|         "type": "git", |         "type": "git", | ||||||
|   | |||||||
| @@ -29,6 +29,7 @@ import * as MetaTransactionsFeature from '../generated-artifacts/MetaTransaction | |||||||
| import * as NativeOrdersFeature from '../generated-artifacts/NativeOrdersFeature.json'; | import * as NativeOrdersFeature from '../generated-artifacts/NativeOrdersFeature.json'; | ||||||
| import * as OwnableFeature from '../generated-artifacts/OwnableFeature.json'; | import * as OwnableFeature from '../generated-artifacts/OwnableFeature.json'; | ||||||
| import * as PayTakerTransformer from '../generated-artifacts/PayTakerTransformer.json'; | import * as PayTakerTransformer from '../generated-artifacts/PayTakerTransformer.json'; | ||||||
|  | import * as PositiveSlippageFeeTransformer from '../generated-artifacts/PositiveSlippageFeeTransformer.json'; | ||||||
| import * as SimpleFunctionRegistryFeature from '../generated-artifacts/SimpleFunctionRegistryFeature.json'; | import * as SimpleFunctionRegistryFeature from '../generated-artifacts/SimpleFunctionRegistryFeature.json'; | ||||||
| import * as TokenSpenderFeature from '../generated-artifacts/TokenSpenderFeature.json'; | import * as TokenSpenderFeature from '../generated-artifacts/TokenSpenderFeature.json'; | ||||||
| import * as TransformERC20Feature from '../generated-artifacts/TransformERC20Feature.json'; | import * as TransformERC20Feature from '../generated-artifacts/TransformERC20Feature.json'; | ||||||
| @@ -48,6 +49,7 @@ export const artifacts = { | |||||||
|     ITransformERC20Feature: ITransformERC20Feature as ContractArtifact, |     ITransformERC20Feature: ITransformERC20Feature as ContractArtifact, | ||||||
|     FillQuoteTransformer: FillQuoteTransformer as ContractArtifact, |     FillQuoteTransformer: FillQuoteTransformer as ContractArtifact, | ||||||
|     PayTakerTransformer: PayTakerTransformer as ContractArtifact, |     PayTakerTransformer: PayTakerTransformer as ContractArtifact, | ||||||
|  |     PositiveSlippageFeeTransformer: PositiveSlippageFeeTransformer as ContractArtifact, | ||||||
|     WethTransformer: WethTransformer as ContractArtifact, |     WethTransformer: WethTransformer as ContractArtifact, | ||||||
|     OwnableFeature: OwnableFeature as ContractArtifact, |     OwnableFeature: OwnableFeature as ContractArtifact, | ||||||
|     SimpleFunctionRegistryFeature: SimpleFunctionRegistryFeature as ContractArtifact, |     SimpleFunctionRegistryFeature: SimpleFunctionRegistryFeature as ContractArtifact, | ||||||
|   | |||||||
| @@ -46,6 +46,7 @@ export { | |||||||
|     IZeroExContract, |     IZeroExContract, | ||||||
|     LogMetadataTransformerContract, |     LogMetadataTransformerContract, | ||||||
|     PayTakerTransformerContract, |     PayTakerTransformerContract, | ||||||
|  |     PositiveSlippageFeeTransformerContract, | ||||||
|     WethTransformerContract, |     WethTransformerContract, | ||||||
|     ZeroExContract, |     ZeroExContract, | ||||||
| } from './wrappers'; | } from './wrappers'; | ||||||
|   | |||||||
| @@ -27,6 +27,7 @@ export * from '../generated-wrappers/meta_transactions_feature'; | |||||||
| export * from '../generated-wrappers/native_orders_feature'; | export * from '../generated-wrappers/native_orders_feature'; | ||||||
| export * from '../generated-wrappers/ownable_feature'; | export * from '../generated-wrappers/ownable_feature'; | ||||||
| export * from '../generated-wrappers/pay_taker_transformer'; | export * from '../generated-wrappers/pay_taker_transformer'; | ||||||
|  | export * from '../generated-wrappers/positive_slippage_fee_transformer'; | ||||||
| export * from '../generated-wrappers/simple_function_registry_feature'; | export * from '../generated-wrappers/simple_function_registry_feature'; | ||||||
| export * from '../generated-wrappers/token_spender_feature'; | export * from '../generated-wrappers/token_spender_feature'; | ||||||
| export * from '../generated-wrappers/transform_erc20_feature'; | export * from '../generated-wrappers/transform_erc20_feature'; | ||||||
|   | |||||||
| @@ -92,6 +92,7 @@ import * as NativeOrdersFeature from '../test/generated-artifacts/NativeOrdersFe | |||||||
| import * as OwnableFeature from '../test/generated-artifacts/OwnableFeature.json'; | import * as OwnableFeature from '../test/generated-artifacts/OwnableFeature.json'; | ||||||
| import * as PayTakerTransformer from '../test/generated-artifacts/PayTakerTransformer.json'; | import * as PayTakerTransformer from '../test/generated-artifacts/PayTakerTransformer.json'; | ||||||
| import * as PermissionlessTransformerDeployer from '../test/generated-artifacts/PermissionlessTransformerDeployer.json'; | import * as PermissionlessTransformerDeployer from '../test/generated-artifacts/PermissionlessTransformerDeployer.json'; | ||||||
|  | import * as PositiveSlippageFeeTransformer from '../test/generated-artifacts/PositiveSlippageFeeTransformer.json'; | ||||||
| import * as SimpleFunctionRegistryFeature from '../test/generated-artifacts/SimpleFunctionRegistryFeature.json'; | import * as SimpleFunctionRegistryFeature from '../test/generated-artifacts/SimpleFunctionRegistryFeature.json'; | ||||||
| import * as TestBridge from '../test/generated-artifacts/TestBridge.json'; | import * as TestBridge from '../test/generated-artifacts/TestBridge.json'; | ||||||
| import * as TestCallTarget from '../test/generated-artifacts/TestCallTarget.json'; | import * as TestCallTarget from '../test/generated-artifacts/TestCallTarget.json'; | ||||||
| @@ -209,6 +210,7 @@ export const artifacts = { | |||||||
|     LibERC20Transformer: LibERC20Transformer as ContractArtifact, |     LibERC20Transformer: LibERC20Transformer as ContractArtifact, | ||||||
|     LogMetadataTransformer: LogMetadataTransformer as ContractArtifact, |     LogMetadataTransformer: LogMetadataTransformer as ContractArtifact, | ||||||
|     PayTakerTransformer: PayTakerTransformer as ContractArtifact, |     PayTakerTransformer: PayTakerTransformer as ContractArtifact, | ||||||
|  |     PositiveSlippageFeeTransformer: PositiveSlippageFeeTransformer as ContractArtifact, | ||||||
|     Transformer: Transformer as ContractArtifact, |     Transformer: Transformer as ContractArtifact, | ||||||
|     WethTransformer: WethTransformer as ContractArtifact, |     WethTransformer: WethTransformer as ContractArtifact, | ||||||
|     BridgeAdapter: BridgeAdapter as ContractArtifact, |     BridgeAdapter: BridgeAdapter as ContractArtifact, | ||||||
|   | |||||||
| @@ -0,0 +1,127 @@ | |||||||
|  | import { blockchainTests, constants, expect, getRandomInteger, randomAddress } from '@0x/contracts-test-utils'; | ||||||
|  | import { encodePositiveSlippageFeeTransformerData } from '@0x/protocol-utils'; | ||||||
|  | import { BigNumber } from '@0x/utils'; | ||||||
|  |  | ||||||
|  | import { artifacts } from '../artifacts'; | ||||||
|  | import { | ||||||
|  |     PositiveSlippageFeeTransformerContract, | ||||||
|  |     TestMintableERC20TokenContract, | ||||||
|  |     TestTransformerHostContract, | ||||||
|  | } from '../wrappers'; | ||||||
|  |  | ||||||
|  | const { ZERO_AMOUNT } = constants; | ||||||
|  |  | ||||||
|  | blockchainTests.resets('PositiveSlippageFeeTransformer', env => { | ||||||
|  |     const recipient = randomAddress(); | ||||||
|  |     let caller: string; | ||||||
|  |     let token: TestMintableERC20TokenContract; | ||||||
|  |     let transformer: PositiveSlippageFeeTransformerContract; | ||||||
|  |     let host: TestTransformerHostContract; | ||||||
|  |  | ||||||
|  |     before(async () => { | ||||||
|  |         [caller] = await env.getAccountAddressesAsync(); | ||||||
|  |         token = await TestMintableERC20TokenContract.deployFrom0xArtifactAsync( | ||||||
|  |             artifacts.TestMintableERC20Token, | ||||||
|  |             env.provider, | ||||||
|  |             env.txDefaults, | ||||||
|  |             artifacts, | ||||||
|  |         ); | ||||||
|  |         transformer = await PositiveSlippageFeeTransformerContract.deployFrom0xArtifactAsync( | ||||||
|  |             artifacts.PositiveSlippageFeeTransformer, | ||||||
|  |             env.provider, | ||||||
|  |             env.txDefaults, | ||||||
|  |             artifacts, | ||||||
|  |         ); | ||||||
|  |         host = await TestTransformerHostContract.deployFrom0xArtifactAsync( | ||||||
|  |             artifacts.TestTransformerHost, | ||||||
|  |             env.provider, | ||||||
|  |             { ...env.txDefaults, from: caller }, | ||||||
|  |             artifacts, | ||||||
|  |         ); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     interface Balances { | ||||||
|  |         ethBalance: BigNumber; | ||||||
|  |         tokenBalance: BigNumber; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async function getBalancesAsync(owner: string): Promise<Balances> { | ||||||
|  |         return { | ||||||
|  |             ethBalance: await env.web3Wrapper.getBalanceInWeiAsync(owner), | ||||||
|  |             tokenBalance: await token.balanceOf(owner).callAsync(), | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async function mintHostTokensAsync(amount: BigNumber): Promise<void> { | ||||||
|  |         await token.mint(host.address, amount).awaitTransactionSuccessAsync(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     it('does not transfer positive slippage fees when bestCaseAmount is equal to amount', async () => { | ||||||
|  |         const amount = getRandomInteger(1, '1e18'); | ||||||
|  |         const data = encodePositiveSlippageFeeTransformerData({ | ||||||
|  |             token: token.address, | ||||||
|  |             bestCaseAmount: amount, | ||||||
|  |             recipient, | ||||||
|  |         }); | ||||||
|  |         await mintHostTokensAsync(amount); | ||||||
|  |         const beforeBalanceHost = await getBalancesAsync(host.address); | ||||||
|  |         const beforeBalanceRecipient = await getBalancesAsync(recipient); | ||||||
|  |         await host | ||||||
|  |             .rawExecuteTransform(transformer.address, { | ||||||
|  |                 data, | ||||||
|  |                 sender: randomAddress(), | ||||||
|  |                 taker: randomAddress(), | ||||||
|  |             }) | ||||||
|  |             .awaitTransactionSuccessAsync(); | ||||||
|  |         expect(await getBalancesAsync(host.address)).to.deep.eq(beforeBalanceHost); | ||||||
|  |         expect(await getBalancesAsync(recipient)).to.deep.eq(beforeBalanceRecipient); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it('does not transfer positive slippage fees when bestCaseAmount is higher than amount', async () => { | ||||||
|  |         const amount = getRandomInteger(1, '1e18'); | ||||||
|  |         const bestCaseAmount = amount.times(1.1).decimalPlaces(0, BigNumber.ROUND_FLOOR); | ||||||
|  |         const data = encodePositiveSlippageFeeTransformerData({ | ||||||
|  |             token: token.address, | ||||||
|  |             bestCaseAmount, | ||||||
|  |             recipient, | ||||||
|  |         }); | ||||||
|  |         await mintHostTokensAsync(amount); | ||||||
|  |         const beforeBalanceHost = await getBalancesAsync(host.address); | ||||||
|  |         const beforeBalanceRecipient = await getBalancesAsync(recipient); | ||||||
|  |         await host | ||||||
|  |             .rawExecuteTransform(transformer.address, { | ||||||
|  |                 data, | ||||||
|  |                 sender: randomAddress(), | ||||||
|  |                 taker: randomAddress(), | ||||||
|  |             }) | ||||||
|  |             .awaitTransactionSuccessAsync(); | ||||||
|  |         expect(await getBalancesAsync(host.address)).to.deep.eq(beforeBalanceHost); | ||||||
|  |         expect(await getBalancesAsync(recipient)).to.deep.eq(beforeBalanceRecipient); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it('send positive slippage fee to recipient when bestCaseAmount is lower than amount', async () => { | ||||||
|  |         const amount = getRandomInteger(1, '1e18'); | ||||||
|  |         const bestCaseAmount = amount.times(0.95).decimalPlaces(0, BigNumber.ROUND_FLOOR); | ||||||
|  |         const data = encodePositiveSlippageFeeTransformerData({ | ||||||
|  |             token: token.address, | ||||||
|  |             bestCaseAmount, | ||||||
|  |             recipient, | ||||||
|  |         }); | ||||||
|  |         await mintHostTokensAsync(amount); | ||||||
|  |         await host | ||||||
|  |             .rawExecuteTransform(transformer.address, { | ||||||
|  |                 data, | ||||||
|  |                 sender: randomAddress(), | ||||||
|  |                 taker: randomAddress(), | ||||||
|  |             }) | ||||||
|  |             .awaitTransactionSuccessAsync(); | ||||||
|  |         expect(await getBalancesAsync(host.address)).to.deep.eq({ | ||||||
|  |             tokenBalance: bestCaseAmount, | ||||||
|  |             ethBalance: ZERO_AMOUNT, | ||||||
|  |         }); | ||||||
|  |         expect(await getBalancesAsync(recipient)).to.deep.eq({ | ||||||
|  |             tokenBalance: amount.minus(bestCaseAmount), // positive slippage | ||||||
|  |             ethBalance: ZERO_AMOUNT, | ||||||
|  |         }); | ||||||
|  |     }); | ||||||
|  | }); | ||||||
| @@ -90,6 +90,7 @@ export * from '../test/generated-wrappers/native_orders_feature'; | |||||||
| export * from '../test/generated-wrappers/ownable_feature'; | export * from '../test/generated-wrappers/ownable_feature'; | ||||||
| export * from '../test/generated-wrappers/pay_taker_transformer'; | export * from '../test/generated-wrappers/pay_taker_transformer'; | ||||||
| export * from '../test/generated-wrappers/permissionless_transformer_deployer'; | export * from '../test/generated-wrappers/permissionless_transformer_deployer'; | ||||||
|  | export * from '../test/generated-wrappers/positive_slippage_fee_transformer'; | ||||||
| export * from '../test/generated-wrappers/simple_function_registry_feature'; | export * from '../test/generated-wrappers/simple_function_registry_feature'; | ||||||
| export * from '../test/generated-wrappers/test_bridge'; | export * from '../test/generated-wrappers/test_bridge'; | ||||||
| export * from '../test/generated-wrappers/test_call_target'; | export * from '../test/generated-wrappers/test_call_target'; | ||||||
|   | |||||||
| @@ -27,6 +27,7 @@ | |||||||
|         "generated-artifacts/NativeOrdersFeature.json", |         "generated-artifacts/NativeOrdersFeature.json", | ||||||
|         "generated-artifacts/OwnableFeature.json", |         "generated-artifacts/OwnableFeature.json", | ||||||
|         "generated-artifacts/PayTakerTransformer.json", |         "generated-artifacts/PayTakerTransformer.json", | ||||||
|  |         "generated-artifacts/PositiveSlippageFeeTransformer.json", | ||||||
|         "generated-artifacts/SimpleFunctionRegistryFeature.json", |         "generated-artifacts/SimpleFunctionRegistryFeature.json", | ||||||
|         "generated-artifacts/TokenSpenderFeature.json", |         "generated-artifacts/TokenSpenderFeature.json", | ||||||
|         "generated-artifacts/TransformERC20Feature.json", |         "generated-artifacts/TransformERC20Feature.json", | ||||||
| @@ -119,6 +120,7 @@ | |||||||
|         "test/generated-artifacts/OwnableFeature.json", |         "test/generated-artifacts/OwnableFeature.json", | ||||||
|         "test/generated-artifacts/PayTakerTransformer.json", |         "test/generated-artifacts/PayTakerTransformer.json", | ||||||
|         "test/generated-artifacts/PermissionlessTransformerDeployer.json", |         "test/generated-artifacts/PermissionlessTransformerDeployer.json", | ||||||
|  |         "test/generated-artifacts/PositiveSlippageFeeTransformer.json", | ||||||
|         "test/generated-artifacts/SimpleFunctionRegistryFeature.json", |         "test/generated-artifacts/SimpleFunctionRegistryFeature.json", | ||||||
|         "test/generated-artifacts/TestBridge.json", |         "test/generated-artifacts/TestBridge.json", | ||||||
|         "test/generated-artifacts/TestCallTarget.json", |         "test/generated-artifacts/TestCallTarget.json", | ||||||
|   | |||||||
| @@ -49,6 +49,10 @@ | |||||||
|             { |             { | ||||||
|                 "note": "Add an alternative RFQ market making implementation", |                 "note": "Add an alternative RFQ market making implementation", | ||||||
|                 "pr": 139 |                 "pr": 139 | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 "note": "Added an opt-in `PositiveSlippageAffiliateFee`", | ||||||
|  |                 "pr": 101 | ||||||
|             } |             } | ||||||
|         ] |         ] | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import { SignatureType } from '@0x/protocol-utils'; | |||||||
| import { BigNumber, logUtils } from '@0x/utils'; | import { BigNumber, logUtils } from '@0x/utils'; | ||||||
|  |  | ||||||
| import { | import { | ||||||
|  |     AffiliateFeeType, | ||||||
|     ExchangeProxyContractOpts, |     ExchangeProxyContractOpts, | ||||||
|     LogFunction, |     LogFunction, | ||||||
|     OrderPrunerOpts, |     OrderPrunerOpts, | ||||||
| @@ -12,7 +13,11 @@ import { | |||||||
|     SwapQuoteRequestOpts, |     SwapQuoteRequestOpts, | ||||||
|     SwapQuoterOpts, |     SwapQuoterOpts, | ||||||
| } from './types'; | } from './types'; | ||||||
| import { DEFAULT_GET_MARKET_ORDERS_OPTS, TOKENS } from './utils/market_operation_utils/constants'; | import { | ||||||
|  |     DEFAULT_GET_MARKET_ORDERS_OPTS, | ||||||
|  |     DEFAULT_INTERMEDIATE_TOKENS, | ||||||
|  |     DEFAULT_TOKEN_ADJACENCY_GRAPH, | ||||||
|  | } from './utils/market_operation_utils/constants'; | ||||||
|  |  | ||||||
| const ETH_GAS_STATION_API_URL = 'https://ethgasstation.info/api/ethgasAPI.json'; | const ETH_GAS_STATION_API_URL = 'https://ethgasstation.info/api/ethgasAPI.json'; | ||||||
| const NULL_BYTES = '0x'; | const NULL_BYTES = '0x'; | ||||||
| @@ -38,7 +43,6 @@ const PROTOCOL_FEE_MULTIPLIER = new BigNumber(70000); | |||||||
| // default 50% buffer for selecting native orders to be aggregated with other sources | // default 50% buffer for selecting native orders to be aggregated with other sources | ||||||
| const MARKET_UTILS_AMOUNT_BUFFER_PERCENTAGE = 0.5; | const MARKET_UTILS_AMOUNT_BUFFER_PERCENTAGE = 0.5; | ||||||
|  |  | ||||||
| const DEFAULT_INTERMEDIATE_TOKENS = [TOKENS.WETH, TOKENS.USDT, TOKENS.DAI, TOKENS.USDC]; |  | ||||||
| const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = { | const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = { | ||||||
|     chainId: ChainId.Mainnet, |     chainId: ChainId.Mainnet, | ||||||
|     orderRefreshIntervalMs: 10000, // 10 seconds |     orderRefreshIntervalMs: 10000, // 10 seconds | ||||||
| @@ -49,12 +53,14 @@ const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = { | |||||||
|         takerApiKeyWhitelist: [], |         takerApiKeyWhitelist: [], | ||||||
|         makerAssetOfferings: {}, |         makerAssetOfferings: {}, | ||||||
|     }, |     }, | ||||||
|  |     tokenAdjacencyGraph: DEFAULT_TOKEN_ADJACENCY_GRAPH, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS: ExchangeProxyContractOpts = { | const DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS: ExchangeProxyContractOpts = { | ||||||
|     isFromETH: false, |     isFromETH: false, | ||||||
|     isToETH: false, |     isToETH: false, | ||||||
|     affiliateFee: { |     affiliateFee: { | ||||||
|  |         feeType: AffiliateFeeType.None, | ||||||
|         recipient: NULL_ADDRESS, |         recipient: NULL_ADDRESS, | ||||||
|         buyTokenFeeAmount: ZERO_AMOUNT, |         buyTokenFeeAmount: ZERO_AMOUNT, | ||||||
|         sellTokenFeeAmount: ZERO_AMOUNT, |         sellTokenFeeAmount: ZERO_AMOUNT, | ||||||
| @@ -86,9 +92,12 @@ export const INVALID_SIGNATURE = { signatureType: SignatureType.Invalid, v: 1, r | |||||||
|  |  | ||||||
| export { DEFAULT_FEE_SCHEDULE, DEFAULT_GAS_SCHEDULE } from './utils/market_operation_utils/constants'; | export { DEFAULT_FEE_SCHEDULE, DEFAULT_GAS_SCHEDULE } from './utils/market_operation_utils/constants'; | ||||||
|  |  | ||||||
|  | export const POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS = new BigNumber(30000); | ||||||
|  |  | ||||||
| export const constants = { | export const constants = { | ||||||
|     ETH_GAS_STATION_API_URL, |     ETH_GAS_STATION_API_URL, | ||||||
|     PROTOCOL_FEE_MULTIPLIER, |     PROTOCOL_FEE_MULTIPLIER, | ||||||
|  |     POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS, | ||||||
|     NULL_BYTES, |     NULL_BYTES, | ||||||
|     ZERO_AMOUNT, |     ZERO_AMOUNT, | ||||||
|     NULL_ADDRESS, |     NULL_ADDRESS, | ||||||
|   | |||||||
| @@ -74,9 +74,10 @@ export { InsufficientAssetLiquidityError } from './errors'; | |||||||
| export { SwapQuoteConsumer } from './quote_consumers/swap_quote_consumer'; | export { SwapQuoteConsumer } from './quote_consumers/swap_quote_consumer'; | ||||||
| export { SwapQuoter, Orderbook } from './swap_quoter'; | export { SwapQuoter, Orderbook } from './swap_quoter'; | ||||||
| export { | export { | ||||||
|     AffiliateFee, |  | ||||||
|     AltOffering, |     AltOffering, | ||||||
|     AltRfqtMakerAssetOfferings, |     AltRfqtMakerAssetOfferings, | ||||||
|  |     AffiliateFeeType, | ||||||
|  |     AffiliateFeeAmount, | ||||||
|     AssetSwapperContractAddresses, |     AssetSwapperContractAddresses, | ||||||
|     CalldataInfo, |     CalldataInfo, | ||||||
|     ExchangeProxyContractOpts, |     ExchangeProxyContractOpts, | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ import { | |||||||
|     encodeCurveLiquidityProviderData, |     encodeCurveLiquidityProviderData, | ||||||
|     encodeFillQuoteTransformerData, |     encodeFillQuoteTransformerData, | ||||||
|     encodePayTakerTransformerData, |     encodePayTakerTransformerData, | ||||||
|  |     encodePositiveSlippageFeeTransformerData, | ||||||
|     encodeWethTransformerData, |     encodeWethTransformerData, | ||||||
|     ETH_TOKEN_ADDRESS, |     ETH_TOKEN_ADDRESS, | ||||||
|     FillQuoteTransformerData, |     FillQuoteTransformerData, | ||||||
| @@ -16,8 +17,9 @@ import { BigNumber, providerUtils } from '@0x/utils'; | |||||||
| import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper'; | import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper'; | ||||||
| import * as _ from 'lodash'; | import * as _ from 'lodash'; | ||||||
|  |  | ||||||
| import { constants } from '../constants'; | import { constants, POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS } from '../constants'; | ||||||
| import { | import { | ||||||
|  |     AffiliateFeeType, | ||||||
|     CalldataInfo, |     CalldataInfo, | ||||||
|     ExchangeProxyContractOpts, |     ExchangeProxyContractOpts, | ||||||
|     MarketBuySwapQuote, |     MarketBuySwapQuote, | ||||||
| @@ -59,6 +61,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { | |||||||
|         payTakerTransformer: number; |         payTakerTransformer: number; | ||||||
|         fillQuoteTransformer: number; |         fillQuoteTransformer: number; | ||||||
|         affiliateFeeTransformer: number; |         affiliateFeeTransformer: number; | ||||||
|  |         positiveSlippageFeeTransformer: number; | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     private readonly _exchangeProxy: IZeroExContract; |     private readonly _exchangeProxy: IZeroExContract; | ||||||
| @@ -92,6 +95,10 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { | |||||||
|                 contractAddresses.transformers.affiliateFeeTransformer, |                 contractAddresses.transformers.affiliateFeeTransformer, | ||||||
|                 contractAddresses.exchangeProxyTransformerDeployer, |                 contractAddresses.exchangeProxyTransformerDeployer, | ||||||
|             ), |             ), | ||||||
|  |             positiveSlippageFeeTransformer: findTransformerNonce( | ||||||
|  |                 contractAddresses.transformers.positiveSlippageFeeTransformer, | ||||||
|  |                 contractAddresses.exchangeProxyTransformerDeployer, | ||||||
|  |             ), | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -117,7 +124,6 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { | |||||||
|         if (isFromETH) { |         if (isFromETH) { | ||||||
|             ethAmount = ethAmount.plus(sellAmount); |             ethAmount = ethAmount.plus(sellAmount); | ||||||
|         } |         } | ||||||
|         const { buyTokenFeeAmount, sellTokenFeeAmount, recipient: feeRecipient } = affiliateFee; |  | ||||||
|  |  | ||||||
|         // VIP routes. |         // VIP routes. | ||||||
|         if ( |         if ( | ||||||
| @@ -144,7 +150,8 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { | |||||||
|                     .getABIEncodedTransactionData(), |                     .getABIEncodedTransactionData(), | ||||||
|                 ethAmount: isFromETH ? sellAmount : ZERO_AMOUNT, |                 ethAmount: isFromETH ? sellAmount : ZERO_AMOUNT, | ||||||
|                 toAddress: this._exchangeProxy.address, |                 toAddress: this._exchangeProxy.address, | ||||||
|                 allowanceTarget: this.contractAddresses.exchangeProxyAllowanceTarget, |                 allowanceTarget: this._exchangeProxy.address, | ||||||
|  |                 gasOverhead: ZERO_AMOUNT, | ||||||
|             }; |             }; | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -165,7 +172,8 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { | |||||||
|                     .getABIEncodedTransactionData(), |                     .getABIEncodedTransactionData(), | ||||||
|                 ethAmount: isFromETH ? sellAmount : ZERO_AMOUNT, |                 ethAmount: isFromETH ? sellAmount : ZERO_AMOUNT, | ||||||
|                 toAddress: this._exchangeProxy.address, |                 toAddress: this._exchangeProxy.address, | ||||||
|                 allowanceTarget: this.contractAddresses.exchangeProxyAllowanceTarget, |                 allowanceTarget: this._exchangeProxy.address, | ||||||
|  |                 gasOverhead: ZERO_AMOUNT, | ||||||
|             }; |             }; | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -190,7 +198,8 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { | |||||||
|                     .getABIEncodedTransactionData(), |                     .getABIEncodedTransactionData(), | ||||||
|                 ethAmount: isFromETH ? sellAmount : ZERO_AMOUNT, |                 ethAmount: isFromETH ? sellAmount : ZERO_AMOUNT, | ||||||
|                 toAddress: this._exchangeProxy.address, |                 toAddress: this._exchangeProxy.address, | ||||||
|                 allowanceTarget: this.contractAddresses.exchangeProxyAllowanceTarget, |                 allowanceTarget: this._exchangeProxy.address, | ||||||
|  |                 gasOverhead: ZERO_AMOUNT, | ||||||
|             }; |             }; | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -262,25 +271,52 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { | |||||||
|             }); |             }); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // This transformer pays affiliate fees. |         const { feeType, buyTokenFeeAmount, sellTokenFeeAmount, recipient: feeRecipient } = affiliateFee; | ||||||
|         if (buyTokenFeeAmount.isGreaterThan(0) && feeRecipient !== NULL_ADDRESS) { |         let gasOverhead = ZERO_AMOUNT; | ||||||
|  |         if (feeType === AffiliateFeeType.PositiveSlippageFee && feeRecipient !== NULL_ADDRESS) { | ||||||
|  |             // bestCaseAmountWithSurplus is used to cover gas cost of sending positive slipapge fee to fee recipient | ||||||
|  |             // this helps avoid sending dust amounts which are not worth the gas cost to transfer | ||||||
|  |             let bestCaseAmountWithSurplus = quote.bestCaseQuoteInfo.makerAmount | ||||||
|  |                 .plus( | ||||||
|  |                     POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS.multipliedBy(quote.gasPrice).multipliedBy( | ||||||
|  |                         quote.makerAmountPerEth, | ||||||
|  |                     ), | ||||||
|  |                 ) | ||||||
|  |                 .integerValue(); | ||||||
|  |             // In the event makerAmountPerEth is unknown, we only allow for positive slippage which is greater than | ||||||
|  |             // the best case amount | ||||||
|  |             bestCaseAmountWithSurplus = BigNumber.max(bestCaseAmountWithSurplus, quote.bestCaseQuoteInfo.makerAmount); | ||||||
|             transforms.push({ |             transforms.push({ | ||||||
|                 deploymentNonce: this.transformerNonces.affiliateFeeTransformer, |                 deploymentNonce: this.transformerNonces.positiveSlippageFeeTransformer, | ||||||
|                 data: encodeAffiliateFeeTransformerData({ |                 data: encodePositiveSlippageFeeTransformerData({ | ||||||
|                     fees: [ |                     token: isToETH ? ETH_TOKEN_ADDRESS : buyToken, | ||||||
|                         { |                     bestCaseAmount: BigNumber.max(bestCaseAmountWithSurplus, quote.bestCaseQuoteInfo.makerAmount), | ||||||
|                             token: isToETH ? ETH_TOKEN_ADDRESS : buyToken, |                     recipient: feeRecipient, | ||||||
|                             amount: buyTokenFeeAmount, |  | ||||||
|                             recipient: feeRecipient, |  | ||||||
|                         }, |  | ||||||
|                     ], |  | ||||||
|                 }), |                 }), | ||||||
|             }); |             }); | ||||||
|             // Adjust the minimum buy amount by the fee. |             // This may not be visible at eth_estimateGas time, so we explicitly add overhead | ||||||
|             minBuyAmount = BigNumber.max(0, minBuyAmount.minus(buyTokenFeeAmount)); |             gasOverhead = POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS; | ||||||
|         } |         } else if (feeType === AffiliateFeeType.PercentageFee && feeRecipient !== NULL_ADDRESS) { | ||||||
|         if (sellTokenFeeAmount.isGreaterThan(0) && feeRecipient !== NULL_ADDRESS) { |             // This transformer pays affiliate fees. | ||||||
|             throw new Error('Affiliate fees denominated in sell token are not yet supported'); |             if (buyTokenFeeAmount.isGreaterThan(0)) { | ||||||
|  |                 transforms.push({ | ||||||
|  |                     deploymentNonce: this.transformerNonces.affiliateFeeTransformer, | ||||||
|  |                     data: encodeAffiliateFeeTransformerData({ | ||||||
|  |                         fees: [ | ||||||
|  |                             { | ||||||
|  |                                 token: isToETH ? ETH_TOKEN_ADDRESS : buyToken, | ||||||
|  |                                 amount: buyTokenFeeAmount, | ||||||
|  |                                 recipient: feeRecipient, | ||||||
|  |                             }, | ||||||
|  |                         ], | ||||||
|  |                     }), | ||||||
|  |                 }); | ||||||
|  |                 // Adjust the minimum buy amount by the fee. | ||||||
|  |                 minBuyAmount = BigNumber.max(0, minBuyAmount.minus(buyTokenFeeAmount)); | ||||||
|  |             } | ||||||
|  |             if (sellTokenFeeAmount.isGreaterThan(0)) { | ||||||
|  |                 throw new Error('Affiliate fees denominated in sell token are not yet supported'); | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // The final transformer will send all funds to the taker. |         // The final transformer will send all funds to the taker. | ||||||
| @@ -306,7 +342,8 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { | |||||||
|             calldataHexString, |             calldataHexString, | ||||||
|             ethAmount, |             ethAmount, | ||||||
|             toAddress: this._exchangeProxy.address, |             toAddress: this._exchangeProxy.address, | ||||||
|             allowanceTarget: this.contractAddresses.exchangeProxyAllowanceTarget, |             allowanceTarget: this._exchangeProxy.address, | ||||||
|  |             gasOverhead, | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -332,6 +369,10 @@ function isDirectSwapCompatible( | |||||||
|     if (!opts.affiliateFee.buyTokenFeeAmount.eq(0) || !opts.affiliateFee.sellTokenFeeAmount.eq(0)) { |     if (!opts.affiliateFee.buyTokenFeeAmount.eq(0) || !opts.affiliateFee.sellTokenFeeAmount.eq(0)) { | ||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
|  |     // Must not have a positive slippage fee. | ||||||
|  |     if (opts.affiliateFee.feeType === AffiliateFeeType.PositiveSlippageFee) { | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|     // Must be a single order. |     // Must be a single order. | ||||||
|     if (quote.orders.length !== 1) { |     if (quote.orders.length !== 1) { | ||||||
|         return false; |         return false; | ||||||
|   | |||||||
| @@ -454,7 +454,7 @@ function createSwapQuote( | |||||||
|     gasSchedule: FeeSchedule, |     gasSchedule: FeeSchedule, | ||||||
|     slippage: number, |     slippage: number, | ||||||
| ): SwapQuote { | ): SwapQuote { | ||||||
|     const { optimizedOrders, quoteReport, sourceFlags, takerTokenToEthRate, makerTokenToEthRate } = optimizerResult; |     const { optimizedOrders, quoteReport, sourceFlags, takerAmountPerEth, makerAmountPerEth } = optimizerResult; | ||||||
|     const isTwoHop = sourceFlags === SOURCE_FLAGS[ERC20BridgeSource.MultiHop]; |     const isTwoHop = sourceFlags === SOURCE_FLAGS[ERC20BridgeSource.MultiHop]; | ||||||
|  |  | ||||||
|     // Calculate quote info |     // Calculate quote info | ||||||
| @@ -474,8 +474,8 @@ function createSwapQuote( | |||||||
|         sourceBreakdown, |         sourceBreakdown, | ||||||
|         makerTokenDecimals, |         makerTokenDecimals, | ||||||
|         takerTokenDecimals, |         takerTokenDecimals, | ||||||
|         takerTokenToEthRate, |         takerAmountPerEth, | ||||||
|         makerTokenToEthRate, |         makerAmountPerEth, | ||||||
|         quoteReport, |         quoteReport, | ||||||
|         isTwoHop, |         isTwoHop, | ||||||
|     }; |     }; | ||||||
|   | |||||||
| @@ -54,12 +54,15 @@ export interface NativeOrderFillableAmountFields { | |||||||
|  * toAddress: The contract address to call. |  * toAddress: The contract address to call. | ||||||
|  * ethAmount: The eth amount in wei to send with the smart contract call. |  * ethAmount: The eth amount in wei to send with the smart contract call. | ||||||
|  * allowanceTarget: The address the taker should grant an allowance to. |  * allowanceTarget: The address the taker should grant an allowance to. | ||||||
|  |  * gasOverhead: The gas overhead needed to be added to the gas limit to allow for optional | ||||||
|  |  * operations which may not visible at eth_estimateGas time | ||||||
|  */ |  */ | ||||||
| export interface CalldataInfo { | export interface CalldataInfo { | ||||||
|     calldataHexString: string; |     calldataHexString: string; | ||||||
|     toAddress: string; |     toAddress: string; | ||||||
|     ethAmount: BigNumber; |     ethAmount: BigNumber; | ||||||
|     allowanceTarget: string; |     allowanceTarget: string; | ||||||
|  |     gasOverhead: BigNumber; | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -98,7 +101,14 @@ export interface SwapQuoteExecutionOpts extends SwapQuoteGetOutputOpts { | |||||||
|     gasLimit?: number; |     gasLimit?: number; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface AffiliateFee { | export enum AffiliateFeeType { | ||||||
|  |     None, | ||||||
|  |     PercentageFee, | ||||||
|  |     PositiveSlippageFee, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface AffiliateFeeAmount { | ||||||
|  |     feeType: AffiliateFeeType; | ||||||
|     recipient: string; |     recipient: string; | ||||||
|     buyTokenFeeAmount: BigNumber; |     buyTokenFeeAmount: BigNumber; | ||||||
|     sellTokenFeeAmount: BigNumber; |     sellTokenFeeAmount: BigNumber; | ||||||
| @@ -130,7 +140,7 @@ export enum ExchangeProxyRefundReceiver { | |||||||
| export interface ExchangeProxyContractOpts { | export interface ExchangeProxyContractOpts { | ||||||
|     isFromETH: boolean; |     isFromETH: boolean; | ||||||
|     isToETH: boolean; |     isToETH: boolean; | ||||||
|     affiliateFee: AffiliateFee; |     affiliateFee: AffiliateFeeAmount; | ||||||
|     refundReceiver: string | ExchangeProxyRefundReceiver; |     refundReceiver: string | ExchangeProxyRefundReceiver; | ||||||
|     isMetaTransaction: boolean; |     isMetaTransaction: boolean; | ||||||
|     shouldSellEntireBalance: boolean; |     shouldSellEntireBalance: boolean; | ||||||
| @@ -161,8 +171,8 @@ export interface SwapQuoteBase { | |||||||
|     isTwoHop: boolean; |     isTwoHop: boolean; | ||||||
|     makerTokenDecimals: number; |     makerTokenDecimals: number; | ||||||
|     takerTokenDecimals: number; |     takerTokenDecimals: number; | ||||||
|     takerTokenToEthRate: BigNumber; |     takerAmountPerEth: BigNumber; | ||||||
|     makerTokenToEthRate: BigNumber; |     makerAmountPerEth: BigNumber; | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|   | |||||||
| @@ -60,10 +60,10 @@ export function getComparisonPrices( | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Calc native order fee penalty in output unit (maker units for sells, taker unit for buys) |     // Calc native order fee penalty in output unit (maker units for sells, taker unit for buys) | ||||||
|     const feePenalty = !marketSideLiquidity.ethToOutputRate.isZero() |     const feePenalty = !marketSideLiquidity.outputAmountPerEth.isZero() | ||||||
|         ? marketSideLiquidity.ethToOutputRate.times(feeInEth) |         ? marketSideLiquidity.outputAmountPerEth.times(feeInEth) | ||||||
|         : // if it's a sell, the input token is the taker token |         : // if it's a sell, the input token is the taker token | ||||||
|           marketSideLiquidity.ethToInputRate |           marketSideLiquidity.inputAmountPerEth | ||||||
|               .times(feeInEth) |               .times(feeInEth) | ||||||
|               .times(marketSideLiquidity.side === MarketOperation.Sell ? adjustedRate : adjustedRate.pow(-1)); |               .times(marketSideLiquidity.side === MarketOperation.Sell ? adjustedRate : adjustedRate.pow(-1)); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -645,6 +645,8 @@ export const DEFAULT_GAS_SCHEDULE: Required<FeeSchedule> = { | |||||||
|  |  | ||||||
| export const DEFAULT_FEE_SCHEDULE: Required<FeeSchedule> = { ...DEFAULT_GAS_SCHEDULE }; | export const DEFAULT_FEE_SCHEDULE: Required<FeeSchedule> = { ...DEFAULT_GAS_SCHEDULE }; | ||||||
|  |  | ||||||
|  | export const POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS = new BigNumber(20000); | ||||||
|  |  | ||||||
| // tslint:enable:custom-no-magic-numbers | // tslint:enable:custom-no-magic-numbers | ||||||
|  |  | ||||||
| export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = { | export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = { | ||||||
|   | |||||||
| @@ -16,8 +16,8 @@ export function createFills(opts: { | |||||||
|     orders?: NativeOrderWithFillableAmounts[]; |     orders?: NativeOrderWithFillableAmounts[]; | ||||||
|     dexQuotes?: DexSample[][]; |     dexQuotes?: DexSample[][]; | ||||||
|     targetInput?: BigNumber; |     targetInput?: BigNumber; | ||||||
|     ethToOutputRate?: BigNumber; |     outputAmountPerEth?: BigNumber; | ||||||
|     ethToInputRate?: BigNumber; |     inputAmountPerEth?: BigNumber; | ||||||
|     excludedSources?: ERC20BridgeSource[]; |     excludedSources?: ERC20BridgeSource[]; | ||||||
|     feeSchedule?: FeeSchedule; |     feeSchedule?: FeeSchedule; | ||||||
| }): Fill[][] { | }): Fill[][] { | ||||||
| @@ -26,20 +26,20 @@ export function createFills(opts: { | |||||||
|     const feeSchedule = opts.feeSchedule || {}; |     const feeSchedule = opts.feeSchedule || {}; | ||||||
|     const orders = opts.orders || []; |     const orders = opts.orders || []; | ||||||
|     const dexQuotes = opts.dexQuotes || []; |     const dexQuotes = opts.dexQuotes || []; | ||||||
|     const ethToOutputRate = opts.ethToOutputRate || ZERO_AMOUNT; |     const outputAmountPerEth = opts.outputAmountPerEth || ZERO_AMOUNT; | ||||||
|     const ethToInputRate = opts.ethToInputRate || ZERO_AMOUNT; |     const inputAmountPerEth = opts.inputAmountPerEth || ZERO_AMOUNT; | ||||||
|     // Create native fills. |     // Create native fills. | ||||||
|     const nativeFills = nativeOrdersToFills( |     const nativeFills = nativeOrdersToFills( | ||||||
|         side, |         side, | ||||||
|         orders.filter(o => o.fillableTakerAmount.isGreaterThan(0)), |         orders.filter(o => o.fillableTakerAmount.isGreaterThan(0)), | ||||||
|         opts.targetInput, |         opts.targetInput, | ||||||
|         ethToOutputRate, |         outputAmountPerEth, | ||||||
|         ethToInputRate, |         inputAmountPerEth, | ||||||
|         feeSchedule, |         feeSchedule, | ||||||
|     ); |     ); | ||||||
|     // Create DEX fills. |     // Create DEX fills. | ||||||
|     const dexFills = dexQuotes.map(singleSourceSamples => |     const dexFills = dexQuotes.map(singleSourceSamples => | ||||||
|         dexSamplesToFills(side, singleSourceSamples, ethToOutputRate, ethToInputRate, feeSchedule), |         dexSamplesToFills(side, singleSourceSamples, outputAmountPerEth, inputAmountPerEth, feeSchedule), | ||||||
|     ); |     ); | ||||||
|     return [...dexFills, nativeFills] |     return [...dexFills, nativeFills] | ||||||
|         .map(p => clipFillsToInput(p, opts.targetInput)) |         .map(p => clipFillsToInput(p, opts.targetInput)) | ||||||
| @@ -75,8 +75,8 @@ function nativeOrdersToFills( | |||||||
|     side: MarketOperation, |     side: MarketOperation, | ||||||
|     orders: NativeOrderWithFillableAmounts[], |     orders: NativeOrderWithFillableAmounts[], | ||||||
|     targetInput: BigNumber = POSITIVE_INF, |     targetInput: BigNumber = POSITIVE_INF, | ||||||
|     ethToOutputRate: BigNumber, |     outputAmountPerEth: BigNumber, | ||||||
|     ethToInputRate: BigNumber, |     inputAmountPerEth: BigNumber, | ||||||
|     fees: FeeSchedule, |     fees: FeeSchedule, | ||||||
| ): Fill[] { | ): Fill[] { | ||||||
|     const sourcePathId = hexUtils.random(); |     const sourcePathId = hexUtils.random(); | ||||||
| @@ -89,9 +89,9 @@ function nativeOrdersToFills( | |||||||
|         const input = side === MarketOperation.Sell ? takerAmount : makerAmount; |         const input = side === MarketOperation.Sell ? takerAmount : makerAmount; | ||||||
|         const output = side === MarketOperation.Sell ? makerAmount : takerAmount; |         const output = side === MarketOperation.Sell ? makerAmount : takerAmount; | ||||||
|         const fee = fees[ERC20BridgeSource.Native] === undefined ? 0 : fees[ERC20BridgeSource.Native]!(o); |         const fee = fees[ERC20BridgeSource.Native] === undefined ? 0 : fees[ERC20BridgeSource.Native]!(o); | ||||||
|         const outputPenalty = !ethToOutputRate.isZero() |         const outputPenalty = !outputAmountPerEth.isZero() | ||||||
|             ? ethToOutputRate.times(fee) |             ? outputAmountPerEth.times(fee) | ||||||
|             : ethToInputRate.times(fee).times(output.dividedToIntegerBy(input)); |             : inputAmountPerEth.times(fee).times(output.dividedToIntegerBy(input)); | ||||||
|         // targetInput can be less than the order size |         // targetInput can be less than the order size | ||||||
|         // whilst the penalty is constant, it affects the adjusted output |         // whilst the penalty is constant, it affects the adjusted output | ||||||
|         // only up until the target has been exhausted. |         // only up until the target has been exhausted. | ||||||
| @@ -135,8 +135,8 @@ function nativeOrdersToFills( | |||||||
| function dexSamplesToFills( | function dexSamplesToFills( | ||||||
|     side: MarketOperation, |     side: MarketOperation, | ||||||
|     samples: DexSample[], |     samples: DexSample[], | ||||||
|     ethToOutputRate: BigNumber, |     outputAmountPerEth: BigNumber, | ||||||
|     ethToInputRate: BigNumber, |     inputAmountPerEth: BigNumber, | ||||||
|     fees: FeeSchedule, |     fees: FeeSchedule, | ||||||
| ): Fill[] { | ): Fill[] { | ||||||
|     const sourcePathId = hexUtils.random(); |     const sourcePathId = hexUtils.random(); | ||||||
| @@ -156,9 +156,9 @@ function dexSamplesToFills( | |||||||
|         let penalty = ZERO_AMOUNT; |         let penalty = ZERO_AMOUNT; | ||||||
|         if (i === 0) { |         if (i === 0) { | ||||||
|             // Only the first fill in a DEX path incurs a penalty. |             // Only the first fill in a DEX path incurs a penalty. | ||||||
|             penalty = !ethToOutputRate.isZero() |             penalty = !outputAmountPerEth.isZero() | ||||||
|                 ? ethToOutputRate.times(fee) |                 ? outputAmountPerEth.times(fee) | ||||||
|                 : ethToInputRate.times(fee).times(output.dividedToIntegerBy(input)); |                 : inputAmountPerEth.times(fee).times(output.dividedToIntegerBy(input)); | ||||||
|         } |         } | ||||||
|         const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty); |         const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -30,7 +30,8 @@ import { | |||||||
| import { createFills } from './fills'; | import { createFills } from './fills'; | ||||||
| import { getBestTwoHopQuote } from './multihop_utils'; | import { getBestTwoHopQuote } from './multihop_utils'; | ||||||
| import { createOrdersFromTwoHopSample } from './orders'; | import { createOrdersFromTwoHopSample } from './orders'; | ||||||
| import { findOptimalPathAsync } from './path_optimizer'; | import { PathPenaltyOpts } from './path'; | ||||||
|  | import { fillsToSortedPaths, findOptimalPathAsync } from './path_optimizer'; | ||||||
| import { DexOrderSampler, getSampleAmounts } from './sampler'; | import { DexOrderSampler, getSampleAmounts } from './sampler'; | ||||||
| import { SourceFilters } from './source_filters'; | import { SourceFilters } from './source_filters'; | ||||||
| import { | import { | ||||||
| @@ -167,8 +168,8 @@ export class MarketOperationUtils { | |||||||
|             [ |             [ | ||||||
|                 tokenDecimals, |                 tokenDecimals, | ||||||
|                 orderFillableTakerAmounts, |                 orderFillableTakerAmounts, | ||||||
|                 ethToMakerAssetRate, |                 outputAmountPerEth, | ||||||
|                 ethToTakerAssetRate, |                 inputAmountPerEth, | ||||||
|                 dexQuotes, |                 dexQuotes, | ||||||
|                 rawTwoHopQuotes, |                 rawTwoHopQuotes, | ||||||
|                 isTxOriginContract, |                 isTxOriginContract, | ||||||
| @@ -195,8 +196,8 @@ export class MarketOperationUtils { | |||||||
|             inputAmount: takerAmount, |             inputAmount: takerAmount, | ||||||
|             inputToken: takerToken, |             inputToken: takerToken, | ||||||
|             outputToken: makerToken, |             outputToken: makerToken, | ||||||
|             ethToOutputRate: ethToMakerAssetRate, |             outputAmountPerEth, | ||||||
|             ethToInputRate: ethToTakerAssetRate, |             inputAmountPerEth, | ||||||
|             quoteSourceFilters, |             quoteSourceFilters, | ||||||
|             makerTokenDecimals: makerTokenDecimals.toNumber(), |             makerTokenDecimals: makerTokenDecimals.toNumber(), | ||||||
|             takerTokenDecimals: takerTokenDecimals.toNumber(), |             takerTokenDecimals: takerTokenDecimals.toNumber(), | ||||||
| @@ -321,8 +322,8 @@ export class MarketOperationUtils { | |||||||
|             inputAmount: makerAmount, |             inputAmount: makerAmount, | ||||||
|             inputToken: makerToken, |             inputToken: makerToken, | ||||||
|             outputToken: takerToken, |             outputToken: takerToken, | ||||||
|             ethToOutputRate: ethToTakerAssetRate, |             outputAmountPerEth: ethToTakerAssetRate, | ||||||
|             ethToInputRate: ethToMakerAssetRate, |             inputAmountPerEth: ethToMakerAssetRate, | ||||||
|             quoteSourceFilters, |             quoteSourceFilters, | ||||||
|             makerTokenDecimals: makerTokenDecimals.toNumber(), |             makerTokenDecimals: makerTokenDecimals.toNumber(), | ||||||
|             takerTokenDecimals: takerTokenDecimals.toNumber(), |             takerTokenDecimals: takerTokenDecimals.toNumber(), | ||||||
| @@ -392,7 +393,7 @@ export class MarketOperationUtils { | |||||||
|         const batchEthToTakerAssetRate = executeResults.splice(0, batchNativeOrders.length) as BigNumber[]; |         const batchEthToTakerAssetRate = executeResults.splice(0, batchNativeOrders.length) as BigNumber[]; | ||||||
|         const batchDexQuotes = executeResults.splice(0, batchNativeOrders.length) as DexSample[][][]; |         const batchDexQuotes = executeResults.splice(0, batchNativeOrders.length) as DexSample[][][]; | ||||||
|         const batchTokenDecimals = executeResults.splice(0, batchNativeOrders.length) as number[][]; |         const batchTokenDecimals = executeResults.splice(0, batchNativeOrders.length) as number[][]; | ||||||
|         const ethToInputRate = ZERO_AMOUNT; |         const inputAmountPerEth = ZERO_AMOUNT; | ||||||
|  |  | ||||||
|         return Promise.all( |         return Promise.all( | ||||||
|             batchNativeOrders.map(async (nativeOrders, i) => { |             batchNativeOrders.map(async (nativeOrders, i) => { | ||||||
| @@ -401,7 +402,7 @@ export class MarketOperationUtils { | |||||||
|                 } |                 } | ||||||
|                 const { makerToken, takerToken } = nativeOrders[0].order; |                 const { makerToken, takerToken } = nativeOrders[0].order; | ||||||
|                 const orderFillableMakerAmounts = batchOrderFillableMakerAmounts[i]; |                 const orderFillableMakerAmounts = batchOrderFillableMakerAmounts[i]; | ||||||
|                 const ethToTakerAssetRate = batchEthToTakerAssetRate[i]; |                 const outputAmountPerEth = batchEthToTakerAssetRate[i]; | ||||||
|                 const dexQuotes = batchDexQuotes[i]; |                 const dexQuotes = batchDexQuotes[i]; | ||||||
|                 const makerAmount = makerAmounts[i]; |                 const makerAmount = makerAmounts[i]; | ||||||
|                 try { |                 try { | ||||||
| @@ -411,8 +412,8 @@ export class MarketOperationUtils { | |||||||
|                             inputToken: makerToken, |                             inputToken: makerToken, | ||||||
|                             outputToken: takerToken, |                             outputToken: takerToken, | ||||||
|                             inputAmount: makerAmount, |                             inputAmount: makerAmount, | ||||||
|                             ethToOutputRate: ethToTakerAssetRate, |                             outputAmountPerEth, | ||||||
|                             ethToInputRate, |                             inputAmountPerEth, | ||||||
|                             quoteSourceFilters, |                             quoteSourceFilters, | ||||||
|                             makerTokenDecimals: batchTokenDecimals[i][0], |                             makerTokenDecimals: batchTokenDecimals[i][0], | ||||||
|                             takerTokenDecimals: batchTokenDecimals[i][1], |                             takerTokenDecimals: batchTokenDecimals[i][1], | ||||||
| @@ -455,8 +456,8 @@ export class MarketOperationUtils { | |||||||
|             side, |             side, | ||||||
|             inputAmount, |             inputAmount, | ||||||
|             quotes, |             quotes, | ||||||
|             ethToOutputRate, |             outputAmountPerEth, | ||||||
|             ethToInputRate, |             inputAmountPerEth, | ||||||
|         } = marketSideLiquidity; |         } = marketSideLiquidity; | ||||||
|         const { nativeOrders, rfqtIndicativeQuotes, dexQuotes } = quotes; |         const { nativeOrders, rfqtIndicativeQuotes, dexQuotes } = quotes; | ||||||
|         const maxFallbackSlippage = opts.maxFallbackSlippage || 0; |         const maxFallbackSlippage = opts.maxFallbackSlippage || 0; | ||||||
| @@ -489,25 +490,29 @@ export class MarketOperationUtils { | |||||||
|             orders: [...nativeOrders, ...augmentedRfqtIndicativeQuotes], |             orders: [...nativeOrders, ...augmentedRfqtIndicativeQuotes], | ||||||
|             dexQuotes, |             dexQuotes, | ||||||
|             targetInput: inputAmount, |             targetInput: inputAmount, | ||||||
|             ethToOutputRate, |             outputAmountPerEth, | ||||||
|             ethToInputRate, |             inputAmountPerEth, | ||||||
|             excludedSources: opts.excludedSources, |             excludedSources: opts.excludedSources, | ||||||
|             feeSchedule: opts.feeSchedule, |             feeSchedule: opts.feeSchedule, | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         // Find the optimal path. |         // Find the optimal path. | ||||||
|         const optimizerOpts = { |         const penaltyOpts: PathPenaltyOpts = { | ||||||
|             ethToOutputRate, |             outputAmountPerEth, | ||||||
|             ethToInputRate, |             inputAmountPerEth, | ||||||
|             exchangeProxyOverhead: opts.exchangeProxyOverhead || (() => ZERO_AMOUNT), |             exchangeProxyOverhead: opts.exchangeProxyOverhead || (() => ZERO_AMOUNT), | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         // NOTE: For sell quotes input is the taker asset and for buy quotes input is the maker asset |         // NOTE: For sell quotes input is the taker asset and for buy quotes input is the maker asset | ||||||
|         const takerTokenToEthRate = side === MarketOperation.Sell ? ethToInputRate : ethToOutputRate; |         const takerAmountPerEth = side === MarketOperation.Sell ? inputAmountPerEth : outputAmountPerEth; | ||||||
|         const makerTokenToEthRate = side === MarketOperation.Sell ? ethToOutputRate : ethToInputRate; |         const makerAmountPerEth = side === MarketOperation.Sell ? outputAmountPerEth : inputAmountPerEth; | ||||||
|  |  | ||||||
|  |         // Find the unoptimized best rate to calculate savings from optimizer | ||||||
|  |         const _unoptimizedPath = fillsToSortedPaths(fills, side, inputAmount, penaltyOpts)[0]; | ||||||
|  |         const unoptimizedPath = _unoptimizedPath ? _unoptimizedPath.collapse(orderOpts) : undefined; | ||||||
|  |  | ||||||
|         // Find the optimal path |         // Find the optimal path | ||||||
|         const optimalPath = await findOptimalPathAsync(side, fills, inputAmount, opts.runLimit, optimizerOpts); |         const optimalPath = await findOptimalPathAsync(side, fills, inputAmount, opts.runLimit, penaltyOpts); | ||||||
|         const optimalPathRate = optimalPath ? optimalPath.adjustedRate() : ZERO_AMOUNT; |         const optimalPathRate = optimalPath ? optimalPath.adjustedRate() : ZERO_AMOUNT; | ||||||
|  |  | ||||||
|         const { adjustedRate: bestTwoHopRate, quote: bestTwoHopQuote } = getBestTwoHopQuote( |         const { adjustedRate: bestTwoHopRate, quote: bestTwoHopQuote } = getBestTwoHopQuote( | ||||||
| @@ -523,8 +528,9 @@ export class MarketOperationUtils { | |||||||
|                 sourceFlags: SOURCE_FLAGS[ERC20BridgeSource.MultiHop], |                 sourceFlags: SOURCE_FLAGS[ERC20BridgeSource.MultiHop], | ||||||
|                 marketSideLiquidity, |                 marketSideLiquidity, | ||||||
|                 adjustedRate: bestTwoHopRate, |                 adjustedRate: bestTwoHopRate, | ||||||
|                 takerTokenToEthRate, |                 unoptimizedPath, | ||||||
|                 makerTokenToEthRate, |                 takerAmountPerEth, | ||||||
|  |                 makerAmountPerEth, | ||||||
|             }; |             }; | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -557,8 +563,9 @@ export class MarketOperationUtils { | |||||||
|             sourceFlags: collapsedPath.sourceFlags, |             sourceFlags: collapsedPath.sourceFlags, | ||||||
|             marketSideLiquidity, |             marketSideLiquidity, | ||||||
|             adjustedRate: optimalPathRate, |             adjustedRate: optimalPathRate, | ||||||
|             takerTokenToEthRate, |             unoptimizedPath, | ||||||
|             makerTokenToEthRate, |             takerAmountPerEth, | ||||||
|  |             makerAmountPerEth, | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -39,7 +39,7 @@ export function getBestTwoHopQuote( | |||||||
|     feeSchedule?: FeeSchedule, |     feeSchedule?: FeeSchedule, | ||||||
|     exchangeProxyOverhead?: ExchangeProxyOverhead, |     exchangeProxyOverhead?: ExchangeProxyOverhead, | ||||||
| ): { quote: DexSample<MultiHopFillData> | undefined; adjustedRate: BigNumber } { | ): { quote: DexSample<MultiHopFillData> | undefined; adjustedRate: BigNumber } { | ||||||
|     const { side, inputAmount, ethToOutputRate, quotes } = marketSideLiquidity; |     const { side, inputAmount, outputAmountPerEth, quotes } = marketSideLiquidity; | ||||||
|     const { twoHopQuotes } = quotes; |     const { twoHopQuotes } = quotes; | ||||||
|     // Ensure the expected data we require exists. In the case where all hops reverted |     // Ensure the expected data we require exists. In the case where all hops reverted | ||||||
|     // or there were no sources included that allowed for multi hop, |     // or there were no sources included that allowed for multi hop, | ||||||
| @@ -57,7 +57,7 @@ export function getBestTwoHopQuote( | |||||||
|     } |     } | ||||||
|     const best = filteredQuotes |     const best = filteredQuotes | ||||||
|         .map(quote => |         .map(quote => | ||||||
|             getTwoHopAdjustedRate(side, quote, inputAmount, ethToOutputRate, feeSchedule, exchangeProxyOverhead), |             getTwoHopAdjustedRate(side, quote, inputAmount, outputAmountPerEth, feeSchedule, exchangeProxyOverhead), | ||||||
|         ) |         ) | ||||||
|         .reduce( |         .reduce( | ||||||
|             (prev, curr, i) => |             (prev, curr, i) => | ||||||
| @@ -67,7 +67,7 @@ export function getBestTwoHopQuote( | |||||||
|                     side, |                     side, | ||||||
|                     filteredQuotes[0], |                     filteredQuotes[0], | ||||||
|                     inputAmount, |                     inputAmount, | ||||||
|                     ethToOutputRate, |                     outputAmountPerEth, | ||||||
|                     feeSchedule, |                     feeSchedule, | ||||||
|                     exchangeProxyOverhead, |                     exchangeProxyOverhead, | ||||||
|                 ), |                 ), | ||||||
|   | |||||||
| @@ -22,14 +22,14 @@ export interface PathSize { | |||||||
| } | } | ||||||
|  |  | ||||||
| export interface PathPenaltyOpts { | export interface PathPenaltyOpts { | ||||||
|     ethToOutputRate: BigNumber; |     outputAmountPerEth: BigNumber; | ||||||
|     ethToInputRate: BigNumber; |     inputAmountPerEth: BigNumber; | ||||||
|     exchangeProxyOverhead: ExchangeProxyOverhead; |     exchangeProxyOverhead: ExchangeProxyOverhead; | ||||||
| } | } | ||||||
|  |  | ||||||
| export const DEFAULT_PATH_PENALTY_OPTS: PathPenaltyOpts = { | export const DEFAULT_PATH_PENALTY_OPTS: PathPenaltyOpts = { | ||||||
|     ethToOutputRate: ZERO_AMOUNT, |     outputAmountPerEth: ZERO_AMOUNT, | ||||||
|     ethToInputRate: ZERO_AMOUNT, |     inputAmountPerEth: ZERO_AMOUNT, | ||||||
|     exchangeProxyOverhead: () => ZERO_AMOUNT, |     exchangeProxyOverhead: () => ZERO_AMOUNT, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -131,11 +131,11 @@ export class Path { | |||||||
|  |  | ||||||
|     public adjustedSize(): PathSize { |     public adjustedSize(): PathSize { | ||||||
|         const { input, output } = this._adjustedSize; |         const { input, output } = this._adjustedSize; | ||||||
|         const { exchangeProxyOverhead, ethToOutputRate, ethToInputRate } = this.pathPenaltyOpts; |         const { exchangeProxyOverhead, outputAmountPerEth, inputAmountPerEth } = this.pathPenaltyOpts; | ||||||
|         const gasOverhead = exchangeProxyOverhead(this.sourceFlags); |         const gasOverhead = exchangeProxyOverhead(this.sourceFlags); | ||||||
|         const pathPenalty = !ethToOutputRate.isZero() |         const pathPenalty = !outputAmountPerEth.isZero() | ||||||
|             ? ethToOutputRate.times(gasOverhead) |             ? outputAmountPerEth.times(gasOverhead) | ||||||
|             : ethToInputRate.times(gasOverhead).times(output.dividedToIntegerBy(input)); |             : inputAmountPerEth.times(gasOverhead).times(output.dividedToIntegerBy(input)); | ||||||
|         return { |         return { | ||||||
|             input, |             input, | ||||||
|             output: this.side === MarketOperation.Sell ? output.minus(pathPenalty) : output.plus(pathPenalty), |             output: this.side === MarketOperation.Sell ? output.minus(pathPenalty) : output.plus(pathPenalty), | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ export function getTwoHopAdjustedRate( | |||||||
|     side: MarketOperation, |     side: MarketOperation, | ||||||
|     twoHopQuote: DexSample<MultiHopFillData>, |     twoHopQuote: DexSample<MultiHopFillData>, | ||||||
|     targetInput: BigNumber, |     targetInput: BigNumber, | ||||||
|     ethToOutputRate: BigNumber, |     outputAmountPerEth: BigNumber, | ||||||
|     fees: FeeSchedule = {}, |     fees: FeeSchedule = {}, | ||||||
|     exchangeProxyOverhead: ExchangeProxyOverhead = () => ZERO_AMOUNT, |     exchangeProxyOverhead: ExchangeProxyOverhead = () => ZERO_AMOUNT, | ||||||
| ): BigNumber { | ): BigNumber { | ||||||
| @@ -21,7 +21,7 @@ export function getTwoHopAdjustedRate( | |||||||
|     if (input.isLessThan(targetInput) || output.isZero()) { |     if (input.isLessThan(targetInput) || output.isZero()) { | ||||||
|         return ZERO_AMOUNT; |         return ZERO_AMOUNT; | ||||||
|     } |     } | ||||||
|     const penalty = ethToOutputRate.times( |     const penalty = outputAmountPerEth.times( | ||||||
|         exchangeProxyOverhead(SOURCE_FLAGS.MultiHop).plus(fees[ERC20BridgeSource.MultiHop]!(fillData)), |         exchangeProxyOverhead(SOURCE_FLAGS.MultiHop).plus(fees[ERC20BridgeSource.MultiHop]!(fillData)), | ||||||
|     ); |     ); | ||||||
|     const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty); |     const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty); | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ import { NativeOrderWithFillableAmounts, RfqtFirmQuoteValidator, RfqtRequestOpts | |||||||
| import { QuoteRequestor } from '../../utils/quote_requestor'; | import { QuoteRequestor } from '../../utils/quote_requestor'; | ||||||
| import { QuoteReport } from '../quote_report_generator'; | import { QuoteReport } from '../quote_report_generator'; | ||||||
|  |  | ||||||
|  | import { CollapsedPath } from './path'; | ||||||
| import { SourceFilters } from './source_filters'; | import { SourceFilters } from './source_filters'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -374,8 +375,9 @@ export interface OptimizerResult { | |||||||
|     liquidityDelivered: CollapsedFill[] | DexSample<MultiHopFillData>; |     liquidityDelivered: CollapsedFill[] | DexSample<MultiHopFillData>; | ||||||
|     marketSideLiquidity: MarketSideLiquidity; |     marketSideLiquidity: MarketSideLiquidity; | ||||||
|     adjustedRate: BigNumber; |     adjustedRate: BigNumber; | ||||||
|     takerTokenToEthRate: BigNumber; |     unoptimizedPath?: CollapsedPath; | ||||||
|     makerTokenToEthRate: BigNumber; |     takerAmountPerEth: BigNumber; | ||||||
|  |     makerAmountPerEth: BigNumber; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface OptimizerResultWithReport extends OptimizerResult { | export interface OptimizerResultWithReport extends OptimizerResult { | ||||||
| @@ -396,8 +398,8 @@ export interface MarketSideLiquidity { | |||||||
|     inputAmount: BigNumber; |     inputAmount: BigNumber; | ||||||
|     inputToken: string; |     inputToken: string; | ||||||
|     outputToken: string; |     outputToken: string; | ||||||
|     ethToOutputRate: BigNumber; |     outputAmountPerEth: BigNumber; | ||||||
|     ethToInputRate: BigNumber; |     inputAmountPerEth: BigNumber; | ||||||
|     quoteSourceFilters: SourceFilters; |     quoteSourceFilters: SourceFilters; | ||||||
|     makerTokenDecimals: number; |     makerTokenDecimals: number; | ||||||
|     takerTokenDecimals: number; |     takerTokenDecimals: number; | ||||||
|   | |||||||
| @@ -49,8 +49,8 @@ const exchangeProxyOverhead = (sourceFlags: number) => { | |||||||
|  |  | ||||||
| const buyMarketSideLiquidity: MarketSideLiquidity = { | const buyMarketSideLiquidity: MarketSideLiquidity = { | ||||||
|     // needed params |     // needed params | ||||||
|     ethToOutputRate: new BigNumber(500), |     outputAmountPerEth: new BigNumber(500), | ||||||
|     ethToInputRate: new BigNumber(1), |     inputAmountPerEth: new BigNumber(1), | ||||||
|     side: MarketOperation.Buy, |     side: MarketOperation.Buy, | ||||||
|     makerTokenDecimals: 18, |     makerTokenDecimals: 18, | ||||||
|     takerTokenDecimals: 18, |     takerTokenDecimals: 18, | ||||||
| @@ -70,8 +70,8 @@ const buyMarketSideLiquidity: MarketSideLiquidity = { | |||||||
|  |  | ||||||
| const sellMarketSideLiquidity: MarketSideLiquidity = { | const sellMarketSideLiquidity: MarketSideLiquidity = { | ||||||
|     // needed params |     // needed params | ||||||
|     ethToOutputRate: new BigNumber(500), |     outputAmountPerEth: new BigNumber(500), | ||||||
|     ethToInputRate: new BigNumber(1), |     inputAmountPerEth: new BigNumber(1), | ||||||
|     side: MarketOperation.Sell, |     side: MarketOperation.Sell, | ||||||
|     makerTokenDecimals: 18, |     makerTokenDecimals: 18, | ||||||
|     takerTokenDecimals: 18, |     takerTokenDecimals: 18, | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import { | |||||||
|     decodeAffiliateFeeTransformerData, |     decodeAffiliateFeeTransformerData, | ||||||
|     decodeFillQuoteTransformerData, |     decodeFillQuoteTransformerData, | ||||||
|     decodePayTakerTransformerData, |     decodePayTakerTransformerData, | ||||||
|  |     decodePositiveSlippageFeeTransformerData, | ||||||
|     decodeWethTransformerData, |     decodeWethTransformerData, | ||||||
|     ETH_TOKEN_ADDRESS, |     ETH_TOKEN_ADDRESS, | ||||||
|     FillQuoteTransformerLimitOrderInfo, |     FillQuoteTransformerLimitOrderInfo, | ||||||
| @@ -17,9 +18,9 @@ import * as chai from 'chai'; | |||||||
| import * as _ from 'lodash'; | import * as _ from 'lodash'; | ||||||
| import 'mocha'; | import 'mocha'; | ||||||
|  |  | ||||||
| import { constants } from '../src/constants'; | import { constants, POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS } from '../src/constants'; | ||||||
| import { ExchangeProxySwapQuoteConsumer } from '../src/quote_consumers/exchange_proxy_swap_quote_consumer'; | import { ExchangeProxySwapQuoteConsumer } from '../src/quote_consumers/exchange_proxy_swap_quote_consumer'; | ||||||
| import { MarketBuySwapQuote, MarketOperation, MarketSellSwapQuote } from '../src/types'; | import { AffiliateFeeType, MarketBuySwapQuote, MarketOperation, MarketSellSwapQuote } from '../src/types'; | ||||||
| import { | import { | ||||||
|     ERC20BridgeSource, |     ERC20BridgeSource, | ||||||
|     OptimizedLimitOrder, |     OptimizedLimitOrder, | ||||||
| @@ -53,6 +54,7 @@ describe('ExchangeProxySwapQuoteConsumer', () => { | |||||||
|             payTakerTransformer: getTransformerAddress(TRANSFORMER_DEPLOYER, 2), |             payTakerTransformer: getTransformerAddress(TRANSFORMER_DEPLOYER, 2), | ||||||
|             fillQuoteTransformer: getTransformerAddress(TRANSFORMER_DEPLOYER, 3), |             fillQuoteTransformer: getTransformerAddress(TRANSFORMER_DEPLOYER, 3), | ||||||
|             affiliateFeeTransformer: getTransformerAddress(TRANSFORMER_DEPLOYER, 4), |             affiliateFeeTransformer: getTransformerAddress(TRANSFORMER_DEPLOYER, 4), | ||||||
|  |             positiveSlippageFeeTransformer: getTransformerAddress(TRANSFORMER_DEPLOYER, 5), | ||||||
|         }, |         }, | ||||||
|     }; |     }; | ||||||
|     let consumer: ExchangeProxySwapQuoteConsumer; |     let consumer: ExchangeProxySwapQuoteConsumer; | ||||||
| @@ -137,11 +139,11 @@ describe('ExchangeProxySwapQuoteConsumer', () => { | |||||||
|                 protocolFeeInWeiAmount: getRandomAmount(), |                 protocolFeeInWeiAmount: getRandomAmount(), | ||||||
|                 feeTakerTokenAmount: getRandomAmount(), |                 feeTakerTokenAmount: getRandomAmount(), | ||||||
|             }, |             }, | ||||||
|  |             makerAmountPerEth: getRandomInteger(1, 1e9), | ||||||
|  |             takerAmountPerEth: getRandomInteger(1, 1e9), | ||||||
|             ...(side === MarketOperation.Buy |             ...(side === MarketOperation.Buy | ||||||
|                 ? { type: MarketOperation.Buy, makerTokenFillAmount } |                 ? { type: MarketOperation.Buy, makerTokenFillAmount } | ||||||
|                 : { type: MarketOperation.Sell, takerTokenFillAmount }), |                 : { type: MarketOperation.Sell, takerTokenFillAmount }), | ||||||
|             takerTokenToEthRate: getRandomAmount(), |  | ||||||
|             makerTokenToEthRate: getRandomAmount(), |  | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -336,6 +338,7 @@ describe('ExchangeProxySwapQuoteConsumer', () => { | |||||||
|                 recipient: randomAddress(), |                 recipient: randomAddress(), | ||||||
|                 buyTokenFeeAmount: getRandomAmount(), |                 buyTokenFeeAmount: getRandomAmount(), | ||||||
|                 sellTokenFeeAmount: ZERO_AMOUNT, |                 sellTokenFeeAmount: ZERO_AMOUNT, | ||||||
|  |                 feeType: AffiliateFeeType.PercentageFee, | ||||||
|             }; |             }; | ||||||
|             const callInfo = await consumer.getCalldataOrThrowAsync(quote, { |             const callInfo = await consumer.getCalldataOrThrowAsync(quote, { | ||||||
|                 extensionContractOpts: { affiliateFee }, |                 extensionContractOpts: { affiliateFee }, | ||||||
| @@ -349,12 +352,42 @@ describe('ExchangeProxySwapQuoteConsumer', () => { | |||||||
|                 { token: MAKER_TOKEN, amount: affiliateFee.buyTokenFeeAmount, recipient: affiliateFee.recipient }, |                 { token: MAKER_TOKEN, amount: affiliateFee.buyTokenFeeAmount, recipient: affiliateFee.recipient }, | ||||||
|             ]); |             ]); | ||||||
|         }); |         }); | ||||||
|  |         it('Appends a positive slippage affiliate fee transformer after the fill if the positive slippage fee feeType is specified', async () => { | ||||||
|  |             const quote = getRandomSellQuote(); | ||||||
|  |             const affiliateFee = { | ||||||
|  |                 recipient: randomAddress(), | ||||||
|  |                 buyTokenFeeAmount: ZERO_AMOUNT, | ||||||
|  |                 sellTokenFeeAmount: ZERO_AMOUNT, | ||||||
|  |                 feeType: AffiliateFeeType.PositiveSlippageFee, | ||||||
|  |             }; | ||||||
|  |             const callInfo = await consumer.getCalldataOrThrowAsync(quote, { | ||||||
|  |                 extensionContractOpts: { affiliateFee }, | ||||||
|  |             }); | ||||||
|  |             const callArgs = transformERC20Encoder.decode(callInfo.calldataHexString) as TransformERC20Args; | ||||||
|  |             expect(callArgs.transformations[1].deploymentNonce.toNumber()).to.eq( | ||||||
|  |                 consumer.transformerNonces.positiveSlippageFeeTransformer, | ||||||
|  |             ); | ||||||
|  |             const positiveSlippageFeeTransformerData = decodePositiveSlippageFeeTransformerData( | ||||||
|  |                 callArgs.transformations[1].data, | ||||||
|  |             ); | ||||||
|  |             const bestCaseAmount = quote.bestCaseQuoteInfo.makerAmount.plus( | ||||||
|  |                 POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS.multipliedBy(quote.gasPrice).multipliedBy( | ||||||
|  |                     quote.makerAmountPerEth, | ||||||
|  |                 ), | ||||||
|  |             ); | ||||||
|  |             expect(positiveSlippageFeeTransformerData).to.deep.equal({ | ||||||
|  |                 token: MAKER_TOKEN, | ||||||
|  |                 bestCaseAmount, | ||||||
|  |                 recipient: affiliateFee.recipient, | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|         it('Throws if a sell token affiliate fee is provided', async () => { |         it('Throws if a sell token affiliate fee is provided', async () => { | ||||||
|             const quote = getRandomSellQuote(); |             const quote = getRandomSellQuote(); | ||||||
|             const affiliateFee = { |             const affiliateFee = { | ||||||
|                 recipient: randomAddress(), |                 recipient: randomAddress(), | ||||||
|                 buyTokenFeeAmount: ZERO_AMOUNT, |                 buyTokenFeeAmount: ZERO_AMOUNT, | ||||||
|                 sellTokenFeeAmount: getRandomAmount(), |                 sellTokenFeeAmount: getRandomAmount(), | ||||||
|  |                 feeType: AffiliateFeeType.PercentageFee, | ||||||
|             }; |             }; | ||||||
|             expect( |             expect( | ||||||
|                 consumer.getCalldataOrThrowAsync(quote, { |                 consumer.getCalldataOrThrowAsync(quote, { | ||||||
|   | |||||||
| @@ -756,8 +756,8 @@ describe('MarketOperationUtils tests', () => { | |||||||
|                             inputAmount: Web3Wrapper.toBaseUnitAmount(1, 18), |                             inputAmount: Web3Wrapper.toBaseUnitAmount(1, 18), | ||||||
|                             inputToken: MAKER_TOKEN, |                             inputToken: MAKER_TOKEN, | ||||||
|                             outputToken: TAKER_TOKEN, |                             outputToken: TAKER_TOKEN, | ||||||
|                             ethToInputRate: Web3Wrapper.toBaseUnitAmount(1, 18), |                             inputAmountPerEth: Web3Wrapper.toBaseUnitAmount(1, 18), | ||||||
|                             ethToOutputRate: Web3Wrapper.toBaseUnitAmount(1, 6), |                             outputAmountPerEth: Web3Wrapper.toBaseUnitAmount(1, 6), | ||||||
|                             quoteSourceFilters: new SourceFilters(), |                             quoteSourceFilters: new SourceFilters(), | ||||||
|                             makerTokenDecimals: 6, |                             makerTokenDecimals: 6, | ||||||
|                             takerTokenDecimals: 18, |                             takerTokenDecimals: 18, | ||||||
| @@ -1787,7 +1787,7 @@ describe('MarketOperationUtils tests', () => { | |||||||
|  |  | ||||||
|     describe('createFills', () => { |     describe('createFills', () => { | ||||||
|         const takerAmount = new BigNumber(5000000); |         const takerAmount = new BigNumber(5000000); | ||||||
|         const ethToOutputRate = new BigNumber(0.5); |         const outputAmountPerEth = new BigNumber(0.5); | ||||||
|         // tslint:disable-next-line:no-object-literal-type-assertion |         // tslint:disable-next-line:no-object-literal-type-assertion | ||||||
|         const smallOrder: NativeOrderWithFillableAmounts = { |         const smallOrder: NativeOrderWithFillableAmounts = { | ||||||
|             order: { |             order: { | ||||||
| @@ -1830,7 +1830,7 @@ describe('MarketOperationUtils tests', () => { | |||||||
|                 orders, |                 orders, | ||||||
|                 dexQuotes: [], |                 dexQuotes: [], | ||||||
|                 targetInput: takerAmount.minus(1), |                 targetInput: takerAmount.minus(1), | ||||||
|                 ethToOutputRate, |                 outputAmountPerEth, | ||||||
|                 feeSchedule, |                 feeSchedule, | ||||||
|             }); |             }); | ||||||
|             expect((path[0][0].fillData as NativeFillData).order.maker).to.eq(smallOrder.order.maker); |             expect((path[0][0].fillData as NativeFillData).order.maker).to.eq(smallOrder.order.maker); | ||||||
| @@ -1843,7 +1843,7 @@ describe('MarketOperationUtils tests', () => { | |||||||
|                 orders, |                 orders, | ||||||
|                 dexQuotes: [], |                 dexQuotes: [], | ||||||
|                 targetInput: POSITIVE_INF, |                 targetInput: POSITIVE_INF, | ||||||
|                 ethToOutputRate, |                 outputAmountPerEth, | ||||||
|                 feeSchedule, |                 feeSchedule, | ||||||
|             }); |             }); | ||||||
|             expect((path[0][0].fillData as NativeFillData).order.maker).to.eq(largeOrder.order.maker); |             expect((path[0][0].fillData as NativeFillData).order.maker).to.eq(largeOrder.order.maker); | ||||||
|   | |||||||
| @@ -39,8 +39,8 @@ export async function getFullyFillableSwapQuoteWithNoFeesAsync( | |||||||
|         worstCaseQuoteInfo: quoteInfo, |         worstCaseQuoteInfo: quoteInfo, | ||||||
|         sourceBreakdown: breakdown, |         sourceBreakdown: breakdown, | ||||||
|         isTwoHop: false, |         isTwoHop: false, | ||||||
|         takerTokenToEthRate: constants.ZERO_AMOUNT, |         takerAmountPerEth: constants.ZERO_AMOUNT, | ||||||
|         makerTokenToEthRate: constants.ZERO_AMOUNT, |         makerAmountPerEth: constants.ZERO_AMOUNT, | ||||||
|         makerTokenDecimals: 18, |         makerTokenDecimals: 18, | ||||||
|         takerTokenDecimals: 18, |         takerTokenDecimals: 18, | ||||||
|     }; |     }; | ||||||
|   | |||||||
| @@ -5,6 +5,10 @@ | |||||||
|             { |             { | ||||||
|                 "note": "Deploy new FQT", |                 "note": "Deploy new FQT", | ||||||
|                 "pr": 155 |                 "pr": 155 | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 "note": "Deploy new `PositiveSlippageFeeTransformer`", | ||||||
|  |                 "pr": 101 | ||||||
|             } |             } | ||||||
|         ] |         ] | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -37,7 +37,8 @@ | |||||||
|             "wethTransformer": "0xb2bc06a4efb20fc6553a69dbfa49b7be938034a7", |             "wethTransformer": "0xb2bc06a4efb20fc6553a69dbfa49b7be938034a7", | ||||||
|             "payTakerTransformer": "0x4638a7ebe75b911b995d0ec73a81e4f85f41f24e", |             "payTakerTransformer": "0x4638a7ebe75b911b995d0ec73a81e4f85f41f24e", | ||||||
|             "affiliateFeeTransformer": "0xda6d9fc5998f550a094585cf9171f0e8ee3ac59f", |             "affiliateFeeTransformer": "0xda6d9fc5998f550a094585cf9171f0e8ee3ac59f", | ||||||
|             "fillQuoteTransformer": "0x227e767a9b7517681d1cb6b846aa9e541484c7ab" |             "fillQuoteTransformer": "0x227e767a9b7517681d1cb6b846aa9e541484c7ab", | ||||||
|  |             "positiveSlippageFeeTransformer": "0xa9416ce1dbde8d331210c07b5c253d94ee4cc3fd" | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|     "3": { |     "3": { | ||||||
| @@ -78,7 +79,8 @@ | |||||||
|             "wethTransformer": "0x05ad19aa3826e0609a19568ffbd1dfe86c6c7184", |             "wethTransformer": "0x05ad19aa3826e0609a19568ffbd1dfe86c6c7184", | ||||||
|             "payTakerTransformer": "0x6d0ebf2bcd9cc93ec553b60ad201943dcca4e291", |             "payTakerTransformer": "0x6d0ebf2bcd9cc93ec553b60ad201943dcca4e291", | ||||||
|             "affiliateFeeTransformer": "0x6588256778ca4432fa43983ac685c45efb2379e2", |             "affiliateFeeTransformer": "0x6588256778ca4432fa43983ac685c45efb2379e2", | ||||||
|             "fillQuoteTransformer": "0x2088a820787ebbe937a0612ef024f1e1d65f9784" |             "fillQuoteTransformer": "0x2088a820787ebbe937a0612ef024f1e1d65f9784", | ||||||
|  |             "positiveSlippageFeeTransformer": "0x8b332f700fd37e71c5c5b26c4d78b5ca63dd33b2" | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|     "4": { |     "4": { | ||||||
| @@ -119,7 +121,8 @@ | |||||||
|             "wethTransformer": "0x8d822fe2b42f60531203e288f5f357fa79474437", |             "wethTransformer": "0x8d822fe2b42f60531203e288f5f357fa79474437", | ||||||
|             "payTakerTransformer": "0x150652244723102faeaefa4c79597d097ffa26c6", |             "payTakerTransformer": "0x150652244723102faeaefa4c79597d097ffa26c6", | ||||||
|             "affiliateFeeTransformer": "0xa39b40642e8e00435857a0fe7d0655e08cc2217e", |             "affiliateFeeTransformer": "0xa39b40642e8e00435857a0fe7d0655e08cc2217e", | ||||||
|             "fillQuoteTransformer": "0x3fb85e0c1e9e0ba4ba9a4072442a2540c0473db1" |             "fillQuoteTransformer": "0x3fb85e0c1e9e0ba4ba9a4072442a2540c0473db1", | ||||||
|  |             "positiveSlippageFeeTransformer": "0x0000000000000000000000000000000000000000" | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|     "42": { |     "42": { | ||||||
| @@ -160,7 +163,8 @@ | |||||||
|             "wethTransformer": "0x9ce35b5ee9e710535e3988e3f8731d9ca9dba17d", |             "wethTransformer": "0x9ce35b5ee9e710535e3988e3f8731d9ca9dba17d", | ||||||
|             "payTakerTransformer": "0x5a53e7b02a83aa9f60ccf4e424f0442c255bc977", |             "payTakerTransformer": "0x5a53e7b02a83aa9f60ccf4e424f0442c255bc977", | ||||||
|             "affiliateFeeTransformer": "0x870893920a96a28d4b63c0a7d06a521e3bd074b3", |             "affiliateFeeTransformer": "0x870893920a96a28d4b63c0a7d06a521e3bd074b3", | ||||||
|             "fillQuoteTransformer": "0x8d2d732e5fe6d4d6d5e715200b84dfa69fb05478" |             "fillQuoteTransformer": "0x8d2d732e5fe6d4d6d5e715200b84dfa69fb05478", | ||||||
|  |             "positiveSlippageFeeTransformer": "0x0000000000000000000000000000000000000000" | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|     "1337": { |     "1337": { | ||||||
| @@ -201,7 +205,8 @@ | |||||||
|             "wethTransformer": "0x7209185959d7227fb77274e1e88151d7c4c368d3", |             "wethTransformer": "0x7209185959d7227fb77274e1e88151d7c4c368d3", | ||||||
|             "payTakerTransformer": "0x3f16ca81691dab9184cb4606c361d73c4fd2510a", |             "payTakerTransformer": "0x3f16ca81691dab9184cb4606c361d73c4fd2510a", | ||||||
|             "affiliateFeeTransformer": "0x99356167edba8fbdc36959e3f5d0c43d1ba9c6db", |             "affiliateFeeTransformer": "0x99356167edba8fbdc36959e3f5d0c43d1ba9c6db", | ||||||
|             "fillQuoteTransformer": "0x45b3a72221e571017c0f0ec42189e11d149d0ace" |             "fillQuoteTransformer": "0x45b3a72221e571017c0f0ec42189e11d149d0ace", | ||||||
|  |             "positiveSlippageFeeTransformer": "0xdd66c23e07b4d6925b6089b5fe6fc9e62941afe8" | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -39,6 +39,7 @@ export interface ContractAddresses { | |||||||
|         payTakerTransformer: string; |         payTakerTransformer: string; | ||||||
|         fillQuoteTransformer: string; |         fillQuoteTransformer: string; | ||||||
|         affiliateFeeTransformer: string; |         affiliateFeeTransformer: string; | ||||||
|  |         positiveSlippageFeeTransformer: string; | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -32,6 +32,7 @@ import { | |||||||
|     FillQuoteTransformerContract, |     FillQuoteTransformerContract, | ||||||
|     fullMigrateAsync as fullMigrateExchangeProxyAsync, |     fullMigrateAsync as fullMigrateExchangeProxyAsync, | ||||||
|     PayTakerTransformerContract, |     PayTakerTransformerContract, | ||||||
|  |     PositiveSlippageFeeTransformerContract, | ||||||
|     WethTransformerContract, |     WethTransformerContract, | ||||||
| } from '@0x/contracts-zero-ex'; | } from '@0x/contracts-zero-ex'; | ||||||
| import { Web3ProviderEngine } from '@0x/subproviders'; | import { Web3ProviderEngine } from '@0x/subproviders'; | ||||||
| @@ -345,7 +346,12 @@ export async function runMigrationsAsync( | |||||||
|         bridgeAdapter.address, |         bridgeAdapter.address, | ||||||
|         exchangeProxy.address, |         exchangeProxy.address, | ||||||
|     ); |     ); | ||||||
|  |     const positiveSlippageFeeTransformer = await PositiveSlippageFeeTransformerContract.deployFrom0xArtifactAsync( | ||||||
|  |         exchangeProxyArtifacts.PositiveSlippageFeeTransformer, | ||||||
|  |         provider, | ||||||
|  |         txDefaults, | ||||||
|  |         allArtifacts, | ||||||
|  |     ); | ||||||
|     const contractAddresses = { |     const contractAddresses = { | ||||||
|         erc20Proxy: erc20Proxy.address, |         erc20Proxy: erc20Proxy.address, | ||||||
|         erc721Proxy: erc721Proxy.address, |         erc721Proxy: erc721Proxy.address, | ||||||
| @@ -385,6 +391,7 @@ export async function runMigrationsAsync( | |||||||
|             payTakerTransformer: payTakerTransformer.address, |             payTakerTransformer: payTakerTransformer.address, | ||||||
|             fillQuoteTransformer: fillQuoteTransformer.address, |             fillQuoteTransformer: fillQuoteTransformer.address, | ||||||
|             affiliateFeeTransformer: affiliateFeeTransformer.address, |             affiliateFeeTransformer: affiliateFeeTransformer.address, | ||||||
|  |             positiveSlippageFeeTransformer: positiveSlippageFeeTransformer.address, | ||||||
|         }, |         }, | ||||||
|     }; |     }; | ||||||
|     return contractAddresses; |     return contractAddresses; | ||||||
|   | |||||||
| @@ -77,6 +77,9 @@ export { | |||||||
|     AffiliateFeeTransformerData, |     AffiliateFeeTransformerData, | ||||||
|     encodeAffiliateFeeTransformerData, |     encodeAffiliateFeeTransformerData, | ||||||
|     decodeAffiliateFeeTransformerData, |     decodeAffiliateFeeTransformerData, | ||||||
|  |     PositiveSlippageFeeTransformerData, | ||||||
|  |     encodePositiveSlippageFeeTransformerData, | ||||||
|  |     decodePositiveSlippageFeeTransformerData, | ||||||
|     findTransformerNonce, |     findTransformerNonce, | ||||||
|     getTransformerAddress, |     getTransformerAddress, | ||||||
| } from './transformer_utils'; | } from './transformer_utils'; | ||||||
|   | |||||||
| @@ -152,7 +152,7 @@ export function decodePayTakerTransformerData(encoded: string): PayTakerTransfor | |||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * ABI encoder for `PayTakerTransformer.TransformData` |  * ABI encoder for `affiliateFeetransformer.TransformData` | ||||||
|  */ |  */ | ||||||
| export const affiliateFeeTransformerDataEncoder = AbiEncoder.create({ | export const affiliateFeeTransformerDataEncoder = AbiEncoder.create({ | ||||||
|     name: 'data', |     name: 'data', | ||||||
| @@ -195,6 +195,42 @@ export function decodeAffiliateFeeTransformerData(encoded: string): AffiliateFee | |||||||
|     return affiliateFeeTransformerDataEncoder.decode(encoded); |     return affiliateFeeTransformerDataEncoder.decode(encoded); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * ABI encoder for `PositiveSlippageFeeTransformer.TransformData` | ||||||
|  |  */ | ||||||
|  | export const positiveSlippageFeeTransformerDataEncoder = AbiEncoder.create({ | ||||||
|  |     name: 'data', | ||||||
|  |     type: 'tuple', | ||||||
|  |     components: [ | ||||||
|  |         { name: 'token', type: 'address' }, | ||||||
|  |         { name: 'bestCaseAmount', type: 'uint256' }, | ||||||
|  |         { name: 'recipient', type: 'address' }, | ||||||
|  |     ], | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * `PositiveSlippageFeeTransformer.TransformData` | ||||||
|  |  */ | ||||||
|  | export interface PositiveSlippageFeeTransformerData { | ||||||
|  |     token: string; | ||||||
|  |     bestCaseAmount: BigNumber; | ||||||
|  |     recipient: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * ABI-encode a `PositiveSlippageFeeTransformer.TransformData` type. | ||||||
|  |  */ | ||||||
|  | export function encodePositiveSlippageFeeTransformerData(data: PositiveSlippageFeeTransformerData): string { | ||||||
|  |     return positiveSlippageFeeTransformerDataEncoder.encode(data); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * ABI-decode a `PositiveSlippageFeeTransformer.TransformData` type. | ||||||
|  |  */ | ||||||
|  | export function decodePositiveSlippageFeeTransformerData(encoded: string): PositiveSlippageFeeTransformerData { | ||||||
|  |     return positiveSlippageFeeTransformerDataEncoder.decode(encoded); | ||||||
|  | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Find the nonce for a transformer given its deployer. |  * Find the nonce for a transformer given its deployer. | ||||||
|  * If `deployer` is the null address, zero will always be returned. |  * If `deployer` is the null address, zero will always be returned. | ||||||
|   | |||||||
| @@ -242,7 +242,7 @@ export function decodePayTakerTransformerData(encoded: string): PayTakerTransfor | |||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * ABI encoder for `PayTakerTransformer.TransformData` |  * ABI encoder for `affiliateFeetransformer.TransformData` | ||||||
|  */ |  */ | ||||||
| export const affiliateFeeTransformerDataEncoder = AbiEncoder.create({ | export const affiliateFeeTransformerDataEncoder = AbiEncoder.create({ | ||||||
|     name: 'data', |     name: 'data', | ||||||
| @@ -317,3 +317,39 @@ export function getTransformerAddress(deployer: string, nonce: number): string { | |||||||
|         ethjs.rlphash([deployer, nonce] as any).slice(12), |         ethjs.rlphash([deployer, nonce] as any).slice(12), | ||||||
|     ); |     ); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * ABI encoder for `PositiveSlippageFeeTransformer.TransformData` | ||||||
|  |  */ | ||||||
|  | export const positiveSlippageFeeTransformerDataEncoder = AbiEncoder.create({ | ||||||
|  |     name: 'data', | ||||||
|  |     type: 'tuple', | ||||||
|  |     components: [ | ||||||
|  |         { name: 'token', type: 'address' }, | ||||||
|  |         { name: 'bestCaseAmount', type: 'uint256' }, | ||||||
|  |         { name: 'recipient', type: 'address' }, | ||||||
|  |     ], | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * `PositiveSlippageFeeTransformer.TransformData` | ||||||
|  |  */ | ||||||
|  | export interface PositiveSlippageFeeTransformerData { | ||||||
|  |     token: string; | ||||||
|  |     bestCaseAmount: BigNumber; | ||||||
|  |     recipient: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * ABI-encode a `PositiveSlippageFeeTransformer.TransformData` type. | ||||||
|  |  */ | ||||||
|  | export function encodePositiveSlippageFeeTransformerData(data: PositiveSlippageFeeTransformerData): string { | ||||||
|  |     return positiveSlippageFeeTransformerDataEncoder.encode(data); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * ABI-decode a `PositiveSlippageFeeTransformer.TransformData` type. | ||||||
|  |  */ | ||||||
|  | export function decodePositiveSlippageFeeTransformerData(encoded: string): PositiveSlippageFeeTransformerData { | ||||||
|  |     return positiveSlippageFeeTransformerDataEncoder.decode(encoded); | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user