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:
Romain Butteaud 2021-02-24 03:51:58 +01:00 committed by GitHub
parent 5b8bbc34e8
commit f98609686d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 528 additions and 119 deletions

View File

@ -21,6 +21,10 @@
{ {
"note": "refund ETH with no gas limit in FQT", "note": "refund ETH with no gas limit in FQT",
"pr": 155 "pr": 155
},
{
"note": "Added an opt-in `PositiveSlippageAffiliateFee`",
"pr": 101
} }
] ]
}, },

View File

@ -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;
}
}

View File

@ -41,9 +41,9 @@
"rollback": "node ./lib/scripts/rollback.js" "rollback": "node ./lib/scripts/rollback.js"
}, },
"config": { "config": {
"publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature,ILiquidityProviderFeature,NativeOrdersFeature,INativeOrdersFeature,FeeCollectorController,FeeCollector,CurveLiquidityProvider", "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,PositiveSlippageFeeTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature,ILiquidityProviderFeature,NativeOrdersFeature,INativeOrdersFeature,FeeCollectorController,FeeCollector,CurveLiquidityProvider",
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
"abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|BridgeSource|CurveLiquidityProvider|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IFeature|IFlashWallet|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|INativeOrdersFeature|IOwnableFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinBalancer|MixinBancor|MixinCoFiX|MixinCryptoCom|MixinCurve|MixinDodo|MixinDodoV2|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinShell|MixinSushiswap|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|NativeOrdersFeature|OwnableFeature|PayTakerTransformer|PermissionlessTransformerDeployer|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestCurve|TestDelegateCaller|TestFeeCollectorController|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFixinProtocolFees|TestFixinTokenSpender|TestFullMigration|TestInitialMigration|TestLibNativeOrder|TestLibSignature|TestLiquidityProvider|TestMetaTransactionsNativeOrdersFeature|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestNativeOrdersFeature|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx|ZeroExOptimized).json" "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|BridgeSource|CurveLiquidityProvider|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IFeature|IFlashWallet|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|INativeOrdersFeature|IOwnableFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinBalancer|MixinBancor|MixinCoFiX|MixinCryptoCom|MixinCurve|MixinDodo|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinShell|MixinSushiswap|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|NativeOrdersFeature|OwnableFeature|PayTakerTransformer|PermissionlessTransformerDeployer|PositiveSlippageFeeTransformer|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestCurve|TestDelegateCaller|TestFeeCollectorController|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFixinProtocolFees|TestFixinTokenSpender|TestFullMigration|TestInitialMigration|TestLibNativeOrder|TestLibSignature|TestLiquidityProvider|TestMetaTransactionsNativeOrdersFeature|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestNativeOrdersFeature|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx|ZeroExOptimized).json"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -29,6 +29,7 @@ import * as MetaTransactionsFeature from '../generated-artifacts/MetaTransaction
import * as NativeOrdersFeature from '../generated-artifacts/NativeOrdersFeature.json'; import * as NativeOrdersFeature from '../generated-artifacts/NativeOrdersFeature.json';
import * as OwnableFeature from '../generated-artifacts/OwnableFeature.json'; import * as OwnableFeature from '../generated-artifacts/OwnableFeature.json';
import * as PayTakerTransformer from '../generated-artifacts/PayTakerTransformer.json'; import * as PayTakerTransformer from '../generated-artifacts/PayTakerTransformer.json';
import * as PositiveSlippageFeeTransformer from '../generated-artifacts/PositiveSlippageFeeTransformer.json';
import * as SimpleFunctionRegistryFeature from '../generated-artifacts/SimpleFunctionRegistryFeature.json'; import * as SimpleFunctionRegistryFeature from '../generated-artifacts/SimpleFunctionRegistryFeature.json';
import * as TokenSpenderFeature from '../generated-artifacts/TokenSpenderFeature.json'; import * as TokenSpenderFeature from '../generated-artifacts/TokenSpenderFeature.json';
import * as TransformERC20Feature from '../generated-artifacts/TransformERC20Feature.json'; import * as TransformERC20Feature from '../generated-artifacts/TransformERC20Feature.json';
@ -48,6 +49,7 @@ export const artifacts = {
ITransformERC20Feature: ITransformERC20Feature as ContractArtifact, ITransformERC20Feature: ITransformERC20Feature as ContractArtifact,
FillQuoteTransformer: FillQuoteTransformer as ContractArtifact, FillQuoteTransformer: FillQuoteTransformer as ContractArtifact,
PayTakerTransformer: PayTakerTransformer as ContractArtifact, PayTakerTransformer: PayTakerTransformer as ContractArtifact,
PositiveSlippageFeeTransformer: PositiveSlippageFeeTransformer as ContractArtifact,
WethTransformer: WethTransformer as ContractArtifact, WethTransformer: WethTransformer as ContractArtifact,
OwnableFeature: OwnableFeature as ContractArtifact, OwnableFeature: OwnableFeature as ContractArtifact,
SimpleFunctionRegistryFeature: SimpleFunctionRegistryFeature as ContractArtifact, SimpleFunctionRegistryFeature: SimpleFunctionRegistryFeature as ContractArtifact,

View File

@ -46,6 +46,7 @@ export {
IZeroExContract, IZeroExContract,
LogMetadataTransformerContract, LogMetadataTransformerContract,
PayTakerTransformerContract, PayTakerTransformerContract,
PositiveSlippageFeeTransformerContract,
WethTransformerContract, WethTransformerContract,
ZeroExContract, ZeroExContract,
} from './wrappers'; } from './wrappers';

View File

@ -27,6 +27,7 @@ export * from '../generated-wrappers/meta_transactions_feature';
export * from '../generated-wrappers/native_orders_feature'; export * from '../generated-wrappers/native_orders_feature';
export * from '../generated-wrappers/ownable_feature'; export * from '../generated-wrappers/ownable_feature';
export * from '../generated-wrappers/pay_taker_transformer'; export * from '../generated-wrappers/pay_taker_transformer';
export * from '../generated-wrappers/positive_slippage_fee_transformer';
export * from '../generated-wrappers/simple_function_registry_feature'; export * from '../generated-wrappers/simple_function_registry_feature';
export * from '../generated-wrappers/token_spender_feature'; export * from '../generated-wrappers/token_spender_feature';
export * from '../generated-wrappers/transform_erc20_feature'; export * from '../generated-wrappers/transform_erc20_feature';

View File

@ -92,6 +92,7 @@ import * as NativeOrdersFeature from '../test/generated-artifacts/NativeOrdersFe
import * as OwnableFeature from '../test/generated-artifacts/OwnableFeature.json'; import * as OwnableFeature from '../test/generated-artifacts/OwnableFeature.json';
import * as PayTakerTransformer from '../test/generated-artifacts/PayTakerTransformer.json'; import * as PayTakerTransformer from '../test/generated-artifacts/PayTakerTransformer.json';
import * as PermissionlessTransformerDeployer from '../test/generated-artifacts/PermissionlessTransformerDeployer.json'; import * as PermissionlessTransformerDeployer from '../test/generated-artifacts/PermissionlessTransformerDeployer.json';
import * as PositiveSlippageFeeTransformer from '../test/generated-artifacts/PositiveSlippageFeeTransformer.json';
import * as SimpleFunctionRegistryFeature from '../test/generated-artifacts/SimpleFunctionRegistryFeature.json'; import * as SimpleFunctionRegistryFeature from '../test/generated-artifacts/SimpleFunctionRegistryFeature.json';
import * as TestBridge from '../test/generated-artifacts/TestBridge.json'; import * as TestBridge from '../test/generated-artifacts/TestBridge.json';
import * as TestCallTarget from '../test/generated-artifacts/TestCallTarget.json'; import * as TestCallTarget from '../test/generated-artifacts/TestCallTarget.json';
@ -209,6 +210,7 @@ export const artifacts = {
LibERC20Transformer: LibERC20Transformer as ContractArtifact, LibERC20Transformer: LibERC20Transformer as ContractArtifact,
LogMetadataTransformer: LogMetadataTransformer as ContractArtifact, LogMetadataTransformer: LogMetadataTransformer as ContractArtifact,
PayTakerTransformer: PayTakerTransformer as ContractArtifact, PayTakerTransformer: PayTakerTransformer as ContractArtifact,
PositiveSlippageFeeTransformer: PositiveSlippageFeeTransformer as ContractArtifact,
Transformer: Transformer as ContractArtifact, Transformer: Transformer as ContractArtifact,
WethTransformer: WethTransformer as ContractArtifact, WethTransformer: WethTransformer as ContractArtifact,
BridgeAdapter: BridgeAdapter as ContractArtifact, BridgeAdapter: BridgeAdapter as ContractArtifact,

View File

@ -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,
});
});
});

View File

@ -90,6 +90,7 @@ export * from '../test/generated-wrappers/native_orders_feature';
export * from '../test/generated-wrappers/ownable_feature'; export * from '../test/generated-wrappers/ownable_feature';
export * from '../test/generated-wrappers/pay_taker_transformer'; export * from '../test/generated-wrappers/pay_taker_transformer';
export * from '../test/generated-wrappers/permissionless_transformer_deployer'; export * from '../test/generated-wrappers/permissionless_transformer_deployer';
export * from '../test/generated-wrappers/positive_slippage_fee_transformer';
export * from '../test/generated-wrappers/simple_function_registry_feature'; export * from '../test/generated-wrappers/simple_function_registry_feature';
export * from '../test/generated-wrappers/test_bridge'; export * from '../test/generated-wrappers/test_bridge';
export * from '../test/generated-wrappers/test_call_target'; export * from '../test/generated-wrappers/test_call_target';

View File

@ -27,6 +27,7 @@
"generated-artifacts/NativeOrdersFeature.json", "generated-artifacts/NativeOrdersFeature.json",
"generated-artifacts/OwnableFeature.json", "generated-artifacts/OwnableFeature.json",
"generated-artifacts/PayTakerTransformer.json", "generated-artifacts/PayTakerTransformer.json",
"generated-artifacts/PositiveSlippageFeeTransformer.json",
"generated-artifacts/SimpleFunctionRegistryFeature.json", "generated-artifacts/SimpleFunctionRegistryFeature.json",
"generated-artifacts/TokenSpenderFeature.json", "generated-artifacts/TokenSpenderFeature.json",
"generated-artifacts/TransformERC20Feature.json", "generated-artifacts/TransformERC20Feature.json",
@ -119,6 +120,7 @@
"test/generated-artifacts/OwnableFeature.json", "test/generated-artifacts/OwnableFeature.json",
"test/generated-artifacts/PayTakerTransformer.json", "test/generated-artifacts/PayTakerTransformer.json",
"test/generated-artifacts/PermissionlessTransformerDeployer.json", "test/generated-artifacts/PermissionlessTransformerDeployer.json",
"test/generated-artifacts/PositiveSlippageFeeTransformer.json",
"test/generated-artifacts/SimpleFunctionRegistryFeature.json", "test/generated-artifacts/SimpleFunctionRegistryFeature.json",
"test/generated-artifacts/TestBridge.json", "test/generated-artifacts/TestBridge.json",
"test/generated-artifacts/TestCallTarget.json", "test/generated-artifacts/TestCallTarget.json",

View File

@ -49,6 +49,10 @@
{ {
"note": "Add an alternative RFQ market making implementation", "note": "Add an alternative RFQ market making implementation",
"pr": 139 "pr": 139
},
{
"note": "Added an opt-in `PositiveSlippageAffiliateFee`",
"pr": 101
} }
] ]
}, },

View File

@ -3,6 +3,7 @@ import { SignatureType } from '@0x/protocol-utils';
import { BigNumber, logUtils } from '@0x/utils'; import { BigNumber, logUtils } from '@0x/utils';
import { import {
AffiliateFeeType,
ExchangeProxyContractOpts, ExchangeProxyContractOpts,
LogFunction, LogFunction,
OrderPrunerOpts, OrderPrunerOpts,
@ -12,7 +13,11 @@ import {
SwapQuoteRequestOpts, SwapQuoteRequestOpts,
SwapQuoterOpts, SwapQuoterOpts,
} from './types'; } from './types';
import { DEFAULT_GET_MARKET_ORDERS_OPTS, TOKENS } from './utils/market_operation_utils/constants'; import {
DEFAULT_GET_MARKET_ORDERS_OPTS,
DEFAULT_INTERMEDIATE_TOKENS,
DEFAULT_TOKEN_ADJACENCY_GRAPH,
} from './utils/market_operation_utils/constants';
const ETH_GAS_STATION_API_URL = 'https://ethgasstation.info/api/ethgasAPI.json'; const ETH_GAS_STATION_API_URL = 'https://ethgasstation.info/api/ethgasAPI.json';
const NULL_BYTES = '0x'; const NULL_BYTES = '0x';
@ -38,7 +43,6 @@ const PROTOCOL_FEE_MULTIPLIER = new BigNumber(70000);
// default 50% buffer for selecting native orders to be aggregated with other sources // default 50% buffer for selecting native orders to be aggregated with other sources
const MARKET_UTILS_AMOUNT_BUFFER_PERCENTAGE = 0.5; const MARKET_UTILS_AMOUNT_BUFFER_PERCENTAGE = 0.5;
const DEFAULT_INTERMEDIATE_TOKENS = [TOKENS.WETH, TOKENS.USDT, TOKENS.DAI, TOKENS.USDC];
const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = { const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = {
chainId: ChainId.Mainnet, chainId: ChainId.Mainnet,
orderRefreshIntervalMs: 10000, // 10 seconds orderRefreshIntervalMs: 10000, // 10 seconds
@ -49,12 +53,14 @@ const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = {
takerApiKeyWhitelist: [], takerApiKeyWhitelist: [],
makerAssetOfferings: {}, makerAssetOfferings: {},
}, },
tokenAdjacencyGraph: DEFAULT_TOKEN_ADJACENCY_GRAPH,
}; };
const DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS: ExchangeProxyContractOpts = { const DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS: ExchangeProxyContractOpts = {
isFromETH: false, isFromETH: false,
isToETH: false, isToETH: false,
affiliateFee: { affiliateFee: {
feeType: AffiliateFeeType.None,
recipient: NULL_ADDRESS, recipient: NULL_ADDRESS,
buyTokenFeeAmount: ZERO_AMOUNT, buyTokenFeeAmount: ZERO_AMOUNT,
sellTokenFeeAmount: ZERO_AMOUNT, sellTokenFeeAmount: ZERO_AMOUNT,
@ -86,9 +92,12 @@ export const INVALID_SIGNATURE = { signatureType: SignatureType.Invalid, v: 1, r
export { DEFAULT_FEE_SCHEDULE, DEFAULT_GAS_SCHEDULE } from './utils/market_operation_utils/constants'; export { DEFAULT_FEE_SCHEDULE, DEFAULT_GAS_SCHEDULE } from './utils/market_operation_utils/constants';
export const POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS = new BigNumber(30000);
export const constants = { export const constants = {
ETH_GAS_STATION_API_URL, ETH_GAS_STATION_API_URL,
PROTOCOL_FEE_MULTIPLIER, PROTOCOL_FEE_MULTIPLIER,
POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS,
NULL_BYTES, NULL_BYTES,
ZERO_AMOUNT, ZERO_AMOUNT,
NULL_ADDRESS, NULL_ADDRESS,

View File

@ -74,9 +74,10 @@ export { InsufficientAssetLiquidityError } from './errors';
export { SwapQuoteConsumer } from './quote_consumers/swap_quote_consumer'; export { SwapQuoteConsumer } from './quote_consumers/swap_quote_consumer';
export { SwapQuoter, Orderbook } from './swap_quoter'; export { SwapQuoter, Orderbook } from './swap_quoter';
export { export {
AffiliateFee,
AltOffering, AltOffering,
AltRfqtMakerAssetOfferings, AltRfqtMakerAssetOfferings,
AffiliateFeeType,
AffiliateFeeAmount,
AssetSwapperContractAddresses, AssetSwapperContractAddresses,
CalldataInfo, CalldataInfo,
ExchangeProxyContractOpts, ExchangeProxyContractOpts,

View File

@ -5,6 +5,7 @@ import {
encodeCurveLiquidityProviderData, encodeCurveLiquidityProviderData,
encodeFillQuoteTransformerData, encodeFillQuoteTransformerData,
encodePayTakerTransformerData, encodePayTakerTransformerData,
encodePositiveSlippageFeeTransformerData,
encodeWethTransformerData, encodeWethTransformerData,
ETH_TOKEN_ADDRESS, ETH_TOKEN_ADDRESS,
FillQuoteTransformerData, FillQuoteTransformerData,
@ -16,8 +17,9 @@ import { BigNumber, providerUtils } from '@0x/utils';
import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper'; import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { constants } from '../constants'; import { constants, POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS } from '../constants';
import { import {
AffiliateFeeType,
CalldataInfo, CalldataInfo,
ExchangeProxyContractOpts, ExchangeProxyContractOpts,
MarketBuySwapQuote, MarketBuySwapQuote,
@ -59,6 +61,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
payTakerTransformer: number; payTakerTransformer: number;
fillQuoteTransformer: number; fillQuoteTransformer: number;
affiliateFeeTransformer: number; affiliateFeeTransformer: number;
positiveSlippageFeeTransformer: number;
}; };
private readonly _exchangeProxy: IZeroExContract; private readonly _exchangeProxy: IZeroExContract;
@ -92,6 +95,10 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
contractAddresses.transformers.affiliateFeeTransformer, contractAddresses.transformers.affiliateFeeTransformer,
contractAddresses.exchangeProxyTransformerDeployer, contractAddresses.exchangeProxyTransformerDeployer,
), ),
positiveSlippageFeeTransformer: findTransformerNonce(
contractAddresses.transformers.positiveSlippageFeeTransformer,
contractAddresses.exchangeProxyTransformerDeployer,
),
}; };
} }
@ -117,7 +124,6 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
if (isFromETH) { if (isFromETH) {
ethAmount = ethAmount.plus(sellAmount); ethAmount = ethAmount.plus(sellAmount);
} }
const { buyTokenFeeAmount, sellTokenFeeAmount, recipient: feeRecipient } = affiliateFee;
// VIP routes. // VIP routes.
if ( if (
@ -144,7 +150,8 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
.getABIEncodedTransactionData(), .getABIEncodedTransactionData(),
ethAmount: isFromETH ? sellAmount : ZERO_AMOUNT, ethAmount: isFromETH ? sellAmount : ZERO_AMOUNT,
toAddress: this._exchangeProxy.address, toAddress: this._exchangeProxy.address,
allowanceTarget: this.contractAddresses.exchangeProxyAllowanceTarget, allowanceTarget: this._exchangeProxy.address,
gasOverhead: ZERO_AMOUNT,
}; };
} }
@ -165,7 +172,8 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
.getABIEncodedTransactionData(), .getABIEncodedTransactionData(),
ethAmount: isFromETH ? sellAmount : ZERO_AMOUNT, ethAmount: isFromETH ? sellAmount : ZERO_AMOUNT,
toAddress: this._exchangeProxy.address, toAddress: this._exchangeProxy.address,
allowanceTarget: this.contractAddresses.exchangeProxyAllowanceTarget, allowanceTarget: this._exchangeProxy.address,
gasOverhead: ZERO_AMOUNT,
}; };
} }
@ -190,7 +198,8 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
.getABIEncodedTransactionData(), .getABIEncodedTransactionData(),
ethAmount: isFromETH ? sellAmount : ZERO_AMOUNT, ethAmount: isFromETH ? sellAmount : ZERO_AMOUNT,
toAddress: this._exchangeProxy.address, toAddress: this._exchangeProxy.address,
allowanceTarget: this.contractAddresses.exchangeProxyAllowanceTarget, allowanceTarget: this._exchangeProxy.address,
gasOverhead: ZERO_AMOUNT,
}; };
} }
@ -262,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. // This transformer pays affiliate fees.
if (buyTokenFeeAmount.isGreaterThan(0) && feeRecipient !== NULL_ADDRESS) { if (buyTokenFeeAmount.isGreaterThan(0)) {
transforms.push({ transforms.push({
deploymentNonce: this.transformerNonces.affiliateFeeTransformer, deploymentNonce: this.transformerNonces.affiliateFeeTransformer,
data: encodeAffiliateFeeTransformerData({ data: encodeAffiliateFeeTransformerData({
@ -279,9 +314,10 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
// Adjust the minimum buy amount by the fee. // Adjust the minimum buy amount by the fee.
minBuyAmount = BigNumber.max(0, minBuyAmount.minus(buyTokenFeeAmount)); 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'); throw new Error('Affiliate fees denominated in sell token are not yet supported');
} }
}
// The final transformer will send all funds to the taker. // The final transformer will send all funds to the taker.
transforms.push({ transforms.push({
@ -306,7 +342,8 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
calldataHexString, calldataHexString,
ethAmount, ethAmount,
toAddress: this._exchangeProxy.address, toAddress: this._exchangeProxy.address,
allowanceTarget: this.contractAddresses.exchangeProxyAllowanceTarget, allowanceTarget: this._exchangeProxy.address,
gasOverhead,
}; };
} }
@ -332,6 +369,10 @@ function isDirectSwapCompatible(
if (!opts.affiliateFee.buyTokenFeeAmount.eq(0) || !opts.affiliateFee.sellTokenFeeAmount.eq(0)) { if (!opts.affiliateFee.buyTokenFeeAmount.eq(0) || !opts.affiliateFee.sellTokenFeeAmount.eq(0)) {
return false; return false;
} }
// Must not have a positive slippage fee.
if (opts.affiliateFee.feeType === AffiliateFeeType.PositiveSlippageFee) {
return false;
}
// Must be a single order. // Must be a single order.
if (quote.orders.length !== 1) { if (quote.orders.length !== 1) {
return false; return false;

View File

@ -454,7 +454,7 @@ function createSwapQuote(
gasSchedule: FeeSchedule, gasSchedule: FeeSchedule,
slippage: number, slippage: number,
): SwapQuote { ): SwapQuote {
const { optimizedOrders, quoteReport, sourceFlags, takerTokenToEthRate, makerTokenToEthRate } = optimizerResult; const { optimizedOrders, quoteReport, sourceFlags, takerAmountPerEth, makerAmountPerEth } = optimizerResult;
const isTwoHop = sourceFlags === SOURCE_FLAGS[ERC20BridgeSource.MultiHop]; const isTwoHop = sourceFlags === SOURCE_FLAGS[ERC20BridgeSource.MultiHop];
// Calculate quote info // Calculate quote info
@ -474,8 +474,8 @@ function createSwapQuote(
sourceBreakdown, sourceBreakdown,
makerTokenDecimals, makerTokenDecimals,
takerTokenDecimals, takerTokenDecimals,
takerTokenToEthRate, takerAmountPerEth,
makerTokenToEthRate, makerAmountPerEth,
quoteReport, quoteReport,
isTwoHop, isTwoHop,
}; };

View File

@ -54,12 +54,15 @@ export interface NativeOrderFillableAmountFields {
* toAddress: The contract address to call. * toAddress: The contract address to call.
* ethAmount: The eth amount in wei to send with the smart contract call. * ethAmount: The eth amount in wei to send with the smart contract call.
* allowanceTarget: The address the taker should grant an allowance to. * allowanceTarget: The address the taker should grant an allowance to.
* gasOverhead: The gas overhead needed to be added to the gas limit to allow for optional
* operations which may not visible at eth_estimateGas time
*/ */
export interface CalldataInfo { export interface CalldataInfo {
calldataHexString: string; calldataHexString: string;
toAddress: string; toAddress: string;
ethAmount: BigNumber; ethAmount: BigNumber;
allowanceTarget: string; allowanceTarget: string;
gasOverhead: BigNumber;
} }
/** /**
@ -98,7 +101,14 @@ export interface SwapQuoteExecutionOpts extends SwapQuoteGetOutputOpts {
gasLimit?: number; gasLimit?: number;
} }
export interface AffiliateFee { export enum AffiliateFeeType {
None,
PercentageFee,
PositiveSlippageFee,
}
export interface AffiliateFeeAmount {
feeType: AffiliateFeeType;
recipient: string; recipient: string;
buyTokenFeeAmount: BigNumber; buyTokenFeeAmount: BigNumber;
sellTokenFeeAmount: BigNumber; sellTokenFeeAmount: BigNumber;
@ -130,7 +140,7 @@ export enum ExchangeProxyRefundReceiver {
export interface ExchangeProxyContractOpts { export interface ExchangeProxyContractOpts {
isFromETH: boolean; isFromETH: boolean;
isToETH: boolean; isToETH: boolean;
affiliateFee: AffiliateFee; affiliateFee: AffiliateFeeAmount;
refundReceiver: string | ExchangeProxyRefundReceiver; refundReceiver: string | ExchangeProxyRefundReceiver;
isMetaTransaction: boolean; isMetaTransaction: boolean;
shouldSellEntireBalance: boolean; shouldSellEntireBalance: boolean;
@ -161,8 +171,8 @@ export interface SwapQuoteBase {
isTwoHop: boolean; isTwoHop: boolean;
makerTokenDecimals: number; makerTokenDecimals: number;
takerTokenDecimals: number; takerTokenDecimals: number;
takerTokenToEthRate: BigNumber; takerAmountPerEth: BigNumber;
makerTokenToEthRate: BigNumber; makerAmountPerEth: BigNumber;
} }
/** /**

View File

@ -60,10 +60,10 @@ export function getComparisonPrices(
} }
// Calc native order fee penalty in output unit (maker units for sells, taker unit for buys) // Calc native order fee penalty in output unit (maker units for sells, taker unit for buys)
const feePenalty = !marketSideLiquidity.ethToOutputRate.isZero() const feePenalty = !marketSideLiquidity.outputAmountPerEth.isZero()
? marketSideLiquidity.ethToOutputRate.times(feeInEth) ? marketSideLiquidity.outputAmountPerEth.times(feeInEth)
: // if it's a sell, the input token is the taker token : // if it's a sell, the input token is the taker token
marketSideLiquidity.ethToInputRate marketSideLiquidity.inputAmountPerEth
.times(feeInEth) .times(feeInEth)
.times(marketSideLiquidity.side === MarketOperation.Sell ? adjustedRate : adjustedRate.pow(-1)); .times(marketSideLiquidity.side === MarketOperation.Sell ? adjustedRate : adjustedRate.pow(-1));

View File

@ -645,6 +645,8 @@ export const DEFAULT_GAS_SCHEDULE: Required<FeeSchedule> = {
export const DEFAULT_FEE_SCHEDULE: Required<FeeSchedule> = { ...DEFAULT_GAS_SCHEDULE }; export const DEFAULT_FEE_SCHEDULE: Required<FeeSchedule> = { ...DEFAULT_GAS_SCHEDULE };
export const POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS = new BigNumber(20000);
// tslint:enable:custom-no-magic-numbers // tslint:enable:custom-no-magic-numbers
export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = { export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = {

View File

@ -16,8 +16,8 @@ export function createFills(opts: {
orders?: NativeOrderWithFillableAmounts[]; orders?: NativeOrderWithFillableAmounts[];
dexQuotes?: DexSample[][]; dexQuotes?: DexSample[][];
targetInput?: BigNumber; targetInput?: BigNumber;
ethToOutputRate?: BigNumber; outputAmountPerEth?: BigNumber;
ethToInputRate?: BigNumber; inputAmountPerEth?: BigNumber;
excludedSources?: ERC20BridgeSource[]; excludedSources?: ERC20BridgeSource[];
feeSchedule?: FeeSchedule; feeSchedule?: FeeSchedule;
}): Fill[][] { }): Fill[][] {
@ -26,20 +26,20 @@ export function createFills(opts: {
const feeSchedule = opts.feeSchedule || {}; const feeSchedule = opts.feeSchedule || {};
const orders = opts.orders || []; const orders = opts.orders || [];
const dexQuotes = opts.dexQuotes || []; const dexQuotes = opts.dexQuotes || [];
const ethToOutputRate = opts.ethToOutputRate || ZERO_AMOUNT; const outputAmountPerEth = opts.outputAmountPerEth || ZERO_AMOUNT;
const ethToInputRate = opts.ethToInputRate || ZERO_AMOUNT; const inputAmountPerEth = opts.inputAmountPerEth || ZERO_AMOUNT;
// Create native fills. // Create native fills.
const nativeFills = nativeOrdersToFills( const nativeFills = nativeOrdersToFills(
side, side,
orders.filter(o => o.fillableTakerAmount.isGreaterThan(0)), orders.filter(o => o.fillableTakerAmount.isGreaterThan(0)),
opts.targetInput, opts.targetInput,
ethToOutputRate, outputAmountPerEth,
ethToInputRate, inputAmountPerEth,
feeSchedule, feeSchedule,
); );
// Create DEX fills. // Create DEX fills.
const dexFills = dexQuotes.map(singleSourceSamples => const dexFills = dexQuotes.map(singleSourceSamples =>
dexSamplesToFills(side, singleSourceSamples, ethToOutputRate, ethToInputRate, feeSchedule), dexSamplesToFills(side, singleSourceSamples, outputAmountPerEth, inputAmountPerEth, feeSchedule),
); );
return [...dexFills, nativeFills] return [...dexFills, nativeFills]
.map(p => clipFillsToInput(p, opts.targetInput)) .map(p => clipFillsToInput(p, opts.targetInput))
@ -75,8 +75,8 @@ function nativeOrdersToFills(
side: MarketOperation, side: MarketOperation,
orders: NativeOrderWithFillableAmounts[], orders: NativeOrderWithFillableAmounts[],
targetInput: BigNumber = POSITIVE_INF, targetInput: BigNumber = POSITIVE_INF,
ethToOutputRate: BigNumber, outputAmountPerEth: BigNumber,
ethToInputRate: BigNumber, inputAmountPerEth: BigNumber,
fees: FeeSchedule, fees: FeeSchedule,
): Fill[] { ): Fill[] {
const sourcePathId = hexUtils.random(); const sourcePathId = hexUtils.random();
@ -89,9 +89,9 @@ function nativeOrdersToFills(
const input = side === MarketOperation.Sell ? takerAmount : makerAmount; const input = side === MarketOperation.Sell ? takerAmount : makerAmount;
const output = side === MarketOperation.Sell ? makerAmount : takerAmount; const output = side === MarketOperation.Sell ? makerAmount : takerAmount;
const fee = fees[ERC20BridgeSource.Native] === undefined ? 0 : fees[ERC20BridgeSource.Native]!(o); const fee = fees[ERC20BridgeSource.Native] === undefined ? 0 : fees[ERC20BridgeSource.Native]!(o);
const outputPenalty = !ethToOutputRate.isZero() const outputPenalty = !outputAmountPerEth.isZero()
? ethToOutputRate.times(fee) ? outputAmountPerEth.times(fee)
: ethToInputRate.times(fee).times(output.dividedToIntegerBy(input)); : inputAmountPerEth.times(fee).times(output.dividedToIntegerBy(input));
// targetInput can be less than the order size // targetInput can be less than the order size
// whilst the penalty is constant, it affects the adjusted output // whilst the penalty is constant, it affects the adjusted output
// only up until the target has been exhausted. // only up until the target has been exhausted.
@ -135,8 +135,8 @@ function nativeOrdersToFills(
function dexSamplesToFills( function dexSamplesToFills(
side: MarketOperation, side: MarketOperation,
samples: DexSample[], samples: DexSample[],
ethToOutputRate: BigNumber, outputAmountPerEth: BigNumber,
ethToInputRate: BigNumber, inputAmountPerEth: BigNumber,
fees: FeeSchedule, fees: FeeSchedule,
): Fill[] { ): Fill[] {
const sourcePathId = hexUtils.random(); const sourcePathId = hexUtils.random();
@ -156,9 +156,9 @@ function dexSamplesToFills(
let penalty = ZERO_AMOUNT; let penalty = ZERO_AMOUNT;
if (i === 0) { if (i === 0) {
// Only the first fill in a DEX path incurs a penalty. // Only the first fill in a DEX path incurs a penalty.
penalty = !ethToOutputRate.isZero() penalty = !outputAmountPerEth.isZero()
? ethToOutputRate.times(fee) ? outputAmountPerEth.times(fee)
: ethToInputRate.times(fee).times(output.dividedToIntegerBy(input)); : inputAmountPerEth.times(fee).times(output.dividedToIntegerBy(input));
} }
const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty); const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty);

View File

@ -30,7 +30,8 @@ import {
import { createFills } from './fills'; import { createFills } from './fills';
import { getBestTwoHopQuote } from './multihop_utils'; import { getBestTwoHopQuote } from './multihop_utils';
import { createOrdersFromTwoHopSample } from './orders'; import { createOrdersFromTwoHopSample } from './orders';
import { findOptimalPathAsync } from './path_optimizer'; import { PathPenaltyOpts } from './path';
import { fillsToSortedPaths, findOptimalPathAsync } from './path_optimizer';
import { DexOrderSampler, getSampleAmounts } from './sampler'; import { DexOrderSampler, getSampleAmounts } from './sampler';
import { SourceFilters } from './source_filters'; import { SourceFilters } from './source_filters';
import { import {
@ -167,8 +168,8 @@ export class MarketOperationUtils {
[ [
tokenDecimals, tokenDecimals,
orderFillableTakerAmounts, orderFillableTakerAmounts,
ethToMakerAssetRate, outputAmountPerEth,
ethToTakerAssetRate, inputAmountPerEth,
dexQuotes, dexQuotes,
rawTwoHopQuotes, rawTwoHopQuotes,
isTxOriginContract, isTxOriginContract,
@ -195,8 +196,8 @@ export class MarketOperationUtils {
inputAmount: takerAmount, inputAmount: takerAmount,
inputToken: takerToken, inputToken: takerToken,
outputToken: makerToken, outputToken: makerToken,
ethToOutputRate: ethToMakerAssetRate, outputAmountPerEth,
ethToInputRate: ethToTakerAssetRate, inputAmountPerEth,
quoteSourceFilters, quoteSourceFilters,
makerTokenDecimals: makerTokenDecimals.toNumber(), makerTokenDecimals: makerTokenDecimals.toNumber(),
takerTokenDecimals: takerTokenDecimals.toNumber(), takerTokenDecimals: takerTokenDecimals.toNumber(),
@ -321,8 +322,8 @@ export class MarketOperationUtils {
inputAmount: makerAmount, inputAmount: makerAmount,
inputToken: makerToken, inputToken: makerToken,
outputToken: takerToken, outputToken: takerToken,
ethToOutputRate: ethToTakerAssetRate, outputAmountPerEth: ethToTakerAssetRate,
ethToInputRate: ethToMakerAssetRate, inputAmountPerEth: ethToMakerAssetRate,
quoteSourceFilters, quoteSourceFilters,
makerTokenDecimals: makerTokenDecimals.toNumber(), makerTokenDecimals: makerTokenDecimals.toNumber(),
takerTokenDecimals: takerTokenDecimals.toNumber(), takerTokenDecimals: takerTokenDecimals.toNumber(),
@ -392,7 +393,7 @@ export class MarketOperationUtils {
const batchEthToTakerAssetRate = executeResults.splice(0, batchNativeOrders.length) as BigNumber[]; const batchEthToTakerAssetRate = executeResults.splice(0, batchNativeOrders.length) as BigNumber[];
const batchDexQuotes = executeResults.splice(0, batchNativeOrders.length) as DexSample[][][]; const batchDexQuotes = executeResults.splice(0, batchNativeOrders.length) as DexSample[][][];
const batchTokenDecimals = executeResults.splice(0, batchNativeOrders.length) as number[][]; const batchTokenDecimals = executeResults.splice(0, batchNativeOrders.length) as number[][];
const ethToInputRate = ZERO_AMOUNT; const inputAmountPerEth = ZERO_AMOUNT;
return Promise.all( return Promise.all(
batchNativeOrders.map(async (nativeOrders, i) => { batchNativeOrders.map(async (nativeOrders, i) => {
@ -401,7 +402,7 @@ export class MarketOperationUtils {
} }
const { makerToken, takerToken } = nativeOrders[0].order; const { makerToken, takerToken } = nativeOrders[0].order;
const orderFillableMakerAmounts = batchOrderFillableMakerAmounts[i]; const orderFillableMakerAmounts = batchOrderFillableMakerAmounts[i];
const ethToTakerAssetRate = batchEthToTakerAssetRate[i]; const outputAmountPerEth = batchEthToTakerAssetRate[i];
const dexQuotes = batchDexQuotes[i]; const dexQuotes = batchDexQuotes[i];
const makerAmount = makerAmounts[i]; const makerAmount = makerAmounts[i];
try { try {
@ -411,8 +412,8 @@ export class MarketOperationUtils {
inputToken: makerToken, inputToken: makerToken,
outputToken: takerToken, outputToken: takerToken,
inputAmount: makerAmount, inputAmount: makerAmount,
ethToOutputRate: ethToTakerAssetRate, outputAmountPerEth,
ethToInputRate, inputAmountPerEth,
quoteSourceFilters, quoteSourceFilters,
makerTokenDecimals: batchTokenDecimals[i][0], makerTokenDecimals: batchTokenDecimals[i][0],
takerTokenDecimals: batchTokenDecimals[i][1], takerTokenDecimals: batchTokenDecimals[i][1],
@ -455,8 +456,8 @@ export class MarketOperationUtils {
side, side,
inputAmount, inputAmount,
quotes, quotes,
ethToOutputRate, outputAmountPerEth,
ethToInputRate, inputAmountPerEth,
} = marketSideLiquidity; } = marketSideLiquidity;
const { nativeOrders, rfqtIndicativeQuotes, dexQuotes } = quotes; const { nativeOrders, rfqtIndicativeQuotes, dexQuotes } = quotes;
const maxFallbackSlippage = opts.maxFallbackSlippage || 0; const maxFallbackSlippage = opts.maxFallbackSlippage || 0;
@ -489,25 +490,29 @@ export class MarketOperationUtils {
orders: [...nativeOrders, ...augmentedRfqtIndicativeQuotes], orders: [...nativeOrders, ...augmentedRfqtIndicativeQuotes],
dexQuotes, dexQuotes,
targetInput: inputAmount, targetInput: inputAmount,
ethToOutputRate, outputAmountPerEth,
ethToInputRate, inputAmountPerEth,
excludedSources: opts.excludedSources, excludedSources: opts.excludedSources,
feeSchedule: opts.feeSchedule, feeSchedule: opts.feeSchedule,
}); });
// Find the optimal path. // Find the optimal path.
const optimizerOpts = { const penaltyOpts: PathPenaltyOpts = {
ethToOutputRate, outputAmountPerEth,
ethToInputRate, inputAmountPerEth,
exchangeProxyOverhead: opts.exchangeProxyOverhead || (() => ZERO_AMOUNT), exchangeProxyOverhead: opts.exchangeProxyOverhead || (() => ZERO_AMOUNT),
}; };
// NOTE: For sell quotes input is the taker asset and for buy quotes input is the maker asset // NOTE: For sell quotes input is the taker asset and for buy quotes input is the maker asset
const takerTokenToEthRate = side === MarketOperation.Sell ? ethToInputRate : ethToOutputRate; const takerAmountPerEth = side === MarketOperation.Sell ? inputAmountPerEth : outputAmountPerEth;
const makerTokenToEthRate = side === MarketOperation.Sell ? ethToOutputRate : ethToInputRate; const makerAmountPerEth = side === MarketOperation.Sell ? outputAmountPerEth : inputAmountPerEth;
// Find the unoptimized best rate to calculate savings from optimizer
const _unoptimizedPath = fillsToSortedPaths(fills, side, inputAmount, penaltyOpts)[0];
const unoptimizedPath = _unoptimizedPath ? _unoptimizedPath.collapse(orderOpts) : undefined;
// Find the optimal path // Find the optimal path
const optimalPath = await findOptimalPathAsync(side, fills, inputAmount, opts.runLimit, optimizerOpts); const optimalPath = await findOptimalPathAsync(side, fills, inputAmount, opts.runLimit, penaltyOpts);
const optimalPathRate = optimalPath ? optimalPath.adjustedRate() : ZERO_AMOUNT; const optimalPathRate = optimalPath ? optimalPath.adjustedRate() : ZERO_AMOUNT;
const { adjustedRate: bestTwoHopRate, quote: bestTwoHopQuote } = getBestTwoHopQuote( const { adjustedRate: bestTwoHopRate, quote: bestTwoHopQuote } = getBestTwoHopQuote(
@ -523,8 +528,9 @@ export class MarketOperationUtils {
sourceFlags: SOURCE_FLAGS[ERC20BridgeSource.MultiHop], sourceFlags: SOURCE_FLAGS[ERC20BridgeSource.MultiHop],
marketSideLiquidity, marketSideLiquidity,
adjustedRate: bestTwoHopRate, adjustedRate: bestTwoHopRate,
takerTokenToEthRate, unoptimizedPath,
makerTokenToEthRate, takerAmountPerEth,
makerAmountPerEth,
}; };
} }
@ -557,8 +563,9 @@ export class MarketOperationUtils {
sourceFlags: collapsedPath.sourceFlags, sourceFlags: collapsedPath.sourceFlags,
marketSideLiquidity, marketSideLiquidity,
adjustedRate: optimalPathRate, adjustedRate: optimalPathRate,
takerTokenToEthRate, unoptimizedPath,
makerTokenToEthRate, takerAmountPerEth,
makerAmountPerEth,
}; };
} }

View File

@ -39,7 +39,7 @@ export function getBestTwoHopQuote(
feeSchedule?: FeeSchedule, feeSchedule?: FeeSchedule,
exchangeProxyOverhead?: ExchangeProxyOverhead, exchangeProxyOverhead?: ExchangeProxyOverhead,
): { quote: DexSample<MultiHopFillData> | undefined; adjustedRate: BigNumber } { ): { quote: DexSample<MultiHopFillData> | undefined; adjustedRate: BigNumber } {
const { side, inputAmount, ethToOutputRate, quotes } = marketSideLiquidity; const { side, inputAmount, outputAmountPerEth, quotes } = marketSideLiquidity;
const { twoHopQuotes } = quotes; const { twoHopQuotes } = quotes;
// Ensure the expected data we require exists. In the case where all hops reverted // Ensure the expected data we require exists. In the case where all hops reverted
// or there were no sources included that allowed for multi hop, // or there were no sources included that allowed for multi hop,
@ -57,7 +57,7 @@ export function getBestTwoHopQuote(
} }
const best = filteredQuotes const best = filteredQuotes
.map(quote => .map(quote =>
getTwoHopAdjustedRate(side, quote, inputAmount, ethToOutputRate, feeSchedule, exchangeProxyOverhead), getTwoHopAdjustedRate(side, quote, inputAmount, outputAmountPerEth, feeSchedule, exchangeProxyOverhead),
) )
.reduce( .reduce(
(prev, curr, i) => (prev, curr, i) =>
@ -67,7 +67,7 @@ export function getBestTwoHopQuote(
side, side,
filteredQuotes[0], filteredQuotes[0],
inputAmount, inputAmount,
ethToOutputRate, outputAmountPerEth,
feeSchedule, feeSchedule,
exchangeProxyOverhead, exchangeProxyOverhead,
), ),

View File

@ -22,14 +22,14 @@ export interface PathSize {
} }
export interface PathPenaltyOpts { export interface PathPenaltyOpts {
ethToOutputRate: BigNumber; outputAmountPerEth: BigNumber;
ethToInputRate: BigNumber; inputAmountPerEth: BigNumber;
exchangeProxyOverhead: ExchangeProxyOverhead; exchangeProxyOverhead: ExchangeProxyOverhead;
} }
export const DEFAULT_PATH_PENALTY_OPTS: PathPenaltyOpts = { export const DEFAULT_PATH_PENALTY_OPTS: PathPenaltyOpts = {
ethToOutputRate: ZERO_AMOUNT, outputAmountPerEth: ZERO_AMOUNT,
ethToInputRate: ZERO_AMOUNT, inputAmountPerEth: ZERO_AMOUNT,
exchangeProxyOverhead: () => ZERO_AMOUNT, exchangeProxyOverhead: () => ZERO_AMOUNT,
}; };
@ -131,11 +131,11 @@ export class Path {
public adjustedSize(): PathSize { public adjustedSize(): PathSize {
const { input, output } = this._adjustedSize; const { input, output } = this._adjustedSize;
const { exchangeProxyOverhead, ethToOutputRate, ethToInputRate } = this.pathPenaltyOpts; const { exchangeProxyOverhead, outputAmountPerEth, inputAmountPerEth } = this.pathPenaltyOpts;
const gasOverhead = exchangeProxyOverhead(this.sourceFlags); const gasOverhead = exchangeProxyOverhead(this.sourceFlags);
const pathPenalty = !ethToOutputRate.isZero() const pathPenalty = !outputAmountPerEth.isZero()
? ethToOutputRate.times(gasOverhead) ? outputAmountPerEth.times(gasOverhead)
: ethToInputRate.times(gasOverhead).times(output.dividedToIntegerBy(input)); : inputAmountPerEth.times(gasOverhead).times(output.dividedToIntegerBy(input));
return { return {
input, input,
output: this.side === MarketOperation.Sell ? output.minus(pathPenalty) : output.plus(pathPenalty), output: this.side === MarketOperation.Sell ? output.minus(pathPenalty) : output.plus(pathPenalty),

View File

@ -13,7 +13,7 @@ export function getTwoHopAdjustedRate(
side: MarketOperation, side: MarketOperation,
twoHopQuote: DexSample<MultiHopFillData>, twoHopQuote: DexSample<MultiHopFillData>,
targetInput: BigNumber, targetInput: BigNumber,
ethToOutputRate: BigNumber, outputAmountPerEth: BigNumber,
fees: FeeSchedule = {}, fees: FeeSchedule = {},
exchangeProxyOverhead: ExchangeProxyOverhead = () => ZERO_AMOUNT, exchangeProxyOverhead: ExchangeProxyOverhead = () => ZERO_AMOUNT,
): BigNumber { ): BigNumber {
@ -21,7 +21,7 @@ export function getTwoHopAdjustedRate(
if (input.isLessThan(targetInput) || output.isZero()) { if (input.isLessThan(targetInput) || output.isZero()) {
return ZERO_AMOUNT; return ZERO_AMOUNT;
} }
const penalty = ethToOutputRate.times( const penalty = outputAmountPerEth.times(
exchangeProxyOverhead(SOURCE_FLAGS.MultiHop).plus(fees[ERC20BridgeSource.MultiHop]!(fillData)), exchangeProxyOverhead(SOURCE_FLAGS.MultiHop).plus(fees[ERC20BridgeSource.MultiHop]!(fillData)),
); );
const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty); const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty);

View File

@ -11,6 +11,7 @@ import { NativeOrderWithFillableAmounts, RfqtFirmQuoteValidator, RfqtRequestOpts
import { QuoteRequestor } from '../../utils/quote_requestor'; import { QuoteRequestor } from '../../utils/quote_requestor';
import { QuoteReport } from '../quote_report_generator'; import { QuoteReport } from '../quote_report_generator';
import { CollapsedPath } from './path';
import { SourceFilters } from './source_filters'; import { SourceFilters } from './source_filters';
/** /**
@ -374,8 +375,9 @@ export interface OptimizerResult {
liquidityDelivered: CollapsedFill[] | DexSample<MultiHopFillData>; liquidityDelivered: CollapsedFill[] | DexSample<MultiHopFillData>;
marketSideLiquidity: MarketSideLiquidity; marketSideLiquidity: MarketSideLiquidity;
adjustedRate: BigNumber; adjustedRate: BigNumber;
takerTokenToEthRate: BigNumber; unoptimizedPath?: CollapsedPath;
makerTokenToEthRate: BigNumber; takerAmountPerEth: BigNumber;
makerAmountPerEth: BigNumber;
} }
export interface OptimizerResultWithReport extends OptimizerResult { export interface OptimizerResultWithReport extends OptimizerResult {
@ -396,8 +398,8 @@ export interface MarketSideLiquidity {
inputAmount: BigNumber; inputAmount: BigNumber;
inputToken: string; inputToken: string;
outputToken: string; outputToken: string;
ethToOutputRate: BigNumber; outputAmountPerEth: BigNumber;
ethToInputRate: BigNumber; inputAmountPerEth: BigNumber;
quoteSourceFilters: SourceFilters; quoteSourceFilters: SourceFilters;
makerTokenDecimals: number; makerTokenDecimals: number;
takerTokenDecimals: number; takerTokenDecimals: number;

View File

@ -49,8 +49,8 @@ const exchangeProxyOverhead = (sourceFlags: number) => {
const buyMarketSideLiquidity: MarketSideLiquidity = { const buyMarketSideLiquidity: MarketSideLiquidity = {
// needed params // needed params
ethToOutputRate: new BigNumber(500), outputAmountPerEth: new BigNumber(500),
ethToInputRate: new BigNumber(1), inputAmountPerEth: new BigNumber(1),
side: MarketOperation.Buy, side: MarketOperation.Buy,
makerTokenDecimals: 18, makerTokenDecimals: 18,
takerTokenDecimals: 18, takerTokenDecimals: 18,
@ -70,8 +70,8 @@ const buyMarketSideLiquidity: MarketSideLiquidity = {
const sellMarketSideLiquidity: MarketSideLiquidity = { const sellMarketSideLiquidity: MarketSideLiquidity = {
// needed params // needed params
ethToOutputRate: new BigNumber(500), outputAmountPerEth: new BigNumber(500),
ethToInputRate: new BigNumber(1), inputAmountPerEth: new BigNumber(1),
side: MarketOperation.Sell, side: MarketOperation.Sell,
makerTokenDecimals: 18, makerTokenDecimals: 18,
takerTokenDecimals: 18, takerTokenDecimals: 18,

View File

@ -4,6 +4,7 @@ import {
decodeAffiliateFeeTransformerData, decodeAffiliateFeeTransformerData,
decodeFillQuoteTransformerData, decodeFillQuoteTransformerData,
decodePayTakerTransformerData, decodePayTakerTransformerData,
decodePositiveSlippageFeeTransformerData,
decodeWethTransformerData, decodeWethTransformerData,
ETH_TOKEN_ADDRESS, ETH_TOKEN_ADDRESS,
FillQuoteTransformerLimitOrderInfo, FillQuoteTransformerLimitOrderInfo,
@ -17,9 +18,9 @@ import * as chai from 'chai';
import * as _ from 'lodash'; import * as _ from 'lodash';
import 'mocha'; import 'mocha';
import { constants } from '../src/constants'; import { constants, POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS } from '../src/constants';
import { ExchangeProxySwapQuoteConsumer } from '../src/quote_consumers/exchange_proxy_swap_quote_consumer'; import { ExchangeProxySwapQuoteConsumer } from '../src/quote_consumers/exchange_proxy_swap_quote_consumer';
import { MarketBuySwapQuote, MarketOperation, MarketSellSwapQuote } from '../src/types'; import { AffiliateFeeType, MarketBuySwapQuote, MarketOperation, MarketSellSwapQuote } from '../src/types';
import { import {
ERC20BridgeSource, ERC20BridgeSource,
OptimizedLimitOrder, OptimizedLimitOrder,
@ -53,6 +54,7 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
payTakerTransformer: getTransformerAddress(TRANSFORMER_DEPLOYER, 2), payTakerTransformer: getTransformerAddress(TRANSFORMER_DEPLOYER, 2),
fillQuoteTransformer: getTransformerAddress(TRANSFORMER_DEPLOYER, 3), fillQuoteTransformer: getTransformerAddress(TRANSFORMER_DEPLOYER, 3),
affiliateFeeTransformer: getTransformerAddress(TRANSFORMER_DEPLOYER, 4), affiliateFeeTransformer: getTransformerAddress(TRANSFORMER_DEPLOYER, 4),
positiveSlippageFeeTransformer: getTransformerAddress(TRANSFORMER_DEPLOYER, 5),
}, },
}; };
let consumer: ExchangeProxySwapQuoteConsumer; let consumer: ExchangeProxySwapQuoteConsumer;
@ -137,11 +139,11 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
protocolFeeInWeiAmount: getRandomAmount(), protocolFeeInWeiAmount: getRandomAmount(),
feeTakerTokenAmount: getRandomAmount(), feeTakerTokenAmount: getRandomAmount(),
}, },
makerAmountPerEth: getRandomInteger(1, 1e9),
takerAmountPerEth: getRandomInteger(1, 1e9),
...(side === MarketOperation.Buy ...(side === MarketOperation.Buy
? { type: MarketOperation.Buy, makerTokenFillAmount } ? { type: MarketOperation.Buy, makerTokenFillAmount }
: { type: MarketOperation.Sell, takerTokenFillAmount }), : { type: MarketOperation.Sell, takerTokenFillAmount }),
takerTokenToEthRate: getRandomAmount(),
makerTokenToEthRate: getRandomAmount(),
}; };
} }
@ -336,6 +338,7 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
recipient: randomAddress(), recipient: randomAddress(),
buyTokenFeeAmount: getRandomAmount(), buyTokenFeeAmount: getRandomAmount(),
sellTokenFeeAmount: ZERO_AMOUNT, sellTokenFeeAmount: ZERO_AMOUNT,
feeType: AffiliateFeeType.PercentageFee,
}; };
const callInfo = await consumer.getCalldataOrThrowAsync(quote, { const callInfo = await consumer.getCalldataOrThrowAsync(quote, {
extensionContractOpts: { affiliateFee }, extensionContractOpts: { affiliateFee },
@ -349,12 +352,42 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
{ token: MAKER_TOKEN, amount: affiliateFee.buyTokenFeeAmount, recipient: affiliateFee.recipient }, { token: MAKER_TOKEN, amount: affiliateFee.buyTokenFeeAmount, recipient: affiliateFee.recipient },
]); ]);
}); });
it('Appends a positive slippage affiliate fee transformer after the fill if the positive slippage fee feeType is specified', async () => {
const quote = getRandomSellQuote();
const affiliateFee = {
recipient: randomAddress(),
buyTokenFeeAmount: ZERO_AMOUNT,
sellTokenFeeAmount: ZERO_AMOUNT,
feeType: AffiliateFeeType.PositiveSlippageFee,
};
const callInfo = await consumer.getCalldataOrThrowAsync(quote, {
extensionContractOpts: { affiliateFee },
});
const callArgs = transformERC20Encoder.decode(callInfo.calldataHexString) as TransformERC20Args;
expect(callArgs.transformations[1].deploymentNonce.toNumber()).to.eq(
consumer.transformerNonces.positiveSlippageFeeTransformer,
);
const positiveSlippageFeeTransformerData = decodePositiveSlippageFeeTransformerData(
callArgs.transformations[1].data,
);
const bestCaseAmount = quote.bestCaseQuoteInfo.makerAmount.plus(
POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS.multipliedBy(quote.gasPrice).multipliedBy(
quote.makerAmountPerEth,
),
);
expect(positiveSlippageFeeTransformerData).to.deep.equal({
token: MAKER_TOKEN,
bestCaseAmount,
recipient: affiliateFee.recipient,
});
});
it('Throws if a sell token affiliate fee is provided', async () => { it('Throws if a sell token affiliate fee is provided', async () => {
const quote = getRandomSellQuote(); const quote = getRandomSellQuote();
const affiliateFee = { const affiliateFee = {
recipient: randomAddress(), recipient: randomAddress(),
buyTokenFeeAmount: ZERO_AMOUNT, buyTokenFeeAmount: ZERO_AMOUNT,
sellTokenFeeAmount: getRandomAmount(), sellTokenFeeAmount: getRandomAmount(),
feeType: AffiliateFeeType.PercentageFee,
}; };
expect( expect(
consumer.getCalldataOrThrowAsync(quote, { consumer.getCalldataOrThrowAsync(quote, {

View File

@ -756,8 +756,8 @@ describe('MarketOperationUtils tests', () => {
inputAmount: Web3Wrapper.toBaseUnitAmount(1, 18), inputAmount: Web3Wrapper.toBaseUnitAmount(1, 18),
inputToken: MAKER_TOKEN, inputToken: MAKER_TOKEN,
outputToken: TAKER_TOKEN, outputToken: TAKER_TOKEN,
ethToInputRate: Web3Wrapper.toBaseUnitAmount(1, 18), inputAmountPerEth: Web3Wrapper.toBaseUnitAmount(1, 18),
ethToOutputRate: Web3Wrapper.toBaseUnitAmount(1, 6), outputAmountPerEth: Web3Wrapper.toBaseUnitAmount(1, 6),
quoteSourceFilters: new SourceFilters(), quoteSourceFilters: new SourceFilters(),
makerTokenDecimals: 6, makerTokenDecimals: 6,
takerTokenDecimals: 18, takerTokenDecimals: 18,
@ -1787,7 +1787,7 @@ describe('MarketOperationUtils tests', () => {
describe('createFills', () => { describe('createFills', () => {
const takerAmount = new BigNumber(5000000); const takerAmount = new BigNumber(5000000);
const ethToOutputRate = new BigNumber(0.5); const outputAmountPerEth = new BigNumber(0.5);
// tslint:disable-next-line:no-object-literal-type-assertion // tslint:disable-next-line:no-object-literal-type-assertion
const smallOrder: NativeOrderWithFillableAmounts = { const smallOrder: NativeOrderWithFillableAmounts = {
order: { order: {
@ -1830,7 +1830,7 @@ describe('MarketOperationUtils tests', () => {
orders, orders,
dexQuotes: [], dexQuotes: [],
targetInput: takerAmount.minus(1), targetInput: takerAmount.minus(1),
ethToOutputRate, outputAmountPerEth,
feeSchedule, feeSchedule,
}); });
expect((path[0][0].fillData as NativeFillData).order.maker).to.eq(smallOrder.order.maker); expect((path[0][0].fillData as NativeFillData).order.maker).to.eq(smallOrder.order.maker);
@ -1843,7 +1843,7 @@ describe('MarketOperationUtils tests', () => {
orders, orders,
dexQuotes: [], dexQuotes: [],
targetInput: POSITIVE_INF, targetInput: POSITIVE_INF,
ethToOutputRate, outputAmountPerEth,
feeSchedule, feeSchedule,
}); });
expect((path[0][0].fillData as NativeFillData).order.maker).to.eq(largeOrder.order.maker); expect((path[0][0].fillData as NativeFillData).order.maker).to.eq(largeOrder.order.maker);

View File

@ -39,8 +39,8 @@ export async function getFullyFillableSwapQuoteWithNoFeesAsync(
worstCaseQuoteInfo: quoteInfo, worstCaseQuoteInfo: quoteInfo,
sourceBreakdown: breakdown, sourceBreakdown: breakdown,
isTwoHop: false, isTwoHop: false,
takerTokenToEthRate: constants.ZERO_AMOUNT, takerAmountPerEth: constants.ZERO_AMOUNT,
makerTokenToEthRate: constants.ZERO_AMOUNT, makerAmountPerEth: constants.ZERO_AMOUNT,
makerTokenDecimals: 18, makerTokenDecimals: 18,
takerTokenDecimals: 18, takerTokenDecimals: 18,
}; };

View File

@ -5,6 +5,10 @@
{ {
"note": "Deploy new FQT", "note": "Deploy new FQT",
"pr": 155 "pr": 155
},
{
"note": "Deploy new `PositiveSlippageFeeTransformer`",
"pr": 101
} }
] ]
}, },

View File

@ -37,7 +37,8 @@
"wethTransformer": "0xb2bc06a4efb20fc6553a69dbfa49b7be938034a7", "wethTransformer": "0xb2bc06a4efb20fc6553a69dbfa49b7be938034a7",
"payTakerTransformer": "0x4638a7ebe75b911b995d0ec73a81e4f85f41f24e", "payTakerTransformer": "0x4638a7ebe75b911b995d0ec73a81e4f85f41f24e",
"affiliateFeeTransformer": "0xda6d9fc5998f550a094585cf9171f0e8ee3ac59f", "affiliateFeeTransformer": "0xda6d9fc5998f550a094585cf9171f0e8ee3ac59f",
"fillQuoteTransformer": "0x227e767a9b7517681d1cb6b846aa9e541484c7ab" "fillQuoteTransformer": "0x227e767a9b7517681d1cb6b846aa9e541484c7ab",
"positiveSlippageFeeTransformer": "0xa9416ce1dbde8d331210c07b5c253d94ee4cc3fd"
} }
}, },
"3": { "3": {
@ -78,7 +79,8 @@
"wethTransformer": "0x05ad19aa3826e0609a19568ffbd1dfe86c6c7184", "wethTransformer": "0x05ad19aa3826e0609a19568ffbd1dfe86c6c7184",
"payTakerTransformer": "0x6d0ebf2bcd9cc93ec553b60ad201943dcca4e291", "payTakerTransformer": "0x6d0ebf2bcd9cc93ec553b60ad201943dcca4e291",
"affiliateFeeTransformer": "0x6588256778ca4432fa43983ac685c45efb2379e2", "affiliateFeeTransformer": "0x6588256778ca4432fa43983ac685c45efb2379e2",
"fillQuoteTransformer": "0x2088a820787ebbe937a0612ef024f1e1d65f9784" "fillQuoteTransformer": "0x2088a820787ebbe937a0612ef024f1e1d65f9784",
"positiveSlippageFeeTransformer": "0x8b332f700fd37e71c5c5b26c4d78b5ca63dd33b2"
} }
}, },
"4": { "4": {
@ -119,7 +121,8 @@
"wethTransformer": "0x8d822fe2b42f60531203e288f5f357fa79474437", "wethTransformer": "0x8d822fe2b42f60531203e288f5f357fa79474437",
"payTakerTransformer": "0x150652244723102faeaefa4c79597d097ffa26c6", "payTakerTransformer": "0x150652244723102faeaefa4c79597d097ffa26c6",
"affiliateFeeTransformer": "0xa39b40642e8e00435857a0fe7d0655e08cc2217e", "affiliateFeeTransformer": "0xa39b40642e8e00435857a0fe7d0655e08cc2217e",
"fillQuoteTransformer": "0x3fb85e0c1e9e0ba4ba9a4072442a2540c0473db1" "fillQuoteTransformer": "0x3fb85e0c1e9e0ba4ba9a4072442a2540c0473db1",
"positiveSlippageFeeTransformer": "0x0000000000000000000000000000000000000000"
} }
}, },
"42": { "42": {
@ -160,7 +163,8 @@
"wethTransformer": "0x9ce35b5ee9e710535e3988e3f8731d9ca9dba17d", "wethTransformer": "0x9ce35b5ee9e710535e3988e3f8731d9ca9dba17d",
"payTakerTransformer": "0x5a53e7b02a83aa9f60ccf4e424f0442c255bc977", "payTakerTransformer": "0x5a53e7b02a83aa9f60ccf4e424f0442c255bc977",
"affiliateFeeTransformer": "0x870893920a96a28d4b63c0a7d06a521e3bd074b3", "affiliateFeeTransformer": "0x870893920a96a28d4b63c0a7d06a521e3bd074b3",
"fillQuoteTransformer": "0x8d2d732e5fe6d4d6d5e715200b84dfa69fb05478" "fillQuoteTransformer": "0x8d2d732e5fe6d4d6d5e715200b84dfa69fb05478",
"positiveSlippageFeeTransformer": "0x0000000000000000000000000000000000000000"
} }
}, },
"1337": { "1337": {
@ -201,7 +205,8 @@
"wethTransformer": "0x7209185959d7227fb77274e1e88151d7c4c368d3", "wethTransformer": "0x7209185959d7227fb77274e1e88151d7c4c368d3",
"payTakerTransformer": "0x3f16ca81691dab9184cb4606c361d73c4fd2510a", "payTakerTransformer": "0x3f16ca81691dab9184cb4606c361d73c4fd2510a",
"affiliateFeeTransformer": "0x99356167edba8fbdc36959e3f5d0c43d1ba9c6db", "affiliateFeeTransformer": "0x99356167edba8fbdc36959e3f5d0c43d1ba9c6db",
"fillQuoteTransformer": "0x45b3a72221e571017c0f0ec42189e11d149d0ace" "fillQuoteTransformer": "0x45b3a72221e571017c0f0ec42189e11d149d0ace",
"positiveSlippageFeeTransformer": "0xdd66c23e07b4d6925b6089b5fe6fc9e62941afe8"
} }
} }
} }

View File

@ -39,6 +39,7 @@ export interface ContractAddresses {
payTakerTransformer: string; payTakerTransformer: string;
fillQuoteTransformer: string; fillQuoteTransformer: string;
affiliateFeeTransformer: string; affiliateFeeTransformer: string;
positiveSlippageFeeTransformer: string;
}; };
} }

View File

@ -32,6 +32,7 @@ import {
FillQuoteTransformerContract, FillQuoteTransformerContract,
fullMigrateAsync as fullMigrateExchangeProxyAsync, fullMigrateAsync as fullMigrateExchangeProxyAsync,
PayTakerTransformerContract, PayTakerTransformerContract,
PositiveSlippageFeeTransformerContract,
WethTransformerContract, WethTransformerContract,
} from '@0x/contracts-zero-ex'; } from '@0x/contracts-zero-ex';
import { Web3ProviderEngine } from '@0x/subproviders'; import { Web3ProviderEngine } from '@0x/subproviders';
@ -345,7 +346,12 @@ export async function runMigrationsAsync(
bridgeAdapter.address, bridgeAdapter.address,
exchangeProxy.address, exchangeProxy.address,
); );
const positiveSlippageFeeTransformer = await PositiveSlippageFeeTransformerContract.deployFrom0xArtifactAsync(
exchangeProxyArtifacts.PositiveSlippageFeeTransformer,
provider,
txDefaults,
allArtifacts,
);
const contractAddresses = { const contractAddresses = {
erc20Proxy: erc20Proxy.address, erc20Proxy: erc20Proxy.address,
erc721Proxy: erc721Proxy.address, erc721Proxy: erc721Proxy.address,
@ -385,6 +391,7 @@ export async function runMigrationsAsync(
payTakerTransformer: payTakerTransformer.address, payTakerTransformer: payTakerTransformer.address,
fillQuoteTransformer: fillQuoteTransformer.address, fillQuoteTransformer: fillQuoteTransformer.address,
affiliateFeeTransformer: affiliateFeeTransformer.address, affiliateFeeTransformer: affiliateFeeTransformer.address,
positiveSlippageFeeTransformer: positiveSlippageFeeTransformer.address,
}, },
}; };
return contractAddresses; return contractAddresses;

View File

@ -77,6 +77,9 @@ export {
AffiliateFeeTransformerData, AffiliateFeeTransformerData,
encodeAffiliateFeeTransformerData, encodeAffiliateFeeTransformerData,
decodeAffiliateFeeTransformerData, decodeAffiliateFeeTransformerData,
PositiveSlippageFeeTransformerData,
encodePositiveSlippageFeeTransformerData,
decodePositiveSlippageFeeTransformerData,
findTransformerNonce, findTransformerNonce,
getTransformerAddress, getTransformerAddress,
} from './transformer_utils'; } from './transformer_utils';

View File

@ -152,7 +152,7 @@ export function decodePayTakerTransformerData(encoded: string): PayTakerTransfor
} }
/** /**
* ABI encoder for `PayTakerTransformer.TransformData` * ABI encoder for `affiliateFeetransformer.TransformData`
*/ */
export const affiliateFeeTransformerDataEncoder = AbiEncoder.create({ export const affiliateFeeTransformerDataEncoder = AbiEncoder.create({
name: 'data', name: 'data',
@ -195,6 +195,42 @@ export function decodeAffiliateFeeTransformerData(encoded: string): AffiliateFee
return affiliateFeeTransformerDataEncoder.decode(encoded); return affiliateFeeTransformerDataEncoder.decode(encoded);
} }
/**
* ABI encoder for `PositiveSlippageFeeTransformer.TransformData`
*/
export const positiveSlippageFeeTransformerDataEncoder = AbiEncoder.create({
name: 'data',
type: 'tuple',
components: [
{ name: 'token', type: 'address' },
{ name: 'bestCaseAmount', type: 'uint256' },
{ name: 'recipient', type: 'address' },
],
});
/**
* `PositiveSlippageFeeTransformer.TransformData`
*/
export interface PositiveSlippageFeeTransformerData {
token: string;
bestCaseAmount: BigNumber;
recipient: string;
}
/**
* ABI-encode a `PositiveSlippageFeeTransformer.TransformData` type.
*/
export function encodePositiveSlippageFeeTransformerData(data: PositiveSlippageFeeTransformerData): string {
return positiveSlippageFeeTransformerDataEncoder.encode(data);
}
/**
* ABI-decode a `PositiveSlippageFeeTransformer.TransformData` type.
*/
export function decodePositiveSlippageFeeTransformerData(encoded: string): PositiveSlippageFeeTransformerData {
return positiveSlippageFeeTransformerDataEncoder.decode(encoded);
}
/** /**
* Find the nonce for a transformer given its deployer. * Find the nonce for a transformer given its deployer.
* If `deployer` is the null address, zero will always be returned. * If `deployer` is the null address, zero will always be returned.

View File

@ -242,7 +242,7 @@ export function decodePayTakerTransformerData(encoded: string): PayTakerTransfor
} }
/** /**
* ABI encoder for `PayTakerTransformer.TransformData` * ABI encoder for `affiliateFeetransformer.TransformData`
*/ */
export const affiliateFeeTransformerDataEncoder = AbiEncoder.create({ export const affiliateFeeTransformerDataEncoder = AbiEncoder.create({
name: 'data', name: 'data',
@ -317,3 +317,39 @@ export function getTransformerAddress(deployer: string, nonce: number): string {
ethjs.rlphash([deployer, nonce] as any).slice(12), ethjs.rlphash([deployer, nonce] as any).slice(12),
); );
} }
/**
* ABI encoder for `PositiveSlippageFeeTransformer.TransformData`
*/
export const positiveSlippageFeeTransformerDataEncoder = AbiEncoder.create({
name: 'data',
type: 'tuple',
components: [
{ name: 'token', type: 'address' },
{ name: 'bestCaseAmount', type: 'uint256' },
{ name: 'recipient', type: 'address' },
],
});
/**
* `PositiveSlippageFeeTransformer.TransformData`
*/
export interface PositiveSlippageFeeTransformerData {
token: string;
bestCaseAmount: BigNumber;
recipient: string;
}
/**
* ABI-encode a `PositiveSlippageFeeTransformer.TransformData` type.
*/
export function encodePositiveSlippageFeeTransformerData(data: PositiveSlippageFeeTransformerData): string {
return positiveSlippageFeeTransformerDataEncoder.encode(data);
}
/**
* ABI-decode a `PositiveSlippageFeeTransformer.TransformData` type.
*/
export function decodePositiveSlippageFeeTransformerData(encoded: string): PositiveSlippageFeeTransformerData {
return positiveSlippageFeeTransformerDataEncoder.decode(encoded);
}