diff --git a/contracts/zero-ex/CHANGELOG.json b/contracts/zero-ex/CHANGELOG.json index d63bbe7f68..4d8051b8ca 100644 --- a/contracts/zero-ex/CHANGELOG.json +++ b/contracts/zero-ex/CHANGELOG.json @@ -21,6 +21,10 @@ { "note": "refund ETH with no gas limit in FQT", "pr": 155 + }, + { + "note": "Added an opt-in `PositiveSlippageAffiliateFee`", + "pr": 101 } ] }, diff --git a/contracts/zero-ex/contracts/src/transformers/PositiveSlippageFeeTransformer.sol b/contracts/zero-ex/contracts/src/transformers/PositiveSlippageFeeTransformer.sol new file mode 100644 index 0000000000..9ab087754b --- /dev/null +++ b/contracts/zero-ex/contracts/src/transformers/PositiveSlippageFeeTransformer.sol @@ -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; + } +} diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index 8d34cc6828..69891b899c 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -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", diff --git a/contracts/zero-ex/src/artifacts.ts b/contracts/zero-ex/src/artifacts.ts index c0455e1253..033dfdfe4a 100644 --- a/contracts/zero-ex/src/artifacts.ts +++ b/contracts/zero-ex/src/artifacts.ts @@ -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, diff --git a/contracts/zero-ex/src/index.ts b/contracts/zero-ex/src/index.ts index 0cb9288c63..f8cf1eff4c 100644 --- a/contracts/zero-ex/src/index.ts +++ b/contracts/zero-ex/src/index.ts @@ -46,6 +46,7 @@ export { IZeroExContract, LogMetadataTransformerContract, PayTakerTransformerContract, + PositiveSlippageFeeTransformerContract, WethTransformerContract, ZeroExContract, } from './wrappers'; diff --git a/contracts/zero-ex/src/wrappers.ts b/contracts/zero-ex/src/wrappers.ts index 79048e1404..649ecfb7d0 100644 --- a/contracts/zero-ex/src/wrappers.ts +++ b/contracts/zero-ex/src/wrappers.ts @@ -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'; diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index e98c46aec1..fc628ab481 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -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, diff --git a/contracts/zero-ex/test/transformers/positive_slippage_fee_transformer_test.ts b/contracts/zero-ex/test/transformers/positive_slippage_fee_transformer_test.ts new file mode 100644 index 0000000000..9ce122bebf --- /dev/null +++ b/contracts/zero-ex/test/transformers/positive_slippage_fee_transformer_test.ts @@ -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 { + return { + ethBalance: await env.web3Wrapper.getBalanceInWeiAsync(owner), + tokenBalance: await token.balanceOf(owner).callAsync(), + }; + } + + async function mintHostTokensAsync(amount: BigNumber): Promise { + await token.mint(host.address, amount).awaitTransactionSuccessAsync(); + } + + 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, + }); + }); +}); diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index 4f97656f06..2d12b48462 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -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'; diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index ab85835a51..7cb1e96cd5 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -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", diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index 081ba314d0..e0107326e6 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -49,6 +49,10 @@ { "note": "Add an alternative RFQ market making implementation", "pr": 139 + }, + { + "note": "Added an opt-in `PositiveSlippageAffiliateFee`", + "pr": 101 } ] }, diff --git a/packages/asset-swapper/src/constants.ts b/packages/asset-swapper/src/constants.ts index 524affb5cd..0efda7bea5 100644 --- a/packages/asset-swapper/src/constants.ts +++ b/packages/asset-swapper/src/constants.ts @@ -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, diff --git a/packages/asset-swapper/src/index.ts b/packages/asset-swapper/src/index.ts index 9797aecd97..1e9f326178 100644 --- a/packages/asset-swapper/src/index.ts +++ b/packages/asset-swapper/src/index.ts @@ -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, diff --git a/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts b/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts index 7e21e60b8f..6768c0c541 100644 --- a/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts +++ b/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts @@ -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,25 +271,52 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { }); } - // This transformer pays affiliate fees. - if (buyTokenFeeAmount.isGreaterThan(0) && feeRecipient !== NULL_ADDRESS) { + 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.affiliateFeeTransformer, - data: encodeAffiliateFeeTransformerData({ - fees: [ - { - token: isToETH ? ETH_TOKEN_ADDRESS : buyToken, - amount: buyTokenFeeAmount, - recipient: feeRecipient, - }, - ], + deploymentNonce: this.transformerNonces.positiveSlippageFeeTransformer, + data: encodePositiveSlippageFeeTransformerData({ + token: isToETH ? ETH_TOKEN_ADDRESS : buyToken, + bestCaseAmount: BigNumber.max(bestCaseAmountWithSurplus, quote.bestCaseQuoteInfo.makerAmount), + recipient: feeRecipient, }), }); - // Adjust the minimum buy amount by the fee. - minBuyAmount = BigNumber.max(0, minBuyAmount.minus(buyTokenFeeAmount)); - } - if (sellTokenFeeAmount.isGreaterThan(0) && feeRecipient !== NULL_ADDRESS) { - throw new Error('Affiliate fees denominated in sell token are not yet supported'); + // 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)) { + 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. @@ -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; diff --git a/packages/asset-swapper/src/swap_quoter.ts b/packages/asset-swapper/src/swap_quoter.ts index a4acc64b84..6518a4ed93 100644 --- a/packages/asset-swapper/src/swap_quoter.ts +++ b/packages/asset-swapper/src/swap_quoter.ts @@ -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, }; diff --git a/packages/asset-swapper/src/types.ts b/packages/asset-swapper/src/types.ts index f4f0d38ec7..ea4657ab5f 100644 --- a/packages/asset-swapper/src/types.ts +++ b/packages/asset-swapper/src/types.ts @@ -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; } /** diff --git a/packages/asset-swapper/src/utils/market_operation_utils/comparison_price.ts b/packages/asset-swapper/src/utils/market_operation_utils/comparison_price.ts index fa12f78fb2..a88c6ed756 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/comparison_price.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/comparison_price.ts @@ -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)); diff --git a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts index 5b8d7ac8cb..8ce3acf478 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts @@ -645,6 +645,8 @@ export const DEFAULT_GAS_SCHEDULE: Required = { export const DEFAULT_FEE_SCHEDULE: Required = { ...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 = { diff --git a/packages/asset-swapper/src/utils/market_operation_utils/fills.ts b/packages/asset-swapper/src/utils/market_operation_utils/fills.ts index 74a4515b9a..e097d0b08a 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/fills.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/fills.ts @@ -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); diff --git a/packages/asset-swapper/src/utils/market_operation_utils/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/index.ts index fd1b905285..c9a473e6ed 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -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, }; } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/multihop_utils.ts b/packages/asset-swapper/src/utils/market_operation_utils/multihop_utils.ts index 6b064fd159..5f8bf98465 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/multihop_utils.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/multihop_utils.ts @@ -39,7 +39,7 @@ export function getBestTwoHopQuote( feeSchedule?: FeeSchedule, exchangeProxyOverhead?: ExchangeProxyOverhead, ): { quote: DexSample | 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, ), diff --git a/packages/asset-swapper/src/utils/market_operation_utils/path.ts b/packages/asset-swapper/src/utils/market_operation_utils/path.ts index 5788c0a73b..11182eb07a 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/path.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/path.ts @@ -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), diff --git a/packages/asset-swapper/src/utils/market_operation_utils/rate_utils.ts b/packages/asset-swapper/src/utils/market_operation_utils/rate_utils.ts index 43d007b623..bc2a0207b1 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/rate_utils.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/rate_utils.ts @@ -13,7 +13,7 @@ export function getTwoHopAdjustedRate( side: MarketOperation, twoHopQuote: DexSample, 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); diff --git a/packages/asset-swapper/src/utils/market_operation_utils/types.ts b/packages/asset-swapper/src/utils/market_operation_utils/types.ts index 9266b5c8ab..1008b63650 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/types.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/types.ts @@ -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; 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; diff --git a/packages/asset-swapper/test/comparison_price_test.ts b/packages/asset-swapper/test/comparison_price_test.ts index de143c42bc..fe490cb5fa 100644 --- a/packages/asset-swapper/test/comparison_price_test.ts +++ b/packages/asset-swapper/test/comparison_price_test.ts @@ -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, diff --git a/packages/asset-swapper/test/exchange_proxy_swap_quote_consumer_test.ts b/packages/asset-swapper/test/exchange_proxy_swap_quote_consumer_test.ts index 120cca401a..62a2c337ea 100644 --- a/packages/asset-swapper/test/exchange_proxy_swap_quote_consumer_test.ts +++ b/packages/asset-swapper/test/exchange_proxy_swap_quote_consumer_test.ts @@ -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, { diff --git a/packages/asset-swapper/test/market_operation_utils_test.ts b/packages/asset-swapper/test/market_operation_utils_test.ts index 5a0f7490a9..9d1cf34c41 100644 --- a/packages/asset-swapper/test/market_operation_utils_test.ts +++ b/packages/asset-swapper/test/market_operation_utils_test.ts @@ -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); diff --git a/packages/asset-swapper/test/utils/swap_quote.ts b/packages/asset-swapper/test/utils/swap_quote.ts index 6e0d174d12..ac5edc8432 100644 --- a/packages/asset-swapper/test/utils/swap_quote.ts +++ b/packages/asset-swapper/test/utils/swap_quote.ts @@ -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, }; diff --git a/packages/contract-addresses/CHANGELOG.json b/packages/contract-addresses/CHANGELOG.json index 3e2cb9d41b..b706f4b475 100644 --- a/packages/contract-addresses/CHANGELOG.json +++ b/packages/contract-addresses/CHANGELOG.json @@ -5,6 +5,10 @@ { "note": "Deploy new FQT", "pr": 155 + }, + { + "note": "Deploy new `PositiveSlippageFeeTransformer`", + "pr": 101 } ] }, diff --git a/packages/contract-addresses/addresses.json b/packages/contract-addresses/addresses.json index 5035e9dab7..b78f7860d3 100644 --- a/packages/contract-addresses/addresses.json +++ b/packages/contract-addresses/addresses.json @@ -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" } } } diff --git a/packages/contract-addresses/src/index.ts b/packages/contract-addresses/src/index.ts index 5ce4b0d738..dc8f07e8ca 100644 --- a/packages/contract-addresses/src/index.ts +++ b/packages/contract-addresses/src/index.ts @@ -39,6 +39,7 @@ export interface ContractAddresses { payTakerTransformer: string; fillQuoteTransformer: string; affiliateFeeTransformer: string; + positiveSlippageFeeTransformer: string; }; } diff --git a/packages/migrations/src/migration.ts b/packages/migrations/src/migration.ts index 886380448c..b9d54c64ec 100644 --- a/packages/migrations/src/migration.ts +++ b/packages/migrations/src/migration.ts @@ -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; diff --git a/packages/order-utils/src/index.ts b/packages/order-utils/src/index.ts index 6a54e31604..d8f1e458d5 100644 --- a/packages/order-utils/src/index.ts +++ b/packages/order-utils/src/index.ts @@ -77,6 +77,9 @@ export { AffiliateFeeTransformerData, encodeAffiliateFeeTransformerData, decodeAffiliateFeeTransformerData, + PositiveSlippageFeeTransformerData, + encodePositiveSlippageFeeTransformerData, + decodePositiveSlippageFeeTransformerData, findTransformerNonce, getTransformerAddress, } from './transformer_utils'; diff --git a/packages/order-utils/src/transformer_utils.ts b/packages/order-utils/src/transformer_utils.ts index 9bdd93b67b..60ad6f00fc 100644 --- a/packages/order-utils/src/transformer_utils.ts +++ b/packages/order-utils/src/transformer_utils.ts @@ -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. diff --git a/packages/protocol-utils/src/transformer_utils.ts b/packages/protocol-utils/src/transformer_utils.ts index febad1284e..c9aa84e85f 100644 --- a/packages/protocol-utils/src/transformer_utils.ts +++ b/packages/protocol-utils/src/transformer_utils.ts @@ -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); +}