Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: native support for fees in DEX #1017

Merged
merged 3 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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