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", | ||||
|                 "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" | ||||
|     }, | ||||
|     "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": "./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": { | ||||
|         "type": "git", | ||||
|   | ||||
| @@ -29,6 +29,7 @@ import * as MetaTransactionsFeature from '../generated-artifacts/MetaTransaction | ||||
| import * as NativeOrdersFeature from '../generated-artifacts/NativeOrdersFeature.json'; | ||||
| import * as OwnableFeature from '../generated-artifacts/OwnableFeature.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 TokenSpenderFeature from '../generated-artifacts/TokenSpenderFeature.json'; | ||||
| import * as TransformERC20Feature from '../generated-artifacts/TransformERC20Feature.json'; | ||||
| @@ -48,6 +49,7 @@ export const artifacts = { | ||||
|     ITransformERC20Feature: ITransformERC20Feature as ContractArtifact, | ||||
|     FillQuoteTransformer: FillQuoteTransformer as ContractArtifact, | ||||
|     PayTakerTransformer: PayTakerTransformer as ContractArtifact, | ||||
|     PositiveSlippageFeeTransformer: PositiveSlippageFeeTransformer as ContractArtifact, | ||||
|     WethTransformer: WethTransformer as ContractArtifact, | ||||
|     OwnableFeature: OwnableFeature as ContractArtifact, | ||||
|     SimpleFunctionRegistryFeature: SimpleFunctionRegistryFeature as ContractArtifact, | ||||
|   | ||||
| @@ -46,6 +46,7 @@ export { | ||||
|     IZeroExContract, | ||||
|     LogMetadataTransformerContract, | ||||
|     PayTakerTransformerContract, | ||||
|     PositiveSlippageFeeTransformerContract, | ||||
|     WethTransformerContract, | ||||
|     ZeroExContract, | ||||
| } from './wrappers'; | ||||
|   | ||||
| @@ -27,6 +27,7 @@ export * from '../generated-wrappers/meta_transactions_feature'; | ||||
| export * from '../generated-wrappers/native_orders_feature'; | ||||
| export * from '../generated-wrappers/ownable_feature'; | ||||
| 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/token_spender_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 PayTakerTransformer from '../test/generated-artifacts/PayTakerTransformer.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 TestBridge from '../test/generated-artifacts/TestBridge.json'; | ||||
| import * as TestCallTarget from '../test/generated-artifacts/TestCallTarget.json'; | ||||
| @@ -209,6 +210,7 @@ export const artifacts = { | ||||
|     LibERC20Transformer: LibERC20Transformer as ContractArtifact, | ||||
|     LogMetadataTransformer: LogMetadataTransformer as ContractArtifact, | ||||
|     PayTakerTransformer: PayTakerTransformer as ContractArtifact, | ||||
|     PositiveSlippageFeeTransformer: PositiveSlippageFeeTransformer as ContractArtifact, | ||||
|     Transformer: Transformer as ContractArtifact, | ||||
|     WethTransformer: WethTransformer 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/pay_taker_transformer'; | ||||
| 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/test_bridge'; | ||||
| export * from '../test/generated-wrappers/test_call_target'; | ||||
|   | ||||
| @@ -27,6 +27,7 @@ | ||||
|         "generated-artifacts/NativeOrdersFeature.json", | ||||
|         "generated-artifacts/OwnableFeature.json", | ||||
|         "generated-artifacts/PayTakerTransformer.json", | ||||
|         "generated-artifacts/PositiveSlippageFeeTransformer.json", | ||||
|         "generated-artifacts/SimpleFunctionRegistryFeature.json", | ||||
|         "generated-artifacts/TokenSpenderFeature.json", | ||||
|         "generated-artifacts/TransformERC20Feature.json", | ||||
| @@ -119,6 +120,7 @@ | ||||
|         "test/generated-artifacts/OwnableFeature.json", | ||||
|         "test/generated-artifacts/PayTakerTransformer.json", | ||||
|         "test/generated-artifacts/PermissionlessTransformerDeployer.json", | ||||
|         "test/generated-artifacts/PositiveSlippageFeeTransformer.json", | ||||
|         "test/generated-artifacts/SimpleFunctionRegistryFeature.json", | ||||
|         "test/generated-artifacts/TestBridge.json", | ||||
|         "test/generated-artifacts/TestCallTarget.json", | ||||
|   | ||||
| @@ -49,6 +49,10 @@ | ||||
|             { | ||||
|                 "note": "Add an alternative RFQ market making implementation", | ||||
|                 "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 { | ||||
|     AffiliateFeeType, | ||||
|     ExchangeProxyContractOpts, | ||||
|     LogFunction, | ||||
|     OrderPrunerOpts, | ||||
| @@ -12,7 +13,11 @@ import { | ||||
|     SwapQuoteRequestOpts, | ||||
|     SwapQuoterOpts, | ||||
| } 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 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 | ||||
| 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 = { | ||||
|     chainId: ChainId.Mainnet, | ||||
|     orderRefreshIntervalMs: 10000, // 10 seconds | ||||
| @@ -49,12 +53,14 @@ const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = { | ||||
|         takerApiKeyWhitelist: [], | ||||
|         makerAssetOfferings: {}, | ||||
|     }, | ||||
|     tokenAdjacencyGraph: DEFAULT_TOKEN_ADJACENCY_GRAPH, | ||||
| }; | ||||
|  | ||||
| const DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS: ExchangeProxyContractOpts = { | ||||
|     isFromETH: false, | ||||
|     isToETH: false, | ||||
|     affiliateFee: { | ||||
|         feeType: AffiliateFeeType.None, | ||||
|         recipient: NULL_ADDRESS, | ||||
|         buyTokenFeeAmount: 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 const POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS = new BigNumber(30000); | ||||
|  | ||||
| export const constants = { | ||||
|     ETH_GAS_STATION_API_URL, | ||||
|     PROTOCOL_FEE_MULTIPLIER, | ||||
|     POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS, | ||||
|     NULL_BYTES, | ||||
|     ZERO_AMOUNT, | ||||
|     NULL_ADDRESS, | ||||
|   | ||||
| @@ -74,9 +74,10 @@ export { InsufficientAssetLiquidityError } from './errors'; | ||||
| export { SwapQuoteConsumer } from './quote_consumers/swap_quote_consumer'; | ||||
| export { SwapQuoter, Orderbook } from './swap_quoter'; | ||||
| export { | ||||
|     AffiliateFee, | ||||
|     AltOffering, | ||||
|     AltRfqtMakerAssetOfferings, | ||||
|     AffiliateFeeType, | ||||
|     AffiliateFeeAmount, | ||||
|     AssetSwapperContractAddresses, | ||||
|     CalldataInfo, | ||||
|     ExchangeProxyContractOpts, | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import { | ||||
|     encodeCurveLiquidityProviderData, | ||||
|     encodeFillQuoteTransformerData, | ||||
|     encodePayTakerTransformerData, | ||||
|     encodePositiveSlippageFeeTransformerData, | ||||
|     encodeWethTransformerData, | ||||
|     ETH_TOKEN_ADDRESS, | ||||
|     FillQuoteTransformerData, | ||||
| @@ -16,8 +17,9 @@ import { BigNumber, providerUtils } from '@0x/utils'; | ||||
| import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper'; | ||||
| import * as _ from 'lodash'; | ||||
|  | ||||
| import { constants } from '../constants'; | ||||
| import { constants, POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS } from '../constants'; | ||||
| import { | ||||
|     AffiliateFeeType, | ||||
|     CalldataInfo, | ||||
|     ExchangeProxyContractOpts, | ||||
|     MarketBuySwapQuote, | ||||
| @@ -59,6 +61,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { | ||||
|         payTakerTransformer: number; | ||||
|         fillQuoteTransformer: number; | ||||
|         affiliateFeeTransformer: number; | ||||
|         positiveSlippageFeeTransformer: number; | ||||
|     }; | ||||
|  | ||||
|     private readonly _exchangeProxy: IZeroExContract; | ||||
| @@ -92,6 +95,10 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { | ||||
|                 contractAddresses.transformers.affiliateFeeTransformer, | ||||
|                 contractAddresses.exchangeProxyTransformerDeployer, | ||||
|             ), | ||||
|             positiveSlippageFeeTransformer: findTransformerNonce( | ||||
|                 contractAddresses.transformers.positiveSlippageFeeTransformer, | ||||
|                 contractAddresses.exchangeProxyTransformerDeployer, | ||||
|             ), | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| @@ -117,7 +124,6 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { | ||||
|         if (isFromETH) { | ||||
|             ethAmount = ethAmount.plus(sellAmount); | ||||
|         } | ||||
|         const { buyTokenFeeAmount, sellTokenFeeAmount, recipient: feeRecipient } = affiliateFee; | ||||
|  | ||||
|         // VIP routes. | ||||
|         if ( | ||||
| @@ -144,7 +150,8 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { | ||||
|                     .getABIEncodedTransactionData(), | ||||
|                 ethAmount: isFromETH ? sellAmount : ZERO_AMOUNT, | ||||
|                 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(), | ||||
|                 ethAmount: isFromETH ? sellAmount : ZERO_AMOUNT, | ||||
|                 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(), | ||||
|                 ethAmount: isFromETH ? sellAmount : ZERO_AMOUNT, | ||||
|                 toAddress: this._exchangeProxy.address, | ||||
|                 allowanceTarget: this.contractAddresses.exchangeProxyAllowanceTarget, | ||||
|                 allowanceTarget: this._exchangeProxy.address, | ||||
|                 gasOverhead: ZERO_AMOUNT, | ||||
|             }; | ||||
|         } | ||||
|  | ||||
| @@ -262,8 +271,34 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         const { feeType, buyTokenFeeAmount, sellTokenFeeAmount, recipient: feeRecipient } = affiliateFee; | ||||
|         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({ | ||||
|                 deploymentNonce: this.transformerNonces.positiveSlippageFeeTransformer, | ||||
|                 data: encodePositiveSlippageFeeTransformerData({ | ||||
|                     token: isToETH ? ETH_TOKEN_ADDRESS : buyToken, | ||||
|                     bestCaseAmount: BigNumber.max(bestCaseAmountWithSurplus, quote.bestCaseQuoteInfo.makerAmount), | ||||
|                     recipient: feeRecipient, | ||||
|                 }), | ||||
|             }); | ||||
|             // This may not be visible at eth_estimateGas time, so we explicitly add overhead | ||||
|             gasOverhead = POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS; | ||||
|         } else if (feeType === AffiliateFeeType.PercentageFee && feeRecipient !== NULL_ADDRESS) { | ||||
|             // This transformer pays affiliate fees. | ||||
|         if (buyTokenFeeAmount.isGreaterThan(0) && feeRecipient !== NULL_ADDRESS) { | ||||
|             if (buyTokenFeeAmount.isGreaterThan(0)) { | ||||
|                 transforms.push({ | ||||
|                     deploymentNonce: this.transformerNonces.affiliateFeeTransformer, | ||||
|                     data: encodeAffiliateFeeTransformerData({ | ||||
| @@ -279,9 +314,10 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { | ||||
|                 // Adjust the minimum buy amount by the fee. | ||||
|                 minBuyAmount = BigNumber.max(0, minBuyAmount.minus(buyTokenFeeAmount)); | ||||
|             } | ||||
|         if (sellTokenFeeAmount.isGreaterThan(0) && feeRecipient !== NULL_ADDRESS) { | ||||
|             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. | ||||
|         transforms.push({ | ||||
| @@ -306,7 +342,8 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { | ||||
|             calldataHexString, | ||||
|             ethAmount, | ||||
|             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)) { | ||||
|         return false; | ||||
|     } | ||||
|     // Must not have a positive slippage fee. | ||||
|     if (opts.affiliateFee.feeType === AffiliateFeeType.PositiveSlippageFee) { | ||||
|         return false; | ||||
|     } | ||||
|     // Must be a single order. | ||||
|     if (quote.orders.length !== 1) { | ||||
|         return false; | ||||
|   | ||||
| @@ -454,7 +454,7 @@ function createSwapQuote( | ||||
|     gasSchedule: FeeSchedule, | ||||
|     slippage: number, | ||||
| ): SwapQuote { | ||||
|     const { optimizedOrders, quoteReport, sourceFlags, takerTokenToEthRate, makerTokenToEthRate } = optimizerResult; | ||||
|     const { optimizedOrders, quoteReport, sourceFlags, takerAmountPerEth, makerAmountPerEth } = optimizerResult; | ||||
|     const isTwoHop = sourceFlags === SOURCE_FLAGS[ERC20BridgeSource.MultiHop]; | ||||
|  | ||||
|     // Calculate quote info | ||||
| @@ -474,8 +474,8 @@ function createSwapQuote( | ||||
|         sourceBreakdown, | ||||
|         makerTokenDecimals, | ||||
|         takerTokenDecimals, | ||||
|         takerTokenToEthRate, | ||||
|         makerTokenToEthRate, | ||||
|         takerAmountPerEth, | ||||
|         makerAmountPerEth, | ||||
|         quoteReport, | ||||
|         isTwoHop, | ||||
|     }; | ||||
|   | ||||
| @@ -54,12 +54,15 @@ export interface NativeOrderFillableAmountFields { | ||||
|  * toAddress: The contract address to call. | ||||
|  * ethAmount: The eth amount in wei to send with the smart contract call. | ||||
|  * 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 { | ||||
|     calldataHexString: string; | ||||
|     toAddress: string; | ||||
|     ethAmount: BigNumber; | ||||
|     allowanceTarget: string; | ||||
|     gasOverhead: BigNumber; | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -98,7 +101,14 @@ export interface SwapQuoteExecutionOpts extends SwapQuoteGetOutputOpts { | ||||
|     gasLimit?: number; | ||||
| } | ||||
|  | ||||
| export interface AffiliateFee { | ||||
| export enum AffiliateFeeType { | ||||
|     None, | ||||
|     PercentageFee, | ||||
|     PositiveSlippageFee, | ||||
| } | ||||
|  | ||||
| export interface AffiliateFeeAmount { | ||||
|     feeType: AffiliateFeeType; | ||||
|     recipient: string; | ||||
|     buyTokenFeeAmount: BigNumber; | ||||
|     sellTokenFeeAmount: BigNumber; | ||||
| @@ -130,7 +140,7 @@ export enum ExchangeProxyRefundReceiver { | ||||
| export interface ExchangeProxyContractOpts { | ||||
|     isFromETH: boolean; | ||||
|     isToETH: boolean; | ||||
|     affiliateFee: AffiliateFee; | ||||
|     affiliateFee: AffiliateFeeAmount; | ||||
|     refundReceiver: string | ExchangeProxyRefundReceiver; | ||||
|     isMetaTransaction: boolean; | ||||
|     shouldSellEntireBalance: boolean; | ||||
| @@ -161,8 +171,8 @@ export interface SwapQuoteBase { | ||||
|     isTwoHop: boolean; | ||||
|     makerTokenDecimals: number; | ||||
|     takerTokenDecimals: number; | ||||
|     takerTokenToEthRate: BigNumber; | ||||
|     makerTokenToEthRate: BigNumber; | ||||
|     takerAmountPerEth: 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) | ||||
|     const feePenalty = !marketSideLiquidity.ethToOutputRate.isZero() | ||||
|         ? marketSideLiquidity.ethToOutputRate.times(feeInEth) | ||||
|     const feePenalty = !marketSideLiquidity.outputAmountPerEth.isZero() | ||||
|         ? marketSideLiquidity.outputAmountPerEth.times(feeInEth) | ||||
|         : // if it's a sell, the input token is the taker token | ||||
|           marketSideLiquidity.ethToInputRate | ||||
|           marketSideLiquidity.inputAmountPerEth | ||||
|               .times(feeInEth) | ||||
|               .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 POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS = new BigNumber(20000); | ||||
|  | ||||
| // tslint:enable:custom-no-magic-numbers | ||||
|  | ||||
| export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = { | ||||
|   | ||||
| @@ -16,8 +16,8 @@ export function createFills(opts: { | ||||
|     orders?: NativeOrderWithFillableAmounts[]; | ||||
|     dexQuotes?: DexSample[][]; | ||||
|     targetInput?: BigNumber; | ||||
|     ethToOutputRate?: BigNumber; | ||||
|     ethToInputRate?: BigNumber; | ||||
|     outputAmountPerEth?: BigNumber; | ||||
|     inputAmountPerEth?: BigNumber; | ||||
|     excludedSources?: ERC20BridgeSource[]; | ||||
|     feeSchedule?: FeeSchedule; | ||||
| }): Fill[][] { | ||||
| @@ -26,20 +26,20 @@ export function createFills(opts: { | ||||
|     const feeSchedule = opts.feeSchedule || {}; | ||||
|     const orders = opts.orders || []; | ||||
|     const dexQuotes = opts.dexQuotes || []; | ||||
|     const ethToOutputRate = opts.ethToOutputRate || ZERO_AMOUNT; | ||||
|     const ethToInputRate = opts.ethToInputRate || ZERO_AMOUNT; | ||||
|     const outputAmountPerEth = opts.outputAmountPerEth || ZERO_AMOUNT; | ||||
|     const inputAmountPerEth = opts.inputAmountPerEth || ZERO_AMOUNT; | ||||
|     // Create native fills. | ||||
|     const nativeFills = nativeOrdersToFills( | ||||
|         side, | ||||
|         orders.filter(o => o.fillableTakerAmount.isGreaterThan(0)), | ||||
|         opts.targetInput, | ||||
|         ethToOutputRate, | ||||
|         ethToInputRate, | ||||
|         outputAmountPerEth, | ||||
|         inputAmountPerEth, | ||||
|         feeSchedule, | ||||
|     ); | ||||
|     // Create DEX fills. | ||||
|     const dexFills = dexQuotes.map(singleSourceSamples => | ||||
|         dexSamplesToFills(side, singleSourceSamples, ethToOutputRate, ethToInputRate, feeSchedule), | ||||
|         dexSamplesToFills(side, singleSourceSamples, outputAmountPerEth, inputAmountPerEth, feeSchedule), | ||||
|     ); | ||||
|     return [...dexFills, nativeFills] | ||||
|         .map(p => clipFillsToInput(p, opts.targetInput)) | ||||
| @@ -75,8 +75,8 @@ function nativeOrdersToFills( | ||||
|     side: MarketOperation, | ||||
|     orders: NativeOrderWithFillableAmounts[], | ||||
|     targetInput: BigNumber = POSITIVE_INF, | ||||
|     ethToOutputRate: BigNumber, | ||||
|     ethToInputRate: BigNumber, | ||||
|     outputAmountPerEth: BigNumber, | ||||
|     inputAmountPerEth: BigNumber, | ||||
|     fees: FeeSchedule, | ||||
| ): Fill[] { | ||||
|     const sourcePathId = hexUtils.random(); | ||||
| @@ -89,9 +89,9 @@ function nativeOrdersToFills( | ||||
|         const input = side === MarketOperation.Sell ? takerAmount : makerAmount; | ||||
|         const output = side === MarketOperation.Sell ? makerAmount : takerAmount; | ||||
|         const fee = fees[ERC20BridgeSource.Native] === undefined ? 0 : fees[ERC20BridgeSource.Native]!(o); | ||||
|         const outputPenalty = !ethToOutputRate.isZero() | ||||
|             ? ethToOutputRate.times(fee) | ||||
|             : ethToInputRate.times(fee).times(output.dividedToIntegerBy(input)); | ||||
|         const outputPenalty = !outputAmountPerEth.isZero() | ||||
|             ? outputAmountPerEth.times(fee) | ||||
|             : inputAmountPerEth.times(fee).times(output.dividedToIntegerBy(input)); | ||||
|         // targetInput can be less than the order size | ||||
|         // whilst the penalty is constant, it affects the adjusted output | ||||
|         // only up until the target has been exhausted. | ||||
| @@ -135,8 +135,8 @@ function nativeOrdersToFills( | ||||
| function dexSamplesToFills( | ||||
|     side: MarketOperation, | ||||
|     samples: DexSample[], | ||||
|     ethToOutputRate: BigNumber, | ||||
|     ethToInputRate: BigNumber, | ||||
|     outputAmountPerEth: BigNumber, | ||||
|     inputAmountPerEth: BigNumber, | ||||
|     fees: FeeSchedule, | ||||
| ): Fill[] { | ||||
|     const sourcePathId = hexUtils.random(); | ||||
| @@ -156,9 +156,9 @@ function dexSamplesToFills( | ||||
|         let penalty = ZERO_AMOUNT; | ||||
|         if (i === 0) { | ||||
|             // Only the first fill in a DEX path incurs a penalty. | ||||
|             penalty = !ethToOutputRate.isZero() | ||||
|                 ? ethToOutputRate.times(fee) | ||||
|                 : ethToInputRate.times(fee).times(output.dividedToIntegerBy(input)); | ||||
|             penalty = !outputAmountPerEth.isZero() | ||||
|                 ? outputAmountPerEth.times(fee) | ||||
|                 : inputAmountPerEth.times(fee).times(output.dividedToIntegerBy(input)); | ||||
|         } | ||||
|         const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty); | ||||
|  | ||||
|   | ||||
| @@ -30,7 +30,8 @@ import { | ||||
| import { createFills } from './fills'; | ||||
| import { getBestTwoHopQuote } from './multihop_utils'; | ||||
| 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 { SourceFilters } from './source_filters'; | ||||
| import { | ||||
| @@ -167,8 +168,8 @@ export class MarketOperationUtils { | ||||
|             [ | ||||
|                 tokenDecimals, | ||||
|                 orderFillableTakerAmounts, | ||||
|                 ethToMakerAssetRate, | ||||
|                 ethToTakerAssetRate, | ||||
|                 outputAmountPerEth, | ||||
|                 inputAmountPerEth, | ||||
|                 dexQuotes, | ||||
|                 rawTwoHopQuotes, | ||||
|                 isTxOriginContract, | ||||
| @@ -195,8 +196,8 @@ export class MarketOperationUtils { | ||||
|             inputAmount: takerAmount, | ||||
|             inputToken: takerToken, | ||||
|             outputToken: makerToken, | ||||
|             ethToOutputRate: ethToMakerAssetRate, | ||||
|             ethToInputRate: ethToTakerAssetRate, | ||||
|             outputAmountPerEth, | ||||
|             inputAmountPerEth, | ||||
|             quoteSourceFilters, | ||||
|             makerTokenDecimals: makerTokenDecimals.toNumber(), | ||||
|             takerTokenDecimals: takerTokenDecimals.toNumber(), | ||||
| @@ -321,8 +322,8 @@ export class MarketOperationUtils { | ||||
|             inputAmount: makerAmount, | ||||
|             inputToken: makerToken, | ||||
|             outputToken: takerToken, | ||||
|             ethToOutputRate: ethToTakerAssetRate, | ||||
|             ethToInputRate: ethToMakerAssetRate, | ||||
|             outputAmountPerEth: ethToTakerAssetRate, | ||||
|             inputAmountPerEth: ethToMakerAssetRate, | ||||
|             quoteSourceFilters, | ||||
|             makerTokenDecimals: makerTokenDecimals.toNumber(), | ||||
|             takerTokenDecimals: takerTokenDecimals.toNumber(), | ||||
| @@ -392,7 +393,7 @@ export class MarketOperationUtils { | ||||
|         const batchEthToTakerAssetRate = executeResults.splice(0, batchNativeOrders.length) as BigNumber[]; | ||||
|         const batchDexQuotes = executeResults.splice(0, batchNativeOrders.length) as DexSample[][][]; | ||||
|         const batchTokenDecimals = executeResults.splice(0, batchNativeOrders.length) as number[][]; | ||||
|         const ethToInputRate = ZERO_AMOUNT; | ||||
|         const inputAmountPerEth = ZERO_AMOUNT; | ||||
|  | ||||
|         return Promise.all( | ||||
|             batchNativeOrders.map(async (nativeOrders, i) => { | ||||
| @@ -401,7 +402,7 @@ export class MarketOperationUtils { | ||||
|                 } | ||||
|                 const { makerToken, takerToken } = nativeOrders[0].order; | ||||
|                 const orderFillableMakerAmounts = batchOrderFillableMakerAmounts[i]; | ||||
|                 const ethToTakerAssetRate = batchEthToTakerAssetRate[i]; | ||||
|                 const outputAmountPerEth = batchEthToTakerAssetRate[i]; | ||||
|                 const dexQuotes = batchDexQuotes[i]; | ||||
|                 const makerAmount = makerAmounts[i]; | ||||
|                 try { | ||||
| @@ -411,8 +412,8 @@ export class MarketOperationUtils { | ||||
|                             inputToken: makerToken, | ||||
|                             outputToken: takerToken, | ||||
|                             inputAmount: makerAmount, | ||||
|                             ethToOutputRate: ethToTakerAssetRate, | ||||
|                             ethToInputRate, | ||||
|                             outputAmountPerEth, | ||||
|                             inputAmountPerEth, | ||||
|                             quoteSourceFilters, | ||||
|                             makerTokenDecimals: batchTokenDecimals[i][0], | ||||
|                             takerTokenDecimals: batchTokenDecimals[i][1], | ||||
| @@ -455,8 +456,8 @@ export class MarketOperationUtils { | ||||
|             side, | ||||
|             inputAmount, | ||||
|             quotes, | ||||
|             ethToOutputRate, | ||||
|             ethToInputRate, | ||||
|             outputAmountPerEth, | ||||
|             inputAmountPerEth, | ||||
|         } = marketSideLiquidity; | ||||
|         const { nativeOrders, rfqtIndicativeQuotes, dexQuotes } = quotes; | ||||
|         const maxFallbackSlippage = opts.maxFallbackSlippage || 0; | ||||
| @@ -489,25 +490,29 @@ export class MarketOperationUtils { | ||||
|             orders: [...nativeOrders, ...augmentedRfqtIndicativeQuotes], | ||||
|             dexQuotes, | ||||
|             targetInput: inputAmount, | ||||
|             ethToOutputRate, | ||||
|             ethToInputRate, | ||||
|             outputAmountPerEth, | ||||
|             inputAmountPerEth, | ||||
|             excludedSources: opts.excludedSources, | ||||
|             feeSchedule: opts.feeSchedule, | ||||
|         }); | ||||
|  | ||||
|         // Find the optimal path. | ||||
|         const optimizerOpts = { | ||||
|             ethToOutputRate, | ||||
|             ethToInputRate, | ||||
|         const penaltyOpts: PathPenaltyOpts = { | ||||
|             outputAmountPerEth, | ||||
|             inputAmountPerEth, | ||||
|             exchangeProxyOverhead: opts.exchangeProxyOverhead || (() => ZERO_AMOUNT), | ||||
|         }; | ||||
|  | ||||
|         // 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 makerTokenToEthRate = side === MarketOperation.Sell ? ethToOutputRate : ethToInputRate; | ||||
|         const takerAmountPerEth = side === MarketOperation.Sell ? inputAmountPerEth : outputAmountPerEth; | ||||
|         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 | ||||
|         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 { adjustedRate: bestTwoHopRate, quote: bestTwoHopQuote } = getBestTwoHopQuote( | ||||
| @@ -523,8 +528,9 @@ export class MarketOperationUtils { | ||||
|                 sourceFlags: SOURCE_FLAGS[ERC20BridgeSource.MultiHop], | ||||
|                 marketSideLiquidity, | ||||
|                 adjustedRate: bestTwoHopRate, | ||||
|                 takerTokenToEthRate, | ||||
|                 makerTokenToEthRate, | ||||
|                 unoptimizedPath, | ||||
|                 takerAmountPerEth, | ||||
|                 makerAmountPerEth, | ||||
|             }; | ||||
|         } | ||||
|  | ||||
| @@ -557,8 +563,9 @@ export class MarketOperationUtils { | ||||
|             sourceFlags: collapsedPath.sourceFlags, | ||||
|             marketSideLiquidity, | ||||
|             adjustedRate: optimalPathRate, | ||||
|             takerTokenToEthRate, | ||||
|             makerTokenToEthRate, | ||||
|             unoptimizedPath, | ||||
|             takerAmountPerEth, | ||||
|             makerAmountPerEth, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -39,7 +39,7 @@ export function getBestTwoHopQuote( | ||||
|     feeSchedule?: FeeSchedule, | ||||
|     exchangeProxyOverhead?: ExchangeProxyOverhead, | ||||
| ): { quote: DexSample<MultiHopFillData> | undefined; adjustedRate: BigNumber } { | ||||
|     const { side, inputAmount, ethToOutputRate, quotes } = marketSideLiquidity; | ||||
|     const { side, inputAmount, outputAmountPerEth, quotes } = marketSideLiquidity; | ||||
|     const { twoHopQuotes } = quotes; | ||||
|     // 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, | ||||
| @@ -57,7 +57,7 @@ export function getBestTwoHopQuote( | ||||
|     } | ||||
|     const best = filteredQuotes | ||||
|         .map(quote => | ||||
|             getTwoHopAdjustedRate(side, quote, inputAmount, ethToOutputRate, feeSchedule, exchangeProxyOverhead), | ||||
|             getTwoHopAdjustedRate(side, quote, inputAmount, outputAmountPerEth, feeSchedule, exchangeProxyOverhead), | ||||
|         ) | ||||
|         .reduce( | ||||
|             (prev, curr, i) => | ||||
| @@ -67,7 +67,7 @@ export function getBestTwoHopQuote( | ||||
|                     side, | ||||
|                     filteredQuotes[0], | ||||
|                     inputAmount, | ||||
|                     ethToOutputRate, | ||||
|                     outputAmountPerEth, | ||||
|                     feeSchedule, | ||||
|                     exchangeProxyOverhead, | ||||
|                 ), | ||||
|   | ||||
| @@ -22,14 +22,14 @@ export interface PathSize { | ||||
| } | ||||
|  | ||||
| export interface PathPenaltyOpts { | ||||
|     ethToOutputRate: BigNumber; | ||||
|     ethToInputRate: BigNumber; | ||||
|     outputAmountPerEth: BigNumber; | ||||
|     inputAmountPerEth: BigNumber; | ||||
|     exchangeProxyOverhead: ExchangeProxyOverhead; | ||||
| } | ||||
|  | ||||
| export const DEFAULT_PATH_PENALTY_OPTS: PathPenaltyOpts = { | ||||
|     ethToOutputRate: ZERO_AMOUNT, | ||||
|     ethToInputRate: ZERO_AMOUNT, | ||||
|     outputAmountPerEth: ZERO_AMOUNT, | ||||
|     inputAmountPerEth: ZERO_AMOUNT, | ||||
|     exchangeProxyOverhead: () => ZERO_AMOUNT, | ||||
| }; | ||||
|  | ||||
| @@ -131,11 +131,11 @@ export class Path { | ||||
|  | ||||
|     public adjustedSize(): PathSize { | ||||
|         const { input, output } = this._adjustedSize; | ||||
|         const { exchangeProxyOverhead, ethToOutputRate, ethToInputRate } = this.pathPenaltyOpts; | ||||
|         const { exchangeProxyOverhead, outputAmountPerEth, inputAmountPerEth } = this.pathPenaltyOpts; | ||||
|         const gasOverhead = exchangeProxyOverhead(this.sourceFlags); | ||||
|         const pathPenalty = !ethToOutputRate.isZero() | ||||
|             ? ethToOutputRate.times(gasOverhead) | ||||
|             : ethToInputRate.times(gasOverhead).times(output.dividedToIntegerBy(input)); | ||||
|         const pathPenalty = !outputAmountPerEth.isZero() | ||||
|             ? outputAmountPerEth.times(gasOverhead) | ||||
|             : inputAmountPerEth.times(gasOverhead).times(output.dividedToIntegerBy(input)); | ||||
|         return { | ||||
|             input, | ||||
|             output: this.side === MarketOperation.Sell ? output.minus(pathPenalty) : output.plus(pathPenalty), | ||||
|   | ||||
| @@ -13,7 +13,7 @@ export function getTwoHopAdjustedRate( | ||||
|     side: MarketOperation, | ||||
|     twoHopQuote: DexSample<MultiHopFillData>, | ||||
|     targetInput: BigNumber, | ||||
|     ethToOutputRate: BigNumber, | ||||
|     outputAmountPerEth: BigNumber, | ||||
|     fees: FeeSchedule = {}, | ||||
|     exchangeProxyOverhead: ExchangeProxyOverhead = () => ZERO_AMOUNT, | ||||
| ): BigNumber { | ||||
| @@ -21,7 +21,7 @@ export function getTwoHopAdjustedRate( | ||||
|     if (input.isLessThan(targetInput) || output.isZero()) { | ||||
|         return ZERO_AMOUNT; | ||||
|     } | ||||
|     const penalty = ethToOutputRate.times( | ||||
|     const penalty = outputAmountPerEth.times( | ||||
|         exchangeProxyOverhead(SOURCE_FLAGS.MultiHop).plus(fees[ERC20BridgeSource.MultiHop]!(fillData)), | ||||
|     ); | ||||
|     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 { QuoteReport } from '../quote_report_generator'; | ||||
|  | ||||
| import { CollapsedPath } from './path'; | ||||
| import { SourceFilters } from './source_filters'; | ||||
|  | ||||
| /** | ||||
| @@ -374,8 +375,9 @@ export interface OptimizerResult { | ||||
|     liquidityDelivered: CollapsedFill[] | DexSample<MultiHopFillData>; | ||||
|     marketSideLiquidity: MarketSideLiquidity; | ||||
|     adjustedRate: BigNumber; | ||||
|     takerTokenToEthRate: BigNumber; | ||||
|     makerTokenToEthRate: BigNumber; | ||||
|     unoptimizedPath?: CollapsedPath; | ||||
|     takerAmountPerEth: BigNumber; | ||||
|     makerAmountPerEth: BigNumber; | ||||
| } | ||||
|  | ||||
| export interface OptimizerResultWithReport extends OptimizerResult { | ||||
| @@ -396,8 +398,8 @@ export interface MarketSideLiquidity { | ||||
|     inputAmount: BigNumber; | ||||
|     inputToken: string; | ||||
|     outputToken: string; | ||||
|     ethToOutputRate: BigNumber; | ||||
|     ethToInputRate: BigNumber; | ||||
|     outputAmountPerEth: BigNumber; | ||||
|     inputAmountPerEth: BigNumber; | ||||
|     quoteSourceFilters: SourceFilters; | ||||
|     makerTokenDecimals: number; | ||||
|     takerTokenDecimals: number; | ||||
|   | ||||
| @@ -49,8 +49,8 @@ const exchangeProxyOverhead = (sourceFlags: number) => { | ||||
|  | ||||
| const buyMarketSideLiquidity: MarketSideLiquidity = { | ||||
|     // needed params | ||||
|     ethToOutputRate: new BigNumber(500), | ||||
|     ethToInputRate: new BigNumber(1), | ||||
|     outputAmountPerEth: new BigNumber(500), | ||||
|     inputAmountPerEth: new BigNumber(1), | ||||
|     side: MarketOperation.Buy, | ||||
|     makerTokenDecimals: 18, | ||||
|     takerTokenDecimals: 18, | ||||
| @@ -70,8 +70,8 @@ const buyMarketSideLiquidity: MarketSideLiquidity = { | ||||
|  | ||||
| const sellMarketSideLiquidity: MarketSideLiquidity = { | ||||
|     // needed params | ||||
|     ethToOutputRate: new BigNumber(500), | ||||
|     ethToInputRate: new BigNumber(1), | ||||
|     outputAmountPerEth: new BigNumber(500), | ||||
|     inputAmountPerEth: new BigNumber(1), | ||||
|     side: MarketOperation.Sell, | ||||
|     makerTokenDecimals: 18, | ||||
|     takerTokenDecimals: 18, | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import { | ||||
|     decodeAffiliateFeeTransformerData, | ||||
|     decodeFillQuoteTransformerData, | ||||
|     decodePayTakerTransformerData, | ||||
|     decodePositiveSlippageFeeTransformerData, | ||||
|     decodeWethTransformerData, | ||||
|     ETH_TOKEN_ADDRESS, | ||||
|     FillQuoteTransformerLimitOrderInfo, | ||||
| @@ -17,9 +18,9 @@ import * as chai from 'chai'; | ||||
| import * as _ from 'lodash'; | ||||
| 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 { MarketBuySwapQuote, MarketOperation, MarketSellSwapQuote } from '../src/types'; | ||||
| import { AffiliateFeeType, MarketBuySwapQuote, MarketOperation, MarketSellSwapQuote } from '../src/types'; | ||||
| import { | ||||
|     ERC20BridgeSource, | ||||
|     OptimizedLimitOrder, | ||||
| @@ -53,6 +54,7 @@ describe('ExchangeProxySwapQuoteConsumer', () => { | ||||
|             payTakerTransformer: getTransformerAddress(TRANSFORMER_DEPLOYER, 2), | ||||
|             fillQuoteTransformer: getTransformerAddress(TRANSFORMER_DEPLOYER, 3), | ||||
|             affiliateFeeTransformer: getTransformerAddress(TRANSFORMER_DEPLOYER, 4), | ||||
|             positiveSlippageFeeTransformer: getTransformerAddress(TRANSFORMER_DEPLOYER, 5), | ||||
|         }, | ||||
|     }; | ||||
|     let consumer: ExchangeProxySwapQuoteConsumer; | ||||
| @@ -137,11 +139,11 @@ describe('ExchangeProxySwapQuoteConsumer', () => { | ||||
|                 protocolFeeInWeiAmount: getRandomAmount(), | ||||
|                 feeTakerTokenAmount: getRandomAmount(), | ||||
|             }, | ||||
|             makerAmountPerEth: getRandomInteger(1, 1e9), | ||||
|             takerAmountPerEth: getRandomInteger(1, 1e9), | ||||
|             ...(side === MarketOperation.Buy | ||||
|                 ? { type: MarketOperation.Buy, makerTokenFillAmount } | ||||
|                 : { type: MarketOperation.Sell, takerTokenFillAmount }), | ||||
|             takerTokenToEthRate: getRandomAmount(), | ||||
|             makerTokenToEthRate: getRandomAmount(), | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| @@ -336,6 +338,7 @@ describe('ExchangeProxySwapQuoteConsumer', () => { | ||||
|                 recipient: randomAddress(), | ||||
|                 buyTokenFeeAmount: getRandomAmount(), | ||||
|                 sellTokenFeeAmount: ZERO_AMOUNT, | ||||
|                 feeType: AffiliateFeeType.PercentageFee, | ||||
|             }; | ||||
|             const callInfo = await consumer.getCalldataOrThrowAsync(quote, { | ||||
|                 extensionContractOpts: { affiliateFee }, | ||||
| @@ -349,12 +352,42 @@ describe('ExchangeProxySwapQuoteConsumer', () => { | ||||
|                 { 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 () => { | ||||
|             const quote = getRandomSellQuote(); | ||||
|             const affiliateFee = { | ||||
|                 recipient: randomAddress(), | ||||
|                 buyTokenFeeAmount: ZERO_AMOUNT, | ||||
|                 sellTokenFeeAmount: getRandomAmount(), | ||||
|                 feeType: AffiliateFeeType.PercentageFee, | ||||
|             }; | ||||
|             expect( | ||||
|                 consumer.getCalldataOrThrowAsync(quote, { | ||||
|   | ||||
| @@ -756,8 +756,8 @@ describe('MarketOperationUtils tests', () => { | ||||
|                             inputAmount: Web3Wrapper.toBaseUnitAmount(1, 18), | ||||
|                             inputToken: MAKER_TOKEN, | ||||
|                             outputToken: TAKER_TOKEN, | ||||
|                             ethToInputRate: Web3Wrapper.toBaseUnitAmount(1, 18), | ||||
|                             ethToOutputRate: Web3Wrapper.toBaseUnitAmount(1, 6), | ||||
|                             inputAmountPerEth: Web3Wrapper.toBaseUnitAmount(1, 18), | ||||
|                             outputAmountPerEth: Web3Wrapper.toBaseUnitAmount(1, 6), | ||||
|                             quoteSourceFilters: new SourceFilters(), | ||||
|                             makerTokenDecimals: 6, | ||||
|                             takerTokenDecimals: 18, | ||||
| @@ -1787,7 +1787,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|  | ||||
|     describe('createFills', () => { | ||||
|         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 | ||||
|         const smallOrder: NativeOrderWithFillableAmounts = { | ||||
|             order: { | ||||
| @@ -1830,7 +1830,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 orders, | ||||
|                 dexQuotes: [], | ||||
|                 targetInput: takerAmount.minus(1), | ||||
|                 ethToOutputRate, | ||||
|                 outputAmountPerEth, | ||||
|                 feeSchedule, | ||||
|             }); | ||||
|             expect((path[0][0].fillData as NativeFillData).order.maker).to.eq(smallOrder.order.maker); | ||||
| @@ -1843,7 +1843,7 @@ describe('MarketOperationUtils tests', () => { | ||||
|                 orders, | ||||
|                 dexQuotes: [], | ||||
|                 targetInput: POSITIVE_INF, | ||||
|                 ethToOutputRate, | ||||
|                 outputAmountPerEth, | ||||
|                 feeSchedule, | ||||
|             }); | ||||
|             expect((path[0][0].fillData as NativeFillData).order.maker).to.eq(largeOrder.order.maker); | ||||
|   | ||||
| @@ -39,8 +39,8 @@ export async function getFullyFillableSwapQuoteWithNoFeesAsync( | ||||
|         worstCaseQuoteInfo: quoteInfo, | ||||
|         sourceBreakdown: breakdown, | ||||
|         isTwoHop: false, | ||||
|         takerTokenToEthRate: constants.ZERO_AMOUNT, | ||||
|         makerTokenToEthRate: constants.ZERO_AMOUNT, | ||||
|         takerAmountPerEth: constants.ZERO_AMOUNT, | ||||
|         makerAmountPerEth: constants.ZERO_AMOUNT, | ||||
|         makerTokenDecimals: 18, | ||||
|         takerTokenDecimals: 18, | ||||
|     }; | ||||
|   | ||||
| @@ -5,6 +5,10 @@ | ||||
|             { | ||||
|                 "note": "Deploy new FQT", | ||||
|                 "pr": 155 | ||||
|             }, | ||||
|             { | ||||
|                 "note": "Deploy new `PositiveSlippageFeeTransformer`", | ||||
|                 "pr": 101 | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|   | ||||
| @@ -37,7 +37,8 @@ | ||||
|             "wethTransformer": "0xb2bc06a4efb20fc6553a69dbfa49b7be938034a7", | ||||
|             "payTakerTransformer": "0x4638a7ebe75b911b995d0ec73a81e4f85f41f24e", | ||||
|             "affiliateFeeTransformer": "0xda6d9fc5998f550a094585cf9171f0e8ee3ac59f", | ||||
|             "fillQuoteTransformer": "0x227e767a9b7517681d1cb6b846aa9e541484c7ab" | ||||
|             "fillQuoteTransformer": "0x227e767a9b7517681d1cb6b846aa9e541484c7ab", | ||||
|             "positiveSlippageFeeTransformer": "0xa9416ce1dbde8d331210c07b5c253d94ee4cc3fd" | ||||
|         } | ||||
|     }, | ||||
|     "3": { | ||||
| @@ -78,7 +79,8 @@ | ||||
|             "wethTransformer": "0x05ad19aa3826e0609a19568ffbd1dfe86c6c7184", | ||||
|             "payTakerTransformer": "0x6d0ebf2bcd9cc93ec553b60ad201943dcca4e291", | ||||
|             "affiliateFeeTransformer": "0x6588256778ca4432fa43983ac685c45efb2379e2", | ||||
|             "fillQuoteTransformer": "0x2088a820787ebbe937a0612ef024f1e1d65f9784" | ||||
|             "fillQuoteTransformer": "0x2088a820787ebbe937a0612ef024f1e1d65f9784", | ||||
|             "positiveSlippageFeeTransformer": "0x8b332f700fd37e71c5c5b26c4d78b5ca63dd33b2" | ||||
|         } | ||||
|     }, | ||||
|     "4": { | ||||
| @@ -119,7 +121,8 @@ | ||||
|             "wethTransformer": "0x8d822fe2b42f60531203e288f5f357fa79474437", | ||||
|             "payTakerTransformer": "0x150652244723102faeaefa4c79597d097ffa26c6", | ||||
|             "affiliateFeeTransformer": "0xa39b40642e8e00435857a0fe7d0655e08cc2217e", | ||||
|             "fillQuoteTransformer": "0x3fb85e0c1e9e0ba4ba9a4072442a2540c0473db1" | ||||
|             "fillQuoteTransformer": "0x3fb85e0c1e9e0ba4ba9a4072442a2540c0473db1", | ||||
|             "positiveSlippageFeeTransformer": "0x0000000000000000000000000000000000000000" | ||||
|         } | ||||
|     }, | ||||
|     "42": { | ||||
| @@ -160,7 +163,8 @@ | ||||
|             "wethTransformer": "0x9ce35b5ee9e710535e3988e3f8731d9ca9dba17d", | ||||
|             "payTakerTransformer": "0x5a53e7b02a83aa9f60ccf4e424f0442c255bc977", | ||||
|             "affiliateFeeTransformer": "0x870893920a96a28d4b63c0a7d06a521e3bd074b3", | ||||
|             "fillQuoteTransformer": "0x8d2d732e5fe6d4d6d5e715200b84dfa69fb05478" | ||||
|             "fillQuoteTransformer": "0x8d2d732e5fe6d4d6d5e715200b84dfa69fb05478", | ||||
|             "positiveSlippageFeeTransformer": "0x0000000000000000000000000000000000000000" | ||||
|         } | ||||
|     }, | ||||
|     "1337": { | ||||
| @@ -201,7 +205,8 @@ | ||||
|             "wethTransformer": "0x7209185959d7227fb77274e1e88151d7c4c368d3", | ||||
|             "payTakerTransformer": "0x3f16ca81691dab9184cb4606c361d73c4fd2510a", | ||||
|             "affiliateFeeTransformer": "0x99356167edba8fbdc36959e3f5d0c43d1ba9c6db", | ||||
|             "fillQuoteTransformer": "0x45b3a72221e571017c0f0ec42189e11d149d0ace" | ||||
|             "fillQuoteTransformer": "0x45b3a72221e571017c0f0ec42189e11d149d0ace", | ||||
|             "positiveSlippageFeeTransformer": "0xdd66c23e07b4d6925b6089b5fe6fc9e62941afe8" | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -39,6 +39,7 @@ export interface ContractAddresses { | ||||
|         payTakerTransformer: string; | ||||
|         fillQuoteTransformer: string; | ||||
|         affiliateFeeTransformer: string; | ||||
|         positiveSlippageFeeTransformer: string; | ||||
|     }; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -32,6 +32,7 @@ import { | ||||
|     FillQuoteTransformerContract, | ||||
|     fullMigrateAsync as fullMigrateExchangeProxyAsync, | ||||
|     PayTakerTransformerContract, | ||||
|     PositiveSlippageFeeTransformerContract, | ||||
|     WethTransformerContract, | ||||
| } from '@0x/contracts-zero-ex'; | ||||
| import { Web3ProviderEngine } from '@0x/subproviders'; | ||||
| @@ -345,7 +346,12 @@ export async function runMigrationsAsync( | ||||
|         bridgeAdapter.address, | ||||
|         exchangeProxy.address, | ||||
|     ); | ||||
|  | ||||
|     const positiveSlippageFeeTransformer = await PositiveSlippageFeeTransformerContract.deployFrom0xArtifactAsync( | ||||
|         exchangeProxyArtifacts.PositiveSlippageFeeTransformer, | ||||
|         provider, | ||||
|         txDefaults, | ||||
|         allArtifacts, | ||||
|     ); | ||||
|     const contractAddresses = { | ||||
|         erc20Proxy: erc20Proxy.address, | ||||
|         erc721Proxy: erc721Proxy.address, | ||||
| @@ -385,6 +391,7 @@ export async function runMigrationsAsync( | ||||
|             payTakerTransformer: payTakerTransformer.address, | ||||
|             fillQuoteTransformer: fillQuoteTransformer.address, | ||||
|             affiliateFeeTransformer: affiliateFeeTransformer.address, | ||||
|             positiveSlippageFeeTransformer: positiveSlippageFeeTransformer.address, | ||||
|         }, | ||||
|     }; | ||||
|     return contractAddresses; | ||||
|   | ||||
| @@ -77,6 +77,9 @@ export { | ||||
|     AffiliateFeeTransformerData, | ||||
|     encodeAffiliateFeeTransformerData, | ||||
|     decodeAffiliateFeeTransformerData, | ||||
|     PositiveSlippageFeeTransformerData, | ||||
|     encodePositiveSlippageFeeTransformerData, | ||||
|     decodePositiveSlippageFeeTransformerData, | ||||
|     findTransformerNonce, | ||||
|     getTransformerAddress, | ||||
| } 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({ | ||||
|     name: 'data', | ||||
| @@ -195,6 +195,42 @@ export function decodeAffiliateFeeTransformerData(encoded: string): AffiliateFee | ||||
|     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. | ||||
|  * 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({ | ||||
|     name: 'data', | ||||
| @@ -317,3 +317,39 @@ export function getTransformerAddress(deployer: string, nonce: number): string { | ||||
|         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