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",
"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"
},
"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",

View File

@ -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,

View File

@ -46,6 +46,7 @@ export {
IZeroExContract,
LogMetadataTransformerContract,
PayTakerTransformerContract,
PositiveSlippageFeeTransformerContract,
WethTransformerContract,
ZeroExContract,
} 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/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';

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 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,

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/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';

View File

@ -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",

View File

@ -49,6 +49,10 @@
{
"note": "Add an alternative RFQ market making implementation",
"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 {
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,

View File

@ -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,

View File

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

View File

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

View File

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

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)
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));

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 POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS = new BigNumber(20000);
// tslint:enable:custom-no-magic-numbers
export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,

View File

@ -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, {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -77,6 +77,9 @@ export {
AffiliateFeeTransformerData,
encodeAffiliateFeeTransformerData,
decodeAffiliateFeeTransformerData,
PositiveSlippageFeeTransformerData,
encodePositiveSlippageFeeTransformerData,
decodePositiveSlippageFeeTransformerData,
findTransformerNonce,
getTransformerAddress,
} 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({
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.

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({
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);
}