-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: utility to calculate quote amounts and costs (#210)
- Loading branch information
Showing
5 changed files
with
339 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
import { OrderParameters, OrderKind, SigningScheme, BuyTokenDestination, SellTokenSource } from './generated' | ||
import { getQuoteAmountsAndCosts } from './quoteAmountsAndCostsUtils' | ||
|
||
const otherFields = { | ||
buyToken: '0xdef1ca1fb7fbcdc777520aa7f396b4e015f497ab', | ||
sellToken: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', | ||
buyTokenBalance: BuyTokenDestination.ERC20, | ||
sellTokenBalance: SellTokenSource.ERC20, | ||
signingScheme: SigningScheme.EIP712, | ||
partiallyFillable: false, | ||
receiver: '0x0000000000000000000000000000000000000000', | ||
validTo: 1716904696, | ||
appData: '{}', | ||
appDataHash: '0x0', | ||
} | ||
|
||
const sellDecimals = 18 | ||
const buyDecimals = 6 | ||
|
||
/** | ||
* Since we have partner fees, now it's not clear what does it mean `feeAmount`? | ||
* To avoid confusion, we should consider this `feeAmount` as `networkCosts` | ||
* | ||
* Fee is always taken from sell token (for sell/buy orders): | ||
* 3855544038281082 + 156144455961718918 = 160000000000000000 | ||
* | ||
* Again, to avoid confusion, we should take this `sellAmount` as `sellAmountBeforeNetworkCosts` | ||
* Hence, `buyAmount` is `buyAmountAfterNetworkCosts` because this amount is what you will get for the sell amount | ||
* | ||
* In this order we are selling 0.16 WETH for 1863 COW - network costs | ||
*/ | ||
const SELL_ORDER: OrderParameters = { | ||
kind: OrderKind.SELL, | ||
sellAmount: '156144455961718918', | ||
feeAmount: '3855544038281082', | ||
buyAmount: '18632013982', | ||
...otherFields, | ||
} | ||
|
||
/** | ||
* In this order we are buying 2000 COW for 1.6897 WETH + network costs | ||
*/ | ||
const BUY_ORDER: OrderParameters = { | ||
kind: OrderKind.BUY, | ||
sellAmount: '168970833896526983', | ||
feeAmount: '2947344072902629', | ||
buyAmount: '2000000000', | ||
...otherFields, | ||
} | ||
|
||
describe('Calculation of before/after fees amounts', () => { | ||
describe('Network costs', () => { | ||
describe.each(['sell', 'buy'])('%s order', (type: string) => { | ||
const orderParams = type === 'sell' ? SELL_ORDER : BUY_ORDER | ||
|
||
it('Sell amount after network costs should be sellAmount + feeAmount', () => { | ||
const result = getQuoteAmountsAndCosts({ | ||
orderParams, | ||
sellDecimals, | ||
buyDecimals, | ||
slippagePercentBps: 0, | ||
partnerFeeBps: undefined, | ||
}) | ||
|
||
expect(result.afterNetworkCosts.sellAmount.toString()).toBe( | ||
String(BigInt(orderParams.sellAmount) + BigInt(orderParams.feeAmount)) | ||
) | ||
}) | ||
|
||
it('Buy amount before network costs should be SellAmountAfterNetworkCosts * Price', () => { | ||
const result = getQuoteAmountsAndCosts({ | ||
orderParams, | ||
sellDecimals, | ||
buyDecimals, | ||
slippagePercentBps: 0, | ||
partnerFeeBps: undefined, | ||
}) | ||
|
||
expect(result.beforeNetworkCosts.buyAmount.toString()).toBe( | ||
( | ||
(+orderParams.sellAmount + +orderParams.feeAmount) * // SellAmountAfterNetworkCosts | ||
(+orderParams.buyAmount / +orderParams.sellAmount) | ||
) // Price | ||
.toFixed() | ||
) | ||
}) | ||
}) | ||
}) | ||
|
||
describe('Partner fee', () => { | ||
const partnerFeeBps = 100 | ||
|
||
describe('Sell order', () => { | ||
it('Partner fee should be substracted from buy amount after network costs', () => { | ||
const orderParams = SELL_ORDER | ||
const result = getQuoteAmountsAndCosts({ | ||
orderParams, | ||
sellDecimals, | ||
buyDecimals, | ||
partnerFeeBps, | ||
slippagePercentBps: 0, | ||
}) | ||
|
||
const buyAmountBeforeNetworkCosts = | ||
(+orderParams.sellAmount + +orderParams.feeAmount) * // SellAmountAfterNetworkCosts | ||
(+orderParams.buyAmount / +orderParams.sellAmount) // Price | ||
|
||
const partnerFeeAmount = Math.floor((buyAmountBeforeNetworkCosts * partnerFeeBps) / 100 / 100) | ||
|
||
expect(Number(result.costs.partnerFee.amount)).toBe(partnerFeeAmount) | ||
}) | ||
}) | ||
|
||
describe('Buy order', () => { | ||
it('Partner fee should be added on top of sell amount after network costs', () => { | ||
const orderParams = BUY_ORDER | ||
const result = getQuoteAmountsAndCosts({ | ||
orderParams, | ||
sellDecimals, | ||
buyDecimals, | ||
partnerFeeBps, | ||
slippagePercentBps: 0, | ||
}) | ||
|
||
const partnerFeeAmount = Math.floor((+orderParams.sellAmount * partnerFeeBps) / 100 / 100) | ||
|
||
expect(Number(result.costs.partnerFee.amount)).toBe(partnerFeeAmount) | ||
}) | ||
}) | ||
}) | ||
|
||
describe('Slippage', () => { | ||
const slippagePercentBps = 200 // 2% | ||
|
||
describe('Sell order', () => { | ||
it('Slippage should be substracted from buy amount after partner fees', () => { | ||
const orderParams = SELL_ORDER | ||
const result = getQuoteAmountsAndCosts({ | ||
orderParams, | ||
sellDecimals, | ||
buyDecimals, | ||
partnerFeeBps: undefined, | ||
slippagePercentBps, | ||
}) | ||
|
||
const buyAmountAfterNetworkCosts = +orderParams.buyAmount | ||
|
||
const slippageAmount = (buyAmountAfterNetworkCosts * slippagePercentBps) / 100 / 100 | ||
|
||
expect(Number(result.afterSlippage.buyAmount)).toBe(Math.ceil(buyAmountAfterNetworkCosts - slippageAmount)) | ||
}) | ||
}) | ||
|
||
describe('Buy order', () => { | ||
it('Slippage should be added on top of sell amount after partner costs', () => { | ||
const orderParams = BUY_ORDER | ||
const result = getQuoteAmountsAndCosts({ | ||
orderParams, | ||
sellDecimals, | ||
buyDecimals, | ||
partnerFeeBps: undefined, | ||
slippagePercentBps, | ||
}) | ||
|
||
const sellAmountAfterNetworkCosts = +orderParams.sellAmount + +orderParams.feeAmount | ||
const slippageAmount = (sellAmountAfterNetworkCosts * slippagePercentBps) / 100 / 100 | ||
|
||
// We are loosing precision here, because of using numbers and we have to use toBeCloseTo() | ||
expect(Number(result.afterSlippage.sellAmount)).toBeCloseTo(sellAmountAfterNetworkCosts + slippageAmount, -2) | ||
}) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import { QuoteAmountsAndCosts } from './types' | ||
import { OrderKind, type OrderParameters } from './generated' | ||
|
||
interface Params { | ||
orderParams: OrderParameters | ||
sellDecimals: number | ||
buyDecimals: number | ||
slippagePercentBps: number | ||
partnerFeeBps: number | undefined | ||
} | ||
|
||
export function getQuoteAmountsAndCosts(params: Params): QuoteAmountsAndCosts { | ||
const { orderParams, sellDecimals, buyDecimals, slippagePercentBps } = params | ||
const partnerFeeBps = params.partnerFeeBps ?? 0 | ||
const isSell = orderParams.kind === OrderKind.SELL | ||
/** | ||
* Wrap raw values into CurrencyAmount objects | ||
* We also make amounts names more specific with "beforeNetworkCosts" and "afterNetworkCosts" suffixes | ||
*/ | ||
const networkCostAmount = getBigNumber(orderParams.feeAmount, sellDecimals) | ||
const sellAmountBeforeNetworkCosts = getBigNumber(orderParams.sellAmount, sellDecimals) | ||
const buyAmountAfterNetworkCosts = getBigNumber(orderParams.buyAmount, buyDecimals) | ||
|
||
/** | ||
* This is an actual price of the quote since it's derrived only from the quote sell and buy amounts | ||
*/ | ||
const quotePrice = buyAmountAfterNetworkCosts.num / sellAmountBeforeNetworkCosts.num | ||
|
||
/** | ||
* Before networkCosts + networkCosts = After networkCosts :) | ||
*/ | ||
const sellAmountAfterNetworkCosts = getBigNumber( | ||
sellAmountBeforeNetworkCosts.big + networkCostAmount.big, | ||
sellDecimals | ||
) | ||
|
||
/** | ||
* Since the quote contains only buy amount after network costs | ||
* we have to calculate the buy amount before network costs from the quote price | ||
*/ | ||
const buyAmountBeforeNetworkCosts = getBigNumber(quotePrice * sellAmountAfterNetworkCosts.num, buyDecimals) | ||
|
||
/** | ||
* Partner fee is always added on the surplus amount, for sell-orders it's buy amount, for buy-orders it's sell amount | ||
*/ | ||
const surplusAmount = isSell ? buyAmountBeforeNetworkCosts.big : sellAmountBeforeNetworkCosts.big | ||
const partnerFeeAmount = partnerFeeBps > 0 ? surplusAmount / BigInt(partnerFeeBps) : BigInt(0) | ||
|
||
/** | ||
* Partner fee is always added on the surplus token, for sell-orders it's buy token, for buy-orders it's sell token | ||
*/ | ||
const afterPartnerFees = isSell | ||
? { | ||
sellAmount: sellAmountAfterNetworkCosts.big, | ||
buyAmount: buyAmountAfterNetworkCosts.big - partnerFeeAmount, | ||
} | ||
: { | ||
sellAmount: sellAmountAfterNetworkCosts.big + partnerFeeAmount, | ||
buyAmount: buyAmountAfterNetworkCosts.big, | ||
} | ||
|
||
const getSlippageAmount = (amount: bigint) => (amount * BigInt(slippagePercentBps)) / BigInt(100 * 100) | ||
|
||
/** | ||
* Same rules apply for slippage as for partner fees | ||
*/ | ||
const afterSlippage = isSell | ||
? { | ||
sellAmount: afterPartnerFees.sellAmount, | ||
buyAmount: afterPartnerFees.buyAmount - getSlippageAmount(afterPartnerFees.buyAmount), | ||
} | ||
: { | ||
sellAmount: afterPartnerFees.sellAmount + getSlippageAmount(afterPartnerFees.sellAmount), | ||
buyAmount: afterPartnerFees.buyAmount, | ||
} | ||
|
||
return { | ||
isSell, | ||
costs: { | ||
networkFee: { | ||
amountInSellCurrency: networkCostAmount.big, | ||
amountInBuyCurrency: getBigNumber(quotePrice * networkCostAmount.num, buyDecimals).big, | ||
}, | ||
partnerFee: { | ||
amount: partnerFeeAmount, | ||
bps: partnerFeeBps, | ||
}, | ||
}, | ||
beforeNetworkCosts: { | ||
sellAmount: sellAmountBeforeNetworkCosts.big, | ||
buyAmount: buyAmountBeforeNetworkCosts.big, | ||
}, | ||
afterNetworkCosts: { | ||
sellAmount: sellAmountAfterNetworkCosts.big, | ||
buyAmount: buyAmountAfterNetworkCosts.big, | ||
}, | ||
afterPartnerFees, | ||
afterSlippage, | ||
} | ||
} | ||
|
||
type BigNumber = { | ||
big: bigint | ||
num: number | ||
} | ||
|
||
/** | ||
* BigInt works well with subtraction and addition, but it's not very good with multiplication and division | ||
* To multiply/divide token amounts we have to convert them to numbers, but we have to be careful with precision | ||
* @param value | ||
* @param decimals | ||
*/ | ||
function getBigNumber(value: string | bigint | number, decimals: number): BigNumber { | ||
if (typeof value === 'number') { | ||
const bigAsNumber = value * 10 ** decimals | ||
const bigAsNumberString = bigAsNumber.toFixed() | ||
const big = BigInt(bigAsNumberString.includes('e') ? bigAsNumber : bigAsNumberString) | ||
|
||
return { big, num: value } | ||
} | ||
|
||
const big = BigInt(value) | ||
const num = Number(big) / 10 ** decimals | ||
|
||
return { big, num } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters