diff --git a/api/_bridges/cctp/strategy.ts b/api/_bridges/cctp/strategy.ts index 42a783a91..dd139f3cc 100644 --- a/api/_bridges/cctp/strategy.ts +++ b/api/_bridges/cctp/strategy.ts @@ -33,7 +33,7 @@ import { CCTP_SUPPORTED_CHAINS, CCTP_SUPPORTED_TOKENS, CCTP_FINALITY_THRESHOLDS, - DEFAULT_CCTP_ACROSS_FINALIZER_ADDRESS, + getCctpFinalizerAddress, getCctpTokenMessengerAddress, getCctpMessageTransmitterAddress, getCctpDomainId, @@ -89,12 +89,7 @@ export function getCctpBridgeStrategy( const isDestinationChainSupported = CCTP_SUPPORTED_CHAINS.includes( params.outputToken.chainId ); - if ( - !isOriginChainSupported || - !isDestinationChainSupported || - // NOTE: Our finalizer doesn't support destination Solana yet. Block the route until we do. - sdk.utils.chainIsSvm(params.outputToken.chainId) - ) { + if (!isOriginChainSupported || !isDestinationChainSupported) { return false; } @@ -295,6 +290,7 @@ export function getCctpBridgeStrategy( const destinationChainId = crossSwap.outputToken.chainId; const destinationDomain = getCctpDomainId(destinationChainId); const tokenMessenger = getCctpTokenMessengerAddress(originChainId); + const destinationFinalizer = getCctpFinalizerAddress(destinationChainId); // Circle's API returns a minimum fee. Add 1 unit as buffer to ensure the transfer meets the threshold for fast mode eligibility. const hasFastFee = bridgeQuote.fees.amount.gt(0); const maxFee = hasFastFee @@ -309,7 +305,7 @@ export function getCctpBridgeStrategy( amount: bridgeQuote.inputAmount, destinationDomain, mintRecipient: crossSwap.recipient, - destinationCaller: DEFAULT_CCTP_ACROSS_FINALIZER_ADDRESS, + destinationCaller: destinationFinalizer, maxFee, minFinalityThreshold, }; diff --git a/api/_bridges/cctp/utils/constants.ts b/api/_bridges/cctp/utils/constants.ts index ea88c24f6..ccdf2818c 100644 --- a/api/_bridges/cctp/utils/constants.ts +++ b/api/_bridges/cctp/utils/constants.ts @@ -1,4 +1,5 @@ import { BigNumber, ethers } from "ethers"; +import * as sdk from "@across-protocol/sdk"; import { CCTP_NO_DOMAIN } from "@across-protocol/constants"; import { CHAIN_IDs, TOKEN_SYMBOLS_MAP, CHAINS } from "../../../_constants"; import { InvalidParamError } from "../../../_errors"; @@ -35,9 +36,17 @@ export const CCTP_FINALITY_THRESHOLDS = { standard: 2000, }; -// CCTP Across Finalizer address -export const DEFAULT_CCTP_ACROSS_FINALIZER_ADDRESS = +// CCTP Across Finalizer addresses +const CCTP_ACROSS_FINALIZER_ADDRESS_EVM = "0x72adB07A487f38321b6665c02D289C413610B081"; +const CCTP_ACROSS_FINALIZER_ADDRESS_SVM = + "5v4SXbcAKKo3YbPBXU9K7zNBMgJ2RQFsvQmg2RAFZT6t"; + +export const getCctpFinalizerAddress = (chainId: number): string => { + return sdk.utils.chainIsSvm(chainId) + ? CCTP_ACROSS_FINALIZER_ADDRESS_SVM + : CCTP_ACROSS_FINALIZER_ADDRESS_EVM; +}; // CCTP TokenMessenger contract addresses // Source: https://developers.circle.com/cctp/evm-smart-contracts diff --git a/test/api/_bridges/cctp/strategy.test.ts b/test/api/_bridges/cctp/strategy.test.ts index c10943d90..c61320031 100644 --- a/test/api/_bridges/cctp/strategy.test.ts +++ b/test/api/_bridges/cctp/strategy.test.ts @@ -4,6 +4,7 @@ import * as sdk from "@across-protocol/sdk"; import { _buildCctpTxForAllowanceHolderEvm } from "../../../../api/_bridges/cctp/strategy"; import { CrossSwapQuotes } from "../../../../api/_dexes/types"; import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "../../../../api/_constants"; +import { encodeDepositForBurn } from "../../../../api/_bridges/cctp/utils/constants"; // Mock only the SVM utilities we need jest.mock("@across-protocol/sdk", () => { @@ -160,4 +161,109 @@ describe("CCTP Strategy - EVM to Solana mint recipient", () => { "0xencoded-mintRecipient:0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D" ); }); + + it("passes through SVM destination caller to encodeDepositForBurn", async () => { + const solanaDestinationCaller = + "5v4SXbcAKKo3YbPBXU9K7zNBMgJ2RQFsvQmg2RAFZT6t"; + const quotes: CrossSwapQuotes = { + crossSwap: { + amount: BigNumber.from("1000000"), + inputToken: { + address: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.OPTIMISM], + decimals: 6, + symbol: "USDC", + chainId: CHAIN_IDs.OPTIMISM, + }, + outputToken: { + address: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.SOLANA], + decimals: 6, + symbol: "USDC", + chainId: CHAIN_IDs.SOLANA, + }, + depositor: "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D", + recipient: "FmMK62wrtWVb5SVoTZftSCGw3nEDA79hDbZNTRnC1R6t", + slippageTolerance: 0.01, + type: "exactInput", + refundOnOrigin: false, + embeddedActions: [], + strictTradeType: true, + isDestinationSvm: true, + }, + bridgeQuote: {} as any, + contracts: {} as any, + }; + + await _buildCctpTxForAllowanceHolderEvm({ + crossSwapQuotes: quotes, + originChainId: CHAIN_IDs.OPTIMISM, + destinationChainId: CHAIN_IDs.SOLANA, + tokenMessenger: "0x1234567890123456789012345678901234567890", + depositForBurnParams: { + amount: BigNumber.from("1000000"), + destinationDomain: 5, + mintRecipient: quotes.crossSwap.recipient, + destinationCaller: solanaDestinationCaller, + maxFee: BigNumber.from(0), + minFinalityThreshold: 12, + }, + }); + + expect(encodeDepositForBurn).toHaveBeenCalledWith( + expect.objectContaining({ + destinationCaller: solanaDestinationCaller, + }) + ); + }); + + it("passes through EVM destination caller to encodeDepositForBurn", async () => { + const evmDestinationCaller = "0x72adB07A487f38321b6665c02D289C413610B081"; + const quotes: CrossSwapQuotes = { + crossSwap: { + amount: BigNumber.from("1000000"), + inputToken: { + address: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.OPTIMISM], + decimals: 6, + symbol: "USDC", + chainId: CHAIN_IDs.OPTIMISM, + }, + outputToken: { + address: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], + decimals: 6, + symbol: "USDC", + chainId: CHAIN_IDs.BASE, + }, + depositor: "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D", + recipient: "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D", + slippageTolerance: 0.01, + type: "exactInput", + refundOnOrigin: false, + embeddedActions: [], + strictTradeType: true, + isDestinationSvm: false, + }, + bridgeQuote: {} as any, + contracts: {} as any, + }; + + await _buildCctpTxForAllowanceHolderEvm({ + crossSwapQuotes: quotes, + originChainId: CHAIN_IDs.OPTIMISM, + destinationChainId: CHAIN_IDs.BASE, + tokenMessenger: "0x1234567890123456789012345678901234567890", + depositForBurnParams: { + amount: BigNumber.from("1000000"), + destinationDomain: 6, + mintRecipient: quotes.crossSwap.recipient, + destinationCaller: evmDestinationCaller, + maxFee: BigNumber.from(0), + minFinalityThreshold: 12, + }, + }); + + expect(encodeDepositForBurn).toHaveBeenCalledWith( + expect.objectContaining({ + destinationCaller: evmDestinationCaller, + }) + ); + }); });