Skip to content

Commit

Permalink
Feat: native support for fees in DEX (#1017)
Browse files Browse the repository at this point in the history
  • Loading branch information
keithbro-imx committed Oct 18, 2023
1 parent 09eeae4 commit 3391980
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 59 deletions.
6 changes: 3 additions & 3 deletions packages/internal/dex/sdk/src/exchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
ERC20,
ExchangeModuleConfiguration, Native, SecondaryFee, TransactionResponse,
} from './types';
import { getSwap, prepareSwap } from './lib/transactionUtils/swap';
import { getSwap, adjustQuoteWithFees } from './lib/transactionUtils/swap';
import { ExchangeConfiguration } from './config';

export class Exchange {
Expand Down Expand Up @@ -160,7 +160,7 @@ export class Exchange {
fetchGasPrice(this.provider, this.nativeToken),
]);

const adjustedQuote = prepareSwap(ourQuote, amountSpecified, fees);
const adjustedQuote = adjustQuoteWithFees(ourQuote, amountSpecified, fees, this.nativeTokenService);

const swap = getSwap(
adjustedQuote,
Expand All @@ -174,7 +174,7 @@ export class Exchange {
this.nativeTokenService,
);

const userQuote = prepareUserQuote(otherToken, adjustedQuote, slippagePercent, fees);
const userQuote = prepareUserQuote(otherToken, adjustedQuote, slippagePercent, fees, this.nativeTokenService);

const preparedApproval = prepareApproval(
tradeType,
Expand Down
28 changes: 16 additions & 12 deletions packages/internal/dex/sdk/src/lib/fees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,36 @@ import { BASIS_POINT_PRECISION } from 'constants/router';
import { BigNumber } from 'ethers';
import {
Amount,
Fee, SecondaryFee, newAmount, ERC20, subtractERC20Amount, addERC20Amount,
SecondaryFee, newAmount, addAmount, Coin, subtractAmount,
} from 'lib';

export class Fees {
private secondaryFees: SecondaryFee[];

private amount: Amount<ERC20>;
private amount: Amount<Coin>;

constructor(secondaryFees: SecondaryFee[], token: ERC20) {
constructor(secondaryFees: SecondaryFee[], token: Coin) {
this.secondaryFees = secondaryFees;
this.amount = newAmount(BigNumber.from(0), token);
}

addAmount(amount: Amount<ERC20>): void {
this.amount = addERC20Amount(this.amount, amount);
get token(): Coin {
return this.amount.token;
}

amountWithFeesApplied(): Amount<ERC20> {
return addERC20Amount(this.amount, this.total());
addAmount(amount: Amount<Coin>): void {
this.amount = addAmount(this.amount, amount);
}

amountLessFees(): Amount<ERC20> {
return subtractERC20Amount(this.amount, this.total());
amountWithFeesApplied(): Amount<Coin> {
return addAmount(this.amount, this.total());
}

withAmounts(): Fee[] {
amountLessFees(): Amount<Coin> {
return subtractAmount(this.amount, this.total());
}

withAmounts() {
return this.secondaryFees.map((fee) => {
const feeAmount = this.amount.value
.mul(fee.basisPoints)
Expand All @@ -40,14 +44,14 @@ export class Fees {
});
}

private total(): Amount<ERC20> {
private total(): Amount<Coin> {
let totalFees = newAmount(BigNumber.from(0), this.amount.token);

for (const fee of this.secondaryFees) {
const feeAmount = this.amount.value
.mul(fee.basisPoints)
.div(BASIS_POINT_PRECISION);
totalFees = addERC20Amount(totalFees, newAmount(feeAmount, this.amount.token));
totalFees = addAmount(totalFees, newAmount(feeAmount, this.amount.token));
}

return totalFees;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,17 @@ describe('applySlippage', () => {

describe('getOurQuoteReqAmount', () => {
describe('when trade is EXACT_INPUT, and amountSpecified is native, and no fees', () => {
it.todo('wraps the amount');
it('wraps the amount', () => {
const amountSpecified = newAmountFromString('1', nativeTokenService.nativeToken);
const noFees = new Fees([], amountSpecified.token);
const quoteReqAmount = getOurQuoteReqAmount(amountSpecified, noFees, TradeType.EXACT_INPUT, nativeTokenService);
expectERC20(quoteReqAmount.token, nativeTokenService.wrappedToken.address);
expect(formatAmount(quoteReqAmount)).toEqual('1.0');
});
});

describe('when trade is EXACT_OUTPUT, and amountSpecified is native, and no fees', () => {
it.skip('wraps the amount unchanged', () => {
it('wraps the amount unchanged', () => {
const amountSpecified = newAmountFromString('1', nativeTokenService.nativeToken);
const noFees = new Fees([], FUN_TEST_TOKEN);
const quoteReqAmount = getOurQuoteReqAmount(amountSpecified, noFees, TradeType.EXACT_OUTPUT, nativeTokenService);
Expand Down
12 changes: 5 additions & 7 deletions packages/internal/dex/sdk/src/lib/transactionUtils/getQuote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { ethers } from 'ethers';
import { Fees } from 'lib/fees';
import { QuoteResult } from 'lib/getQuotesForRoutes';
import { NativeTokenService } from 'lib/nativeTokenService';
import { isERC20Amount } from 'lib/utils';
import {
Amount, Coin, ERC20, Quote,
} from '../../types';
Expand Down Expand Up @@ -34,6 +33,7 @@ export function prepareUserQuote(
tradeInfo: QuoteResult,
slippage: number,
fees: Fees,
nativeTokenService: NativeTokenService,
): Quote {
const quote = getQuoteAmountFromTradeType(tradeInfo);
const amountWithSlippage = applySlippage(tradeInfo.tradeType, quote.value, slippage);
Expand All @@ -45,7 +45,10 @@ export function prepareUserQuote(
value: amountWithSlippage,
},
slippage,
fees: fees.withAmounts(),
fees: fees.withAmounts().map((fee) => ({
...fee,
amount: nativeTokenService.maybeWrapAmount(fee.amount),
})),
};
}

Expand All @@ -55,11 +58,6 @@ export function getOurQuoteReqAmount(
tradeType: TradeType,
nativeTokenService: NativeTokenService,
): Amount<ERC20> {
// TODO: TP-1649: Remove this when we support Native
if (!isERC20Amount(amountSpecified)) {
throw new Error('Not implemented yet!');
}

if (tradeType === TradeType.EXACT_OUTPUT) {
// For an exact output swap, we do not need to subtract fees from the given amount
return nativeTokenService.maybeWrapAmount(amountSpecified);
Expand Down
121 changes: 102 additions & 19 deletions packages/internal/dex/sdk/src/lib/transactionUtils/swap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import {
decodeMulticallExactInputSingleWithFees, decodeMulticallExactInputSingleWithoutFees,
decodeMulticallExactOutputSingleWithFees, decodeMulticallExactOutputSingleWithoutFees,
expectInstanceOf, expectToBeDefined, makeAddr, formatAmount, newAmountFromString,
nativeTokenService, NATIVE_TEST_TOKEN,
nativeTokenService, NATIVE_TEST_TOKEN, expectERC20,
} from 'test/utils';
import { Pool, Route } from '@uniswap/v3-sdk';
import { Fees } from 'lib/fees';
import { erc20ToUniswapToken, newAmount, uniswapTokenToERC20 } from 'lib';
import {
Coin, erc20ToUniswapToken, newAmount, uniswapTokenToERC20,
} from 'lib';
import { QuoteResult } from 'lib/getQuotesForRoutes';
import { getSwap, prepareSwap } from './swap';
import { getSwap, adjustQuoteWithFees } from './swap';

const UNISWAP_IMX = erc20ToUniswapToken(IMX_TEST_TOKEN);
const UNISWAP_FUN = erc20ToUniswapToken(FUN_TEST_TOKEN);
Expand All @@ -26,17 +28,22 @@ const testPool = new Pool(
);

const route = new Route([testPool], UNISWAP_IMX, UNISWAP_FUN);
const gasEstimate = BigNumber.from(0);

const tenPercentFees = (tokenIn: Coin): Fees =>
// eslint-disable-next-line implicit-arrow-linebreak
new Fees([{ recipient: TEST_FEE_RECIPIENT, basisPoints: 1000 }], tokenIn);

const buildExactInputQuote = (): QuoteResult => ({
gasEstimate: BigNumber.from(0),
gasEstimate,
route,
amountIn: newAmountFromString('99', uniswapTokenToERC20(route.input)),
amountOut: newAmountFromString('990', uniswapTokenToERC20(route.output)),
tradeType: TradeType.EXACT_INPUT,
});

const buildExactOutputQuote = (): QuoteResult => ({
gasEstimate: BigNumber.from(0),
gasEstimate,
route,
amountIn: newAmountFromString('100', uniswapTokenToERC20(route.input)),
amountOut: newAmountFromString('1000', uniswapTokenToERC20(route.output)),
Expand Down Expand Up @@ -141,53 +148,103 @@ describe('getSwap', () => {
});
});

describe('prepareSwap', () => {
describe('adjustQuoteWithFees', () => {
describe('when the trade type is exact input', () => {
it('should use the specified amount for the amountIn', async () => {
const quote = buildExactInputQuote();
const userSpecifiedAmountIn = newAmountFromString('100', quote.amountIn.token);

const preparedSwap = prepareSwap(quote, quote.amountIn, new Fees([], IMX_TEST_TOKEN));
const preparedSwap = adjustQuoteWithFees(
quote,
userSpecifiedAmountIn,
new Fees([], userSpecifiedAmountIn.token),
nativeTokenService,
);

expect(formatAmount(preparedSwap.amountIn)).toEqual(formatAmount(quote.amountIn));
expect(formatAmount(preparedSwap.amountIn)).toEqual(formatAmount(userSpecifiedAmountIn));
});

it('should use the quoted amount for the amountOut', async () => {
const quote = buildExactInputQuote();
const userSpecifiedAmountIn = newAmountFromString('100', quote.amountIn.token);

const preparedSwap = prepareSwap(quote, quote.amountIn, new Fees([], IMX_TEST_TOKEN));
const preparedSwap = adjustQuoteWithFees(
quote,
userSpecifiedAmountIn,
new Fees([], userSpecifiedAmountIn.token),
nativeTokenService,
);

expect(formatAmount(preparedSwap.amountOut)).toEqual(formatAmount(quote.amountOut));
});

describe('with fees', () => {
it('does not apply fees to any amount', async () => {
const quote = buildExactInputQuote();
const userSpecifiedAmountIn = newAmountFromString('100', quote.amountIn.token);

const preparedSwap = prepareSwap(
const preparedSwap = adjustQuoteWithFees(
quote,
quote.amountIn,
new Fees([{ recipient: TEST_FEE_RECIPIENT, basisPoints: 1000 }], IMX_TEST_TOKEN), // 1% fee
userSpecifiedAmountIn,
new Fees([{ recipient: TEST_FEE_RECIPIENT, basisPoints: 1000 }], userSpecifiedAmountIn.token), // 1% fee
nativeTokenService,
);

expect(formatAmount(preparedSwap.amountIn)).toEqual(formatAmount(quote.amountIn));
expect(formatAmount(preparedSwap.amountIn)).toEqual(formatAmount(userSpecifiedAmountIn));
expect(formatAmount(preparedSwap.amountOut)).toEqual(formatAmount(quote.amountOut));
});
});

describe('when the user specified tokenIn is native', () => {
it('wraps it and uses it as the amountIn', () => {
const quote: QuoteResult = {
gasEstimate,
route,
amountIn: newAmountFromString('9', nativeTokenService.wrappedToken), // has been wrapped
amountOut: newAmountFromString('1', FUN_TEST_TOKEN),
tradeType: TradeType.EXACT_INPUT,
};
const userSpecifiedAmountIn = newAmountFromString('10', nativeTokenService.nativeToken);

const preparedSwap = adjustQuoteWithFees(
quote,
userSpecifiedAmountIn,
new Fees([], userSpecifiedAmountIn.token),
nativeTokenService,
);

expect(formatAmount(preparedSwap.amountIn)).toEqual('10.0');
});
});
});

describe('when the trade type is exact output', () => {
it('should use the quoted amount for the amountIn', async () => {
const quote = buildExactOutputQuote();
// In this case, the user-specified amount is always equal to the amountOut in the quote
const userSpecifiedAmountOut = quote.amountOut;

const preparedSwap = prepareSwap(quote, quote.amountOut, new Fees([], IMX_TEST_TOKEN));
const preparedSwap = adjustQuoteWithFees(
quote,
userSpecifiedAmountOut,
new Fees([], quote.amountIn.token),
nativeTokenService,
);

expect(formatAmount(preparedSwap.amountIn)).toEqual(formatAmount(quote.amountIn));
});

it('should use the specified amount for the amountOut', async () => {
it('should use the amountOut from the quote for the amountOut', async () => {
const quote = buildExactOutputQuote();
// In this case, the user-specified amount is always equal to the amountOut in the quote
const userSpecifiedAmountOut = quote.amountOut;

const preparedSwap = prepareSwap(quote, quote.amountOut, new Fees([], IMX_TEST_TOKEN));
const preparedSwap = adjustQuoteWithFees(
quote,
userSpecifiedAmountOut,
new Fees([], quote.amountIn.token),
nativeTokenService,
);

expect(formatAmount(preparedSwap.amountOut)).toEqual(formatAmount(quote.amountOut));
});
Expand All @@ -196,16 +253,42 @@ describe('prepareSwap', () => {
it('applies fees to the quoted amount', async () => {
const quote = buildExactOutputQuote();
quote.amountOut.value = utils.parseEther('100');
// In this case, the user-specified amount is always equal to the amountOut in the quote
const userSpecifiedAmountOut = quote.amountOut;

const preparedSwap = prepareSwap(
const preparedSwap = adjustQuoteWithFees(
quote,
quote.amountOut,
new Fees([{ recipient: TEST_FEE_RECIPIENT, basisPoints: 1000 }], IMX_TEST_TOKEN), // 1% fee
userSpecifiedAmountOut,
new Fees([{ recipient: TEST_FEE_RECIPIENT, basisPoints: 1000 }], quote.amountIn.token), // 10% fee
nativeTokenService,
);

expect(formatAmount(preparedSwap.amountIn)).toEqual('110.0'); // quotedAmount + 1% fee
expect(formatAmount(preparedSwap.amountOut)).toEqual(formatAmount(quote.amountOut));
});
});

describe('when the tokenIn is native', () => {
// Want to buy 1 FUN in exchange for native IMX
it('applies fees to the amountIn', () => {
const quote: QuoteResult = {
gasEstimate,
route,
amountIn: newAmountFromString('10', nativeTokenService.wrappedToken), // has been wrapped
amountOut: newAmountFromString('1', FUN_TEST_TOKEN),
tradeType: TradeType.EXACT_OUTPUT,
};
const userSpecifiedAmountOut = quote.amountOut;

const fees = tenPercentFees(NATIVE_TEST_TOKEN);
const preparedSwap = adjustQuoteWithFees(quote, userSpecifiedAmountOut, fees, nativeTokenService);

expectERC20(preparedSwap.amountIn.token, nativeTokenService.wrappedToken.address);
expect(formatAmount(preparedSwap.amountIn)).toEqual('11.0');

expectERC20(preparedSwap.amountOut.token, FUN_TEST_TOKEN.address);
expect(formatAmount(preparedSwap.amountOut)).toEqual('1.0');
});
});
});
});
Loading

0 comments on commit 3391980

Please sign in to comment.