From 1ed60c3524830137718dc0990bc631224ee17ab1 Mon Sep 17 00:00:00 2001 From: gitshreevatsa Date: Fri, 17 Jul 2026 23:12:25 +0530 Subject: [PATCH] fix: fee calculation --- .../NearIntentsBridgeProvider.test.ts | 115 +++++++++++++++++- .../near-intents/NearIntentsBridgeProvider.ts | 9 +- 2 files changed, 116 insertions(+), 8 deletions(-) diff --git a/packages/bridging/src/providers/near-intents/NearIntentsBridgeProvider.test.ts b/packages/bridging/src/providers/near-intents/NearIntentsBridgeProvider.test.ts index 7700a35ea..a29ff77fc 100644 --- a/packages/bridging/src/providers/near-intents/NearIntentsBridgeProvider.test.ts +++ b/packages/bridging/src/providers/near-intents/NearIntentsBridgeProvider.test.ts @@ -438,10 +438,11 @@ adapterNames.forEach((adapterName) => { const testQuoteHash = '0xtestFeeCurrencyQuoteHash' // Sell token (USDC, 6 decimals) and buy token (ETH, 18 decimals) differ hugely in scale, - // and amountInUsd !== amountOutUsd so the slippage-derived fees are non-zero. This makes a - // sell/buy-currency swap observable (with equal USD values, slippage is 0 and the swap hides). + // and amountOut !== minAmountOut so the slippage-derived fees are non-zero. This makes a + // sell/buy-currency swap observable (with zero slippage, the fee is 0 and the swap hides). const amountIn = '52000000' // 52 USDC (6 decimals) const amountOut = '11760237526222378' // ~0.01176 ETH (18 decimals) + const minAmountOut = '11701433538591266' const amountInUsd = '51.9897' const amountOutUsd = '51.9508' @@ -454,7 +455,7 @@ adapterNames.forEach((adapterName) => { amountOut, amountOutFormatted: '0.011760237526222378', amountOutUsd, - minAmountOut: '11701433538591266', + minAmountOut, timeEstimate: 60, deadline: '2025-09-05T12:10:38.605Z', timeWhenInactive: '2025-09-05T12:10:38.605Z', @@ -528,7 +529,7 @@ adapterNames.forEach((adapterName) => { signer: '0x0000000000000000000000000000000000000000', }) - const slippage = 1 - Number(amountOutUsd) / Number(amountInUsd) + const slippage = (Number(amountOut) - Number(minAmountOut)) / Number(amountOut) const bridgingFee = quote.amountsAndCosts.costs.bridgingFee // The sell-currency fee must be denominated in the sell token (USDC, 6 decimals) and thus @@ -696,6 +697,112 @@ adapterNames.forEach((adapterName) => { expect(quote.amountsAndCosts.beforeFee.buyAmount).toBe(10000n) }) + it('should compute a sane BTC-denominated fee for a real production BTC bridge quote', async () => { + // Regression test for a swapped-currency bug: the BTC-denominated fee was + // being computed from the USDC-side amount (and vice versa), which is invisible + // on same-magnitude routes but produces an absurd result on BTC (8 decimals). + // Numbers below are the real quote/settlement for deposit address + // 0x712bf81469904cee52f5ba897fd51a338f6b73e4, see + // https://explorer.near-intents.org/transactions/0x712bf81469904cee52f5ba897fd51a338f6b73e4 + const api = new NearIntentsApi() + const sellTokenAddress = '0xaf88d065e77c8cc2239327c5edb3a432268e5831' + const buyTokenAddress = BTC_CURRENCY_ADDRESS + const testQuoteHash = '0xtestRealBtcTxQuoteHash' + + const mockQuoteResponse: QuoteResponse = { + quote: { + amountIn: '5945707', + amountInFormatted: '5.945707', + amountInUsd: '5.9446', + minAmountIn: '5915978', + amountOut: '7047', + amountOutFormatted: '0.00007047', + amountOutUsd: '4.6811', + minAmountOut: '7011', + timeEstimate: 470, + deadline: '2026-06-19T12:03:12.000Z', + timeWhenInactive: '2026-06-19T12:03:12.000Z', + depositAddress: '0x712bF81469904cEE52f5ba897Fd51A338f6b73e4', + }, + quoteRequest: { + dry: false, + swapType: QuoteRequest.swapType.FLEX_INPUT, + depositMode: QuoteRequest.depositMode.SIMPLE, + slippageTolerance: 50, + originAsset: 'nep141:arb-0xaf88d065e77c8cc2239327c5edb3a432268e5831.omft.near', + depositType: QuoteRequest.depositType.ORIGIN_CHAIN, + destinationAsset: '1cs_v1:btc:native:coin', + amount: '5945707', + refundTo: '0x0000000000000000000000000000000000000000', + refundType: QuoteRequest.refundType.ORIGIN_CHAIN, + recipient: 'bc1qray0vz42y0sl4m0qwar58yel6lure25q8f22cn', + recipientType: QuoteRequest.recipientType.DESTINATION_CHAIN, + deadline: '2026-06-19T12:03:12.000Z', + }, + signature: 'ed25519:testRealBtcSignature', + timestamp: '2026-06-16T11:33:13.082Z', + } + + jest.spyOn(api, 'getQuote').mockResolvedValue(mockQuoteResponse) + jest.spyOn(api, 'getTokens').mockResolvedValue([ + { + assetId: 'nep141:arb-0xaf88d065e77c8cc2239327c5edb3a432268e5831.omft.near', + decimals: 6, + blockchain: TokenResponse.blockchain.ARB, + symbol: 'USDC', + price: 1, + priceUpdatedAt: '2026-06-16T11:33:13.082Z', + contractAddress: sellTokenAddress, + }, + { + assetId: '1cs_v1:btc:native:coin', + decimals: 8, + blockchain: TokenResponse.blockchain.BTC, + symbol: 'BTC(OMNI)', + price: 66432, + priceUpdatedAt: '2026-06-16T11:33:13.082Z', + contractAddress: 'coin', + }, + ]) + jest.spyOn(api, 'getAttestation').mockResolvedValue({ + version: 1, + signature: + '0x66edc32e2ab001213321ab7d959a2207fcef5190cc9abb6da5b0d2a8a9af2d4d2b0700e2c317c4106f337fd934fbbb0bf62efc8811a78603b33a8265d3b8f8cb1c', + }) + provider.setApi(api) + + jest.spyOn(provider, 'recoverDepositAddress').mockResolvedValue({ + address: ATTESTATOR_ADDRESS, + quoteHash: testQuoteHash, + stringifiedQuote: '', + attestationSignature: '', + }) + + const quote = await provider.getQuote({ + kind: OrderKind.SELL, + sellTokenChainId: SupportedChainId.ARBITRUM_ONE, + sellTokenAddress, + sellTokenDecimals: 6, + buyTokenChainId: NonEvmChains.BITCOIN as number, + buyTokenAddress, + buyTokenDecimals: 8, + amount: 5945707n, + account: '0x0000000000000000000000000000000000000000', + appCode: 'test', + signer: '0x0000000000000000000000000000000000000000', + }) + + // The BTC-denominated fee must never exceed the BTC amount being bridged (7047 sats). + expect(quote.fees.bridgeFee).toBeLessThan(quote.amountsAndCosts.beforeFee.buyAmount) + expect(quote.amountsAndCosts.costs.bridgingFee.amountInBuyCurrency).toBeLessThan( + quote.amountsAndCosts.beforeFee.buyAmount, + ) + // With the swapped-currency bug this was 1,263,735 sats (bigger than the whole trade). + // Fixed value derived from amountOut (7047) vs minAmountOut (7011). + expect(quote.fees.bridgeFee).toBe(36n) + expect(quote.amountsAndCosts.slippageBps).toBe(51) + }) + it('should return quote when destination asset is solana', async () => { const api = new NearIntentsApi() const sellTokenAddress = '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913' diff --git a/packages/bridging/src/providers/near-intents/NearIntentsBridgeProvider.ts b/packages/bridging/src/providers/near-intents/NearIntentsBridgeProvider.ts index 424ba3b1d..fa6c598cf 100644 --- a/packages/bridging/src/providers/near-intents/NearIntentsBridgeProvider.ts +++ b/packages/bridging/src/providers/near-intents/NearIntentsBridgeProvider.ts @@ -174,12 +174,13 @@ export class NearIntentsBridgeProvider implements ReceiverAccountBridgeProvider< const { quote, timestamp: isoDate } = quoteResponse - const payoutRatio = Number(quote.amountOutUsd) / Number(quote.amountInUsd) - const slippage = 1 - payoutRatio + const amountOut = Number(quote.amountOut) + const minAmountOut = Number(quote.minAmountOut) + const slippage = amountOut > 0 ? (amountOut - minAmountOut) / amountOut : 0 const slippageBps = Math.trunc(slippage * 10_000) - const feeAmountInBuyCurrency = Math.trunc(Number(quote.amountOut) * slippage) + const feeAmountInBuyCurrency = Math.trunc(amountOut * slippage) const feeAmountInSellCurrency = Math.trunc(Number(quote.amountIn) * slippage) - const bridgeFee = Math.trunc(Number(quote.amountIn) * slippage) + const bridgeFee = feeAmountInBuyCurrency return { id: recoveredDepositAddress.quoteHash,