diff --git a/packages/sdk-router/src/rfq/fastBridgeRouter.test.ts b/packages/sdk-router/src/rfq/fastBridgeRouter.test.ts index 1e3ac90583..172d1aa332 100644 --- a/packages/sdk-router/src/rfq/fastBridgeRouter.test.ts +++ b/packages/sdk-router/src/rfq/fastBridgeRouter.test.ts @@ -17,6 +17,8 @@ jest.mock('@ethersproject/contracts', () => { interface: args[1], bridgeRelays: jest.fn(), fastBridge: jest.fn(), + getOriginAmountOut: jest.fn(), + protocolFeeRate: jest.fn(), populateTransaction: { bridge: actualInstance.populateTransaction.bridge, }, @@ -223,4 +225,106 @@ describe('FastBridgeRouter', () => { createBridgeTest(fastBridgeRouter, bridgeParams, originQuery, destQuery) }) }) + + describe('getOriginAmountOut', () => { + const mockQueryFragment = { + routerAdapter: '1', + deadline: BigNumber.from(2), + rawParams: '3', + } + + const mockTokenIn = '0xA' + const mockRfqTokens = ['0xA', '0xB'] + + beforeAll(() => { + jest + .spyOn(fastBridgeRouter['routerContract'], 'getOriginAmountOut') + .mockImplementation((token, rfqTokens, amountIn) => + Promise.resolve( + token === mockTokenIn + ? rfqTokens.map((rfqToken, index) => { + const query = { + ...mockQueryFragment, + tokenOut: rfqToken, + minAmountOut: BigNumber.from(amountIn).mul(index + 1), + } + const tuple: [string, string, BigNumber, BigNumber, string] = + [ + query.routerAdapter, + query.tokenOut, + query.minAmountOut, + query.deadline, + query.rawParams, + ] + return Object.assign(tuple, { + routerAdapter: query.routerAdapter, + tokenOut: query.tokenOut, + minAmountOut: query.minAmountOut, + deadline: query.deadline, + rawParams: query.rawParams, + }) + }) + : [] + ) + ) + }) + + it('Returns correct values with protocol fee = 0 bps', async () => { + jest + .spyOn(fastBridgeRouter['fastBridgeContractCache']!, 'protocolFeeRate') + .mockImplementation(() => Promise.resolve(BigNumber.from(0))) + const result = await fastBridgeRouter.getOriginAmountOut( + mockTokenIn, + mockRfqTokens, + 1_000_001 + ) + expect(result).toEqual([ + { + tokenOut: '0xA', + minAmountOut: BigNumber.from(1_000_001), + ...mockQueryFragment, + }, + { + tokenOut: '0xB', + minAmountOut: BigNumber.from(2_000_002), + ...mockQueryFragment, + }, + ]) + }) + + it('Returns correct values with protocol fee = 10 bps', async () => { + // protocolFeeRate uses 10**6 as the denominator + jest + .spyOn(fastBridgeRouter['fastBridgeContractCache']!, 'protocolFeeRate') + .mockImplementation(() => Promise.resolve(BigNumber.from(1000))) + const result = await fastBridgeRouter.getOriginAmountOut( + mockTokenIn, + mockRfqTokens, + 1_000_001 + ) + // Protocol fees should have no effect on the result + expect(result).toEqual([ + { + tokenOut: '0xA', + minAmountOut: BigNumber.from(1_000_001), + ...mockQueryFragment, + }, + { + tokenOut: '0xB', + minAmountOut: BigNumber.from(2_000_002), + ...mockQueryFragment, + }, + ]) + }) + }) + + describe('getProtocolFeeRate', () => { + it('Returns correct value', async () => { + jest + .spyOn(fastBridgeRouter['fastBridgeContractCache']!, 'protocolFeeRate') + .mockImplementation(() => Promise.resolve(BigNumber.from(1000))) + const result = await fastBridgeRouter.getProtocolFeeRate() + expect(result).toEqual(BigNumber.from(1000)) + }) + }) }) diff --git a/packages/sdk-router/src/rfq/fastBridgeRouter.ts b/packages/sdk-router/src/rfq/fastBridgeRouter.ts index 6ed1bdf4cc..f1adc10ba8 100644 --- a/packages/sdk-router/src/rfq/fastBridgeRouter.ts +++ b/packages/sdk-router/src/rfq/fastBridgeRouter.ts @@ -138,4 +138,12 @@ export class FastBridgeRouter implements SynapseModule { ) return queries.map(reduceToQuery) } + + /** + * @returns The protocol fee rate, multiplied by 1_000_000 (e.g. 1 basis point = 100). + */ + public async getProtocolFeeRate(): Promise { + const fastBridgeContract = await this.getFastBridgeContract() + return fastBridgeContract.protocolFeeRate() + } } diff --git a/packages/sdk-router/src/rfq/fastBridgeRouterSet.test.ts b/packages/sdk-router/src/rfq/fastBridgeRouterSet.test.ts index 53e7d5b1ce..5cbeaeae48 100644 --- a/packages/sdk-router/src/rfq/fastBridgeRouterSet.test.ts +++ b/packages/sdk-router/src/rfq/fastBridgeRouterSet.test.ts @@ -101,4 +101,20 @@ describe('FastBridgeRouterSet', () => { ) }) }) + + describe('applyProtocolFeeRate', () => { + const amount = BigNumber.from(1_000_001) + + it('Applies 0 bps fee rate', () => { + const protocolFeeRate = BigNumber.from(0) + const result = routerSet.applyProtocolFeeRate(amount, protocolFeeRate) + expect(result).toEqual(amount) + }) + + it('Applies 10 bps fee rate', () => { + const protocolFeeRate = BigNumber.from(1_000) + const result = routerSet.applyProtocolFeeRate(amount, protocolFeeRate) + expect(result).toEqual(BigNumber.from(999_001)) + }) + }) }) diff --git a/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts b/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts index 9e39e743c3..bb52326167 100644 --- a/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts +++ b/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts @@ -112,13 +112,19 @@ export class FastBridgeRouterSet extends SynapseModuleSet { amountIn, allQuotes ) + const protocolFeeRate = await this.getFastBridgeRouter( + originChainId + ).getProtocolFeeRate() return filteredQuotes .map(({ quote, originQuery }) => ({ quote, originQuery, - // Apply quote to the proceeds of the origin swap + // Apply quote to the proceeds of the origin swap with protocol fee applied // TODO: handle optional gas airdrop pricing - destAmountOut: applyQuote(quote, originQuery.minAmountOut), + destAmountOut: applyQuote( + quote, + this.applyProtocolFeeRate(originQuery.minAmountOut, protocolFeeRate) + ), })) .filter(({ destAmountOut }) => destAmountOut.gt(0)) .map(({ quote, originQuery, destAmountOut }) => ({ @@ -228,6 +234,19 @@ export class FastBridgeRouterSet extends SynapseModuleSet { return fastBridgeContract.address } + /** + * Applies the protocol fee to the amount. + * + * @returns The amount after the fee. + */ + public applyProtocolFeeRate( + amount: BigNumber, + protocolFeeRate: BigNumber + ): BigNumber { + const protocolFee = amount.mul(protocolFeeRate).div(1_000_000) + return amount.sub(protocolFee) + } + /** * Filters the list of quotes to only include those that can be used for given amount of input token. * For every filtered quote, the origin query is returned with the information for tokenIn -> RFQ token swaps.