Skip to content
This repository has been archived by the owner on Jul 9, 2021. It is now read-only.

Commit

Permalink
@0x/asset-swapper: Add ExchangeProxySwapQuoteConsumer.
Browse files Browse the repository at this point in the history
  • Loading branch information
merklejerk committed Jun 1, 2020
1 parent c1f7791 commit a4dc11b
Show file tree
Hide file tree
Showing 7 changed files with 439 additions and 11 deletions.
1 change: 1 addition & 0 deletions packages/asset-swapper/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"@0x/assert": "^3.0.7",
"@0x/contract-addresses": "^4.9.0",
"@0x/contract-wrappers": "^13.6.3",
"@0x/contracts-zero-ex": "^0.1.0",
"@0x/json-schemas": "^5.0.7",
"@0x/order-utils": "^10.2.4",
"@0x/orderbook": "^2.2.5",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { ContractAddresses } from '@0x/contract-addresses';
import { ITransformERC20Contract } from '@0x/contract-wrappers';
import {
encodeFillQuoteTransformerData,
encodePayTakerTransformerData,
encodeWethTransformerData,
ETH_TOKEN_ADDRESS,
FillQuoteTransformerSide,
} from '@0x/contracts-zero-ex';
import { assetDataUtils, ERC20AssetData } from '@0x/order-utils';
import { AssetProxyId } from '@0x/types';
import { BigNumber, providerUtils } from '@0x/utils';
import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper';
import * as _ from 'lodash';

import { constants } from '../constants';
import {
CalldataInfo,
ExchangeProxyContractOpts,
MarketBuySwapQuote,
MarketOperation,
MarketSellSwapQuote,
SwapQuote,
SwapQuoteConsumerBase,
SwapQuoteConsumerOpts,
SwapQuoteExecutionOpts,
SwapQuoteGetOutputOpts,
} from '../types';
import { assert } from '../utils/assert';

// tslint:disable-next-line:custom-no-magic-numbers
const MAX_UINT256 = new BigNumber(2).pow(256).minus(1);

export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
public readonly provider: ZeroExProvider;
public readonly chainId: number;

private readonly _transformFeature: ITransformERC20Contract;

constructor(
supportedProvider: SupportedProvider,
public readonly contractAddresses: ContractAddresses,
options: Partial<SwapQuoteConsumerOpts> = {},
) {
const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options);
assert.isNumber('chainId', chainId);
const provider = providerUtils.standardizeOrThrow(supportedProvider);
this.provider = provider;
this.chainId = chainId;
this.contractAddresses = contractAddresses;
this._transformFeature = new ITransformERC20Contract(contractAddresses.exchangeProxy, supportedProvider);
}

public async getCalldataOrThrowAsync(
quote: MarketBuySwapQuote | MarketSellSwapQuote,
opts: Partial<SwapQuoteGetOutputOpts> = {},
): Promise<CalldataInfo> {
assert.isValidSwapQuote('quote', quote);
const exchangeProxyOpts = {
...constants.DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS,
...{
isFromETH: false,
isToETH: false,
},
...opts,
}.extensionContractOpts as ExchangeProxyContractOpts;

const sellToken = getTokenFromAssetData(quote.takerAssetData);
const buyToken = getTokenFromAssetData(quote.makerAssetData);

// Build up the transforms.
const transforms = [];
if (exchangeProxyOpts.isFromETH) {
// Create a WETH wrapper if coming from ETH.
transforms.push({
transformer: this.contractAddresses.transformers.wethTransformer,
data: encodeWethTransformerData({
token: ETH_TOKEN_ADDRESS,
amount: quote.worstCaseQuoteInfo.totalTakerAssetAmount,
}),
});
}

// This transformer will fill the quote.
transforms.push({
transformer: this.contractAddresses.transformers.fillQuoteTransformer,
data: encodeFillQuoteTransformerData({
sellToken,
buyToken,
side: isBuyQuote(quote) ? FillQuoteTransformerSide.Buy : FillQuoteTransformerSide.Sell,
fillAmount: isBuyQuote(quote) ? quote.makerAssetFillAmount : quote.takerAssetFillAmount,
maxOrderFillAmounts: [],
orders: quote.orders,
signatures: quote.orders.map(o => o.signature),
}),
});

if (exchangeProxyOpts.isToETH) {
// Create a WETH unwrapper if going to ETH.
transforms.push({
transformer: this.contractAddresses.transformers.wethTransformer,
data: encodeWethTransformerData({
token: this.contractAddresses.etherToken,
amount: MAX_UINT256,
}),
});
}

// The final transformer will send all funds to the taker.
transforms.push({
transformer: this.contractAddresses.transformers.payTakerTransformer,
data: encodePayTakerTransformerData({
tokens: [sellToken, buyToken, ETH_TOKEN_ADDRESS],
amounts: [],
}),
});

const calldataHexString = this._transformFeature
.transformERC20(
sellToken,
buyToken,
quote.worstCaseQuoteInfo.totalTakerAssetAmount,
quote.worstCaseQuoteInfo.makerAssetAmount,
transforms,
)
.getABIEncodedTransactionData();

return {
calldataHexString,
ethAmount: quote.worstCaseQuoteInfo.protocolFeeInWeiAmount,
toAddress: this._transformFeature.address,
allowanceTarget: this.contractAddresses.exchangeProxyAllowanceTarget,
};
}

// tslint:disable-next-line:prefer-function-over-method
public async executeSwapQuoteOrThrowAsync(
_quote: SwapQuote,
_opts: Partial<SwapQuoteExecutionOpts>,
): Promise<string> {
throw new Error('Execution not supported for Exchange Proxy quotes');
}
}

function isBuyQuote(quote: SwapQuote): quote is MarketBuySwapQuote {
return quote.type === MarketOperation.Buy;
}

function getTokenFromAssetData(assetData: string): string {
const data = assetDataUtils.decodeAssetDataOrThrow(assetData);
if (data.assetProxyId !== AssetProxyId.ERC20) {
throw new Error(`Unsupported exchange proxy quote asset type: ${data.assetProxyId}`);
}
// tslint:disable-next-line:no-unnecessary-type-assertion
return (data as ERC20AssetData).tokenAddress;
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class ExchangeSwapQuoteConsumer implements SwapQuoteConsumerBase {

constructor(
supportedProvider: SupportedProvider,
contractAddresses: ContractAddresses,
public readonly contractAddresses: ContractAddresses,
options: Partial<SwapQuoteConsumerOpts> = {},
) {
const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options);
Expand Down Expand Up @@ -59,6 +59,7 @@ export class ExchangeSwapQuoteConsumer implements SwapQuoteConsumerBase {
calldataHexString,
ethAmount: quote.worstCaseQuoteInfo.protocolFeeInWeiAmount,
toAddress: this._exchangeContract.address,
allowanceTarget: this.contractAddresses.erc20Proxy,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,24 @@ import { affiliateFeeUtils } from '../utils/affiliate_fee_utils';
import { assert } from '../utils/assert';
import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils';

const { NULL_ADDRESS } = constants;

export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase {
public readonly provider: ZeroExProvider;
public readonly chainId: number;

private readonly _contractAddresses: ContractAddresses;
private readonly _forwarder: ForwarderContract;

constructor(
supportedProvider: SupportedProvider,
contractAddresses: ContractAddresses,
public readonly contractAddresses: ContractAddresses,
options: Partial<SwapQuoteConsumerOpts> = {},
) {
const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options);
assert.isNumber('chainId', chainId);
const provider = providerUtils.standardizeOrThrow(supportedProvider);
this.provider = provider;
this.chainId = chainId;
this._contractAddresses = contractAddresses;
this._forwarder = new ForwarderContract(contractAddresses.forwarder, supportedProvider);
}

Expand Down Expand Up @@ -90,6 +90,7 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase {
calldataHexString,
toAddress: this._forwarder.address,
ethAmount: ethAmountWithFees,
allowanceTarget: NULL_ADDRESS,
};
}

Expand Down Expand Up @@ -160,6 +161,6 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase {
}

private _getEtherTokenAssetDataOrThrow(): string {
return assetDataUtils.encodeERC20AssetData(this._contractAddresses.etherToken);
return assetDataUtils.encodeERC20AssetData(this.contractAddresses.etherToken);
}
}
17 changes: 14 additions & 3 deletions packages/asset-swapper/src/quote_consumers/swap_quote_consumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { assert } from '../utils/assert';
import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils';

import { ExchangeProxySwapQuoteConsumer } from './exchange_proxy_swap_quote_consumer';
import { ExchangeSwapQuoteConsumer } from './exchange_swap_quote_consumer';
import { ForwarderSwapQuoteConsumer } from './forwarder_swap_quote_consumer';

Expand All @@ -27,6 +28,7 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase {
private readonly _exchangeConsumer: ExchangeSwapQuoteConsumer;
private readonly _forwarderConsumer: ForwarderSwapQuoteConsumer;
private readonly _contractAddresses: ContractAddresses;
private readonly _exchangeProxyConsumer: ExchangeProxySwapQuoteConsumer;

public static getSwapQuoteConsumer(
supportedProvider: SupportedProvider,
Expand All @@ -45,6 +47,11 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase {
this._contractAddresses = options.contractAddresses || getContractAddressesForChainOrThrow(chainId);
this._exchangeConsumer = new ExchangeSwapQuoteConsumer(supportedProvider, this._contractAddresses, options);
this._forwarderConsumer = new ForwarderSwapQuoteConsumer(supportedProvider, this._contractAddresses, options);
this._exchangeProxyConsumer = new ExchangeProxySwapQuoteConsumer(
supportedProvider,
this._contractAddresses,
options,
);
}

/**
Expand Down Expand Up @@ -93,9 +100,13 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase {
}

private async _getConsumerForSwapQuoteAsync(opts: Partial<SwapQuoteGetOutputOpts>): Promise<SwapQuoteConsumerBase> {
if (opts.useExtensionContract === ExtensionContractType.Forwarder) {
return this._forwarderConsumer;
switch (opts.useExtensionContract) {
case ExtensionContractType.Forwarder:
return this._forwarderConsumer;
case ExtensionContractType.ExchangeProxy:
return this._exchangeProxyConsumer;
default:
return this._exchangeConsumer;
}
return this._exchangeConsumer;
}
}
17 changes: 14 additions & 3 deletions packages/asset-swapper/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,22 @@ export interface SignedOrderWithFillableAmounts extends SignedOrder {
* calldataHexString: The hexstring of the calldata.
* 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.
*/
export interface CalldataInfo {
calldataHexString: string;
toAddress: string;
ethAmount: BigNumber;
allowanceTarget: string;
}

/**
* Represents the varying smart contracts that can consume a valid swap quote
*/
export enum ExtensionContractType {
Forwarder = 'FORWARDER',
None = 'NONE',
Forwarder = 'FORWARDER',
ExchangeProxy = 'EXCHANGE_PROXY',
}

/**
Expand Down Expand Up @@ -97,7 +100,7 @@ export interface SwapQuoteConsumerOpts {
*/
export interface SwapQuoteGetOutputOpts {
useExtensionContract: ExtensionContractType;
extensionContractOpts?: ForwarderExtensionContractOpts | any;
extensionContractOpts?: ForwarderExtensionContractOpts | ExchangeProxyContractOpts | any;
}

/**
Expand All @@ -112,7 +115,6 @@ export interface SwapQuoteExecutionOpts extends SwapQuoteGetOutputOpts {
}

/**
* ethAmount: The amount of eth (in Wei) sent to the forwarder contract.
* feePercentage: percentage (up to 5%) of the taker asset paid to feeRecipient
* feeRecipient: address of the receiver of the feePercentage of taker asset
*/
Expand All @@ -121,6 +123,15 @@ export interface ForwarderExtensionContractOpts {
feeRecipient: string;
}

/**
* @param isFromETH Whether the input token is ETH.
* @param isToETH Whether the output token is ETH.
*/
export interface ExchangeProxyContractOpts {
isFromETH: boolean;
isToETH: boolean;
}

export type SwapQuote = MarketBuySwapQuote | MarketSellSwapQuote;

export interface GetExtensionContractTypeOpts {
Expand Down
Loading

0 comments on commit a4dc11b

Please sign in to comment.