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(sdk-router)!: add support for FastBridgeRouterV2 #2957

Merged
merged 17 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from 15 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
34 changes: 32 additions & 2 deletions packages/sdk-router/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,26 @@ const synapseSDK = new SynapseSDK(chainIds, providers)

## Bridging

### Router Deployments

The Routers for the same module are deployed to the same address on all chains (except for the exceptions below).

| Bridge Module | Chain | Address |
| ------------- | --------- | -------------------------------------------- |
| SynapseBridge | \* | `0x7E7A0e201FD38d3ADAA9523Da6C109a07118C96a` |
| SynapseBridge | **Blast** | `0x0000000000365b1d5B142732CF4d33BcddED21Fc` |
| SynapseCCTP | \* | `0xd5a597d6e7ddf373a92C8f477DAAA673b0902F48` |
| SynapseRFQ | \* | `0x00cD000000003f7F682BE4813200893d4e690000` |

### Router Deployments (deprecated)

The following deployments are deprecated and should not be used. Use the newest deployments from the table above instead.

| Bridge Module | Chain | Address |
| ------------- | --------- | ------------------------------------------------ |
| SynapseBridge | **Blast** | ~~`0x7E7A0e201FD38d3ADAA9523Da6C109a07118C96a`~~ |
| SynapseRFQ | \* | ~~`0x0000000000489d89D2B233D3375C045dfD05745F`~~ |

### Full bridging workflow

![Bridging Workflow](./puml/BridgingWorkflow.png)
Expand Down Expand Up @@ -77,11 +97,21 @@ const bridgeQuotes: BridgeQuote[] = await synapseSDK.allBridgeQuotes(
tokenOut,
// Amount of tokens to bridge, in origin token decimals: 1_000_000_000
amountIn,
// Deadline for the transaction to be initiated on the origin chain, in seconds (optional)
deadline
{
// Deadline for the transaction to be initiated on the origin chain, in seconds (optional)
deadline: 1234567890,
// List of bridge modules to exclude from the result, optional.
// Empty list means that all modules are included.
excludedModules: ['SynapseBridge', 'SynapseCCTP', 'SynapseRFQ'],
// Address of the user on the origin chain, optional.
// MANDATORY if a smart contract is going to initiate the bridge operation on behalf of the user.
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
originUserAddress: '0x1234567890abcdef1234567890abcdef12345678',
}
)
```

> **Note:** The `originUserAddress` MUST BE provided, if a smart contract is going to initiate the bridge operation on behalf of the user. That includes smart wallets (like Safe), or a third party integration (like a bridge aggregator smart contract).
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved

The returned list is sorted by the `maxAmountOut` field, so the first quote is the one yielding the highest amount of tokens on the destination chain.

If either of the input/output tokens is a native gas token (e.g. ETH on Ethereum/Arbitrum, AVAX on Avalanche, etc.), the specialized address `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` should be used instead of the token address.
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk-router/src/constants/addresses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const CCTP_ROUTER_ADDRESS_MAP: AddressMap = generateAddressMap(
/**
* FastBridgeRouter contract address for all chains except ones from FAST_BRIDGE_ROUTER_EXCEPTION_MAP.
*/
const FAST_BRIDGE_ROUTER_ADDRESS = '0x0000000000489d89D2B233D3375C045dfD05745F'
const FAST_BRIDGE_ROUTER_ADDRESS = '0x00cD000000003f7F682BE4813200893d4e690000'
const FAST_BRIDGE_ROUTER_EXCEPTION_MAP: AddressMap = {}
export const FAST_BRIDGE_ROUTER_ADDRESS_MAP: AddressMap = generateAddressMap(
RFQ_SUPPORTED_CHAIN_IDS,
Expand Down
4 changes: 3 additions & 1 deletion packages/sdk-router/src/module/synapseModuleSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export abstract class SynapseModuleSet {
* @param tokenIn - The input token.
* @param tokenOut - The output token.
* @param amountIn - The amount of input token.
* @param originUserAddress - The address of the user on the origin chain.
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
*
* @returns - A list of BridgeRoute objects with the found routes.
*/
Expand All @@ -106,7 +107,8 @@ export abstract class SynapseModuleSet {
destChainId: number,
tokenIn: string,
tokenOut: string,
amountIn: BigintIsh
amountIn: BigintIsh,
originUserAddress?: string
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
): Promise<BridgeRoute[]>

/**
Expand Down
116 changes: 69 additions & 47 deletions packages/sdk-router/src/operations/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,24 @@ import {
} from '../module'

/**
* Executes a bridge operation between two different chains. Depending on the origin router address, the operation
* will use either a SynapseRouter or a SynapseCCTPRouter. This function creates a populated transaction ready
* to be signed and sent to the origin chain.
* Creates a populated bridge transaction ready for signing and submission to the origin chain.
* The method selects the appropriate router based on the origin router address:
* - `SynapseRouter` is used for SynapseBridge module
* - `SynapseCCTPRouter` is used for SynapseCCTP module
* - `FastBridgeRouter` is used for SynapseRFQ module
*
* @param to - The recipient address of the bridged tokens.
* @param originRouterAddress - The address of the origin router.
* @param originChainId - The ID of the origin chain.
* @param destChainId - The ID of the destination chain.
* @param token - The token to bridge.
* @param amount - The amount of token to bridge.
* @param originQuery - The query for the origin chain.
* @param destQuery - The query for the destination chain.
* @param to - Recipient address for the bridged tokens on the destination chain.
* @param originRouterAddress - Address of the router on the origin chain.
* @param originChainId - ID of the origin chain.
* @param destChainId - ID of the destination chain.
* @param token - Address of the token to be bridged.
* @param amount - Amount of tokens to bridge.
* @param originQuery - Query for the origin chain, obtained from `allBridgeQuotes()` or `bridgeQuote()`.
* @param destQuery - Query for the destination chain, obtained from `allBridgeQuotes()` or `bridgeQuote()`.
*
* @returns A promise that resolves to a populated transaction object which can be used to send the transaction.
* @returns A Promise resolving to a populated transaction object, ready for sending.
*
* @throws Will throw an error if there's an issue with the bridge operation.
* @throws Error if any issues arise during the bridge operation.
*/
export async function bridge(
this: SynapseSDK,
Expand Down Expand Up @@ -56,23 +58,36 @@ export async function bridge(
}

/**
* This method tries to fetch the best quote from either the Synapse Router or SynapseCCTP Router.
* It first handles the native token, then fetches the best quote for both types of routers.
* If the router addresses are valid for CCTP, it will fetch the quote from the CCTP routers, otherwise it will resolve to undefined.
* It waits for both types of quotes, then determines the best one by comparing the maximum output amount.
* If no best quote can be found, it will throw an error.
* Options for the bridgeQuote and allBridgeQuotes functions.
*
* @param originChainId - The ID of the original chain.
* @param destChainId - The ID of the destination chain.
* @param tokenIn - The input token.
* @param tokenOut - The output token.
* @param amountIn - The amount of input token.
* @param deadline - The transaction deadline, optional.
* @param excludeCCTP - Flag to exclude CCTP quotes from the result, optional and defaults to False.
* @param deadline - Optional transaction deadline on the origin chain.
* @param excludedModules - Optional array of module names to exclude from the quote.
* @param originUserAddress - Optional address of the user on the origin chain. This parameter must be
* specified if a smart contract will initiate the bridge operation on behalf of the user.
*/
interface BridgeQuoteOptions {
deadline?: BigNumber
excludedModules?: string[]
originUserAddress?: string
}
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved

/**
* Retrieves the best quote from all available bridge modules (SynapseBridge, SynapseCCTP, and SynapseRFQ).
* Users can customize the query by specifying a deadline, excluding certain modules, and providing the user's address on the origin chain.
*
* @returns - A promise that resolves to the best bridge quote.
* Important: The originUserAddress MUST be provided if a smart contract will initiate the bridge operation on the user's behalf.
* This applies to smart wallets (e.g., Safe) and third-party integrations (such as bridge aggregator smart contracts).
*
* @throws - Will throw an error if no best quote could be determined.
* @param originChainId - ID of the origin chain.
* @param destChainId - ID of the destination chain.
* @param tokenIn - Address of the token to be bridged from the origin chain.
* @param tokenOut - Address of the token to be received on the destination chain.
* @param amountIn - Amount of input tokens on the origin chain.
* @param options - Optional parameters including origin deadline, excludedModules, and originUserAddress.
*
* @returns A promise resolving to the best available bridge quote.
*
* @throws An error if no bridge route is found.
*/
export async function bridgeQuote(
this: SynapseSDK,
Expand All @@ -81,8 +96,7 @@ export async function bridgeQuote(
tokenIn: string,
tokenOut: string,
amountIn: BigintIsh,
deadline?: BigNumber,
excludeCCTP: boolean = false
options: BridgeQuoteOptions = {}
): Promise<BridgeQuote> {
// Get the quotes sorted by maxAmountOut
const allQuotes = await allBridgeQuotes.call(
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -92,30 +106,31 @@ export async function bridgeQuote(
tokenIn,
tokenOut,
amountIn,
deadline
)
// Get the first quote that is not excluded
const bestQuote = allQuotes.find(
(quote) =>
!excludeCCTP ||
quote.bridgeModuleName !== this.synapseCCTPRouterSet.bridgeModuleName
options
)
const bestQuote = allQuotes[0]
if (!bestQuote) {
throw new Error('No route found')
}
return bestQuote
}

/**
* This method tries to fetch all available quotes from all available bridge modules.
* Fetches all available quotes from the supported bridge modules (SynapseBridge, SynapseCCTP, and SynapseRFQ).
* Users can customize the query by specifying a deadline, excluding certain modules, and providing the user's address on the origin chain.
*
* @param originChainId - The ID of the original chain.
* @param destChainId - The ID of the destination chain.
* @param tokenIn - The input token.
* @param tokenOut - The output token.
* @param amountIn - The amount of input token.
* @param deadline - The transaction deadline, optional.
* @returns - A promise that resolves to an array of bridge quotes.
* Important: The originUserAddress MUST be provided if a smart contract will initiate the bridge operation on the user's behalf.
* This applies to smart wallets (e.g., Safe) and third-party integrations (such as bridge aggregator smart contracts).
*
* @param originChainId - ID of the origin chain.
* @param destChainId - ID of the destination chain.
* @param tokenIn - Address of the token to be bridged from the origin chain.
* @param tokenOut - Address of the token to be received on the destination chain.
* @param amountIn - Amount of input tokens on the origin chain.
* @param options - Optional parameters including origin deadline, excludedModules, and originUserAddress.
*
* @returns A promise that resolves to an array of bridge quotes.
* The returned array is sorted by maxAmountOut in descending order, with all quotes having a non-zero amountOut.
*/
export async function allBridgeQuotes(
this: SynapseSDK,
Expand All @@ -124,7 +139,7 @@ export async function allBridgeQuotes(
tokenIn: string,
tokenOut: string,
amountIn: BigintIsh,
deadline?: BigNumber
options: BridgeQuoteOptions = {}
): Promise<BridgeQuote[]> {
invariant(
originChainId !== destChainId,
Expand All @@ -134,18 +149,25 @@ export async function allBridgeQuotes(
tokenIn = handleNativeToken(tokenIn)
const allQuotes: BridgeQuote[][] = await Promise.all(
this.allModuleSets.map(async (moduleSet) => {
// No-op if the module is explicitly excluded
if (options.excludedModules?.includes(moduleSet.bridgeModuleName)) {
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
return []
}
const routes = await moduleSet.getBridgeRoutes(
originChainId,
destChainId,
tokenIn,
tokenOut,
amountIn
amountIn,
options.originUserAddress
)
// Filter out routes with zero minAmountOut and finalize the rest
return Promise.all(
routes
.filter((route) => route.destQuery.minAmountOut.gt(0))
.map((route) => moduleSet.finalizeBridgeRoute(route, deadline))
.map((route) =>
moduleSet.finalizeBridgeRoute(route, options.deadline)
)
)
})
)
Expand Down
38 changes: 38 additions & 0 deletions packages/sdk-router/src/rfq/fastBridgeRouterSet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,42 @@ describe('FastBridgeRouterSet', () => {
expect(result).toEqual(BigNumber.from(999_001))
})
})

describe('createRFQDestQuery', () => {
const tokenOut = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'
const amountOut = parseFixed('1000', 18)

const expectedDestQuery: CCTPRouterQuery = {
routerAdapter: '0x0000000000000000000000000000000000000000',
tokenOut,
minAmountOut: amountOut,
deadline: BigNumber.from(0),
rawParams: '0x',
}

const originUserAddress = '0x1234567890abcdef1234567890abcdef12345678'
// "No gas rebate" flag, followed by the user address
const expectedRawParamsWithUserAddress =
'0x001234567890abcdef1234567890abcdef12345678'

it('Origin user address is not provided', () => {
const destQuery = FastBridgeRouterSet.createRFQDestQuery(
tokenOut,
amountOut
)
expect(destQuery).toEqual(expectedDestQuery)
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
})

it('Origin user address is provided', () => {
const destQuery = FastBridgeRouterSet.createRFQDestQuery(
tokenOut,
amountOut,
originUserAddress
)
expect(destQuery).toEqual({
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
...expectedDestQuery,
rawParams: expectedRawParamsWithUserAddress,
})
})
})
})
29 changes: 25 additions & 4 deletions packages/sdk-router/src/rfq/fastBridgeRouterSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ export class FastBridgeRouterSet extends SynapseModuleSet {
destChainId: number,
tokenIn: string,
tokenOut: string,
amountIn: BigintIsh
amountIn: BigintIsh,
originUserAddress?: string
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
): Promise<BridgeRoute[]> {
// Check that Routers exist on both chains
if (!this.getModule(originChainId) || !this.getModule(destChainId)) {
Expand Down Expand Up @@ -135,9 +136,11 @@ export class FastBridgeRouterSet extends SynapseModuleSet {
token: quote.ticker.destToken.token,
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
},
originQuery,
// On-chain swaps are not supported for RFQ tokens
// TODO: signal optional gas airdrop
destQuery: createNoSwapQuery(tokenOut, destAmountOut),
destQuery: FastBridgeRouterSet.createRFQDestQuery(
tokenOut,
destAmountOut,
originUserAddress
),
bridgeModuleName: this.bridgeModuleName,
}))
}
Expand Down Expand Up @@ -309,4 +312,22 @@ export class FastBridgeRouterSet extends SynapseModuleSet {
return 0 <= age && age < FastBridgeRouterSet.MAX_QUOTE_AGE_MILLISECONDS
})
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
}

public static createRFQDestQuery(
tokenOut: string,
amountOut: BigNumber,
originUserAddress?: string
): Query {
// On-chain swaps are not supported for RFQ on the destination chain
const destQuery = createNoSwapQuery(tokenOut, amountOut)
// Don't modify the Query if user address is undefined
if (!originUserAddress) {
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
return destQuery
}
// Make sure the rebate flag is always included if user address is defined.
// 0x00 is a single byte that indicates the rebate flag is turned off.
// Concatenate the originUserAddress (without 0x prefix) to the end of the rawParams.
destQuery.rawParams = '0x00' + originUserAddress.slice(2)
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
return destQuery
}
}
9 changes: 4 additions & 5 deletions packages/sdk-router/src/sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,9 +521,7 @@ describe('SynapseSDK', () => {
SupportedChainId.ETH,
ARB_USDT,
ETH_USDC,
amount,
undefined,
false
amount
ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
)

createBridgeQuoteTests(
Expand Down Expand Up @@ -562,8 +560,9 @@ describe('SynapseSDK', () => {
ARB_USDT,
ETH_USDC,
amount,
undefined,
true
{
excludedModules: ['SynapseCCTP'],
}
)

createBridgeQuoteTests(
Expand Down
Loading