Skip to content
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
4 changes: 2 additions & 2 deletions src/adapter/bridges/SolanaUsdcCCTPBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ import { getCctpTokenMessenger, isCctpV2L2ChainId } from "../../utils/CCTPUtils"
import { CCTP_NO_DOMAIN } from "@across-protocol/constants";
import { arch } from "@across-protocol/sdk";
import { TokenMessengerMinterIdl } from "@across-protocol/contracts";
import { CCTP_MAX_SEND_AMOUNT } from "../../common";

type MintAndWithdrawData = {
mintRecipient: string;
amount: bigint;
};

export class SolanaUsdcCCTPBridge extends BaseBridgeAdapter {
private CCTP_MAX_SEND_AMOUNT = toBN(1_000_000_000_000); // 1MM USDC.
private IS_CCTP_V2 = false;
private readonly l1UsdcTokenAddress: EvmAddress;
private readonly l2UsdcTokenAddress: SvmAddress;
Expand Down Expand Up @@ -92,7 +92,7 @@ export class SolanaUsdcCCTPBridge extends BaseBridgeAdapter {
const signer = await this.l1Signer.getAddress();
assert(compareAddressesSimple(signer, toAddress.toEvmAddress()), "Cannot rebalance to a non-signer address");
const associatedTokenAddress = await this._getAssociatedTokenAddress();
amount = amount.gt(this.CCTP_MAX_SEND_AMOUNT) ? this.CCTP_MAX_SEND_AMOUNT : amount;
amount = amount.gt(CCTP_MAX_SEND_AMOUNT) ? CCTP_MAX_SEND_AMOUNT : amount;
return Promise.resolve({
contract: this.getL1Bridge(),
method: "depositForBurn",
Expand Down
5 changes: 2 additions & 3 deletions src/adapter/bridges/UsdcCCTPBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
TOKEN_SYMBOLS_MAP,
compareAddressesSimple,
assert,
toBN,
getCctpDomainForChainId,
Address,
EvmAddress,
Expand All @@ -18,9 +17,9 @@ import {
import { processEvent } from "../utils";
import { getCctpTokenMessenger, isCctpV2L2ChainId } from "../../utils/CCTPUtils";
import { CCTP_NO_DOMAIN } from "@across-protocol/constants";
import { CCTP_MAX_SEND_AMOUNT } from "../../common";

export class UsdcCCTPBridge extends BaseBridgeAdapter {
private CCTP_MAX_SEND_AMOUNT = toBN(1_000_000_000_000); // 1MM USDC.
private IS_CCTP_V2 = false;
private readonly l1UsdcTokenAddress: EvmAddress;

Expand Down Expand Up @@ -66,7 +65,7 @@ export class UsdcCCTPBridge extends BaseBridgeAdapter {
amount: BigNumber
): Promise<BridgeTransactionDetails> {
assert(l1Token.eq(this.l1UsdcTokenAddress));
amount = amount.gt(this.CCTP_MAX_SEND_AMOUNT) ? this.CCTP_MAX_SEND_AMOUNT : amount;
amount = amount.gt(CCTP_MAX_SEND_AMOUNT) ? CCTP_MAX_SEND_AMOUNT : amount;
return Promise.resolve({
contract: this.getL1Bridge(),
method: "depositForBurn",
Expand Down
4 changes: 2 additions & 2 deletions src/adapter/l2Bridges/UsdcCCTPBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import {
} from "../../utils";
import { BaseL2BridgeAdapter } from "./BaseL2BridgeAdapter";
import { AugmentedTransaction } from "../../clients/TransactionClient";
import { CCTP_MAX_SEND_AMOUNT } from "../../common";

export class UsdcCCTPBridge extends BaseL2BridgeAdapter {
private CCTP_MAX_SEND_AMOUNT = toBN(1_000_000_000_000); // 1MM USDC.
private IS_CCTP_V2 = false;
private readonly l1UsdcTokenAddress: EvmAddress;
private readonly l2UsdcTokenAddress: EvmAddress;
Expand Down Expand Up @@ -63,7 +63,7 @@ export class UsdcCCTPBridge extends BaseL2BridgeAdapter {
const { decimals } = getTokenInfo(l2Token, this.l2chainId);
const formatter = createFormatFunction(2, 4, false, decimals);

amount = amount.gt(this.CCTP_MAX_SEND_AMOUNT) ? this.CCTP_MAX_SEND_AMOUNT : amount;
amount = amount.gt(CCTP_MAX_SEND_AMOUNT) ? CCTP_MAX_SEND_AMOUNT : amount;
return Promise.resolve([
{
contract: this.l2Bridge,
Expand Down
4 changes: 4 additions & 0 deletions src/common/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
toWei,
BigNumber,
winston,
toBN,
} from "../utils";
import {
BaseBridgeAdapter,
Expand Down Expand Up @@ -61,6 +62,9 @@ export const CONFIG_STORE_VERSION = 6;

export const RELAYER_MIN_FEE_PCT = 0.0001;

// The maximum amount of USDC permitted to be sent over CCTP in a single transaction.
export const CCTP_MAX_SEND_AMOUNT = toBN(1_000_000_000_000); // 1MM USDC.

// max(uint256) - 1
export const INFINITE_FILL_DEADLINE = bnUint32Max;

Expand Down
17 changes: 16 additions & 1 deletion src/finalizer/utils/cctp/l2ToL1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,21 @@ import {
getStatePda,
toAddressType,
createFormatFunction,
getKitKeypairFromEvmSigner,
isSVMSpokePoolClient,
} from "../../../utils";
import {
AttestedCCTPDeposit,
CCTPMessageStatus,
getAttestedCCTPDeposits,
getCctpMessageTransmitter,
} from "../../../utils/CCTPUtils";
import { bridgeTokensToHubPool } from "./svm";
import { FinalizerPromise, CrossChainMessage, AddressesToFinalize } from "../../types";

export async function cctpL2toL1Finalizer(
logger: winston.Logger,
_signer: Signer,
signer: Signer,
hubPoolClient: HubPoolClient,
spokePoolClient: SpokePoolClient,
_l1SpokePoolClient: SpokePoolClient,
Expand All @@ -48,6 +51,18 @@ export async function cctpL2toL1Finalizer(
? await augmentSendersListForSolana(senderAddresses, spokePoolClient)
: senderAddresses;

// Solana has a two step withdrawal process, where the funds in the spoke pool's transfer liability PDA must be manually withdrawn after
// a refund leaf has been executed.
if (finalizingFromSolana) {
assert(isSVMSpokePoolClient(spokePoolClient));
const svmSigner = await getKitKeypairFromEvmSigner(signer);
const bridgeTokens = await bridgeTokensToHubPool(spokePoolClient, svmSigner, logger, hubPoolClient.chainId);
logger[isDefined(bridgeTokens.signature) ? "info" : "debug"]({
at: `Finalizer#CCTPL2ToL1Finalizer:${spokePoolClient.chainId}`,
message: bridgeTokens.message,
signature: bridgeTokens.signature,
});
}
const outstandingDeposits = await getAttestedCCTPDeposits(
augmentedSenderAddresses,
spokePoolClient.chainId,
Expand Down
1 change: 1 addition & 0 deletions src/finalizer/utils/cctp/svm/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./l1Tol2";
export * from "./l2Tol1";
122 changes: 122 additions & 0 deletions src/finalizer/utils/cctp/svm/l2Tol1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { KeyPairSigner, appendTransactionMessageInstructions, pipe, address, generateKeyPairSigner } from "@solana/kit";
import { SVMSpokePoolClient } from "../../../../clients";
import { CONTRACT_ADDRESSES, CCTP_MAX_SEND_AMOUNT } from "../../../../common";
import {
winston,
SvmAddress,
sendAndConfirmSolanaTransaction,
simulateSolanaTransaction,
toKitAddress,
CHAIN_IDs,
getCCTPDepositAccounts,
createDefaultTransaction,
getAssociatedTokenAddress,
getStatePda,
getTransferLiabilityPda,
TOKEN_SYMBOLS_MAP,
PUBLIC_NETWORKS,
chainIsProd,
getEventAuthority,
createFormatFunction,
} from "../../../../utils";
import { SvmSpokeClient } from "@across-protocol/contracts";

/**
* Initiates a withdrawal from the Solana spoke pool.
*
* @param solanaClient The Solana client.
* @param signer A base signer to be converted into a Solana signer.
* @param simulate Whether to simulate the transaction.
* @param hubChainId The chain ID of the hub.
* @returns A list of executed transaction signatures.
*/
export async function bridgeTokensToHubPool(
solanaClient: SVMSpokePoolClient,
signer: KeyPairSigner,
logger: winston.Logger,
hubChainId = 1
): Promise<{ message: string; signature?: string }> {
const svmProvider = solanaClient.svmEventsClient.getRpc();
const svmSpoke = solanaClient.spokePoolAddress;
const svmSpokeProgramId = toKitAddress(svmSpoke);

const l2ChainId = chainIsProd(hubChainId) ? CHAIN_IDs.SOLANA : CHAIN_IDs.SOLANA_DEVNET;
const { address: tokenMessengerAddress } = CONTRACT_ADDRESSES[l2ChainId].cctpTokenMessenger;
const { address: messageTransmitterAddress } = CONTRACT_ADDRESSES[l2ChainId].cctpMessageTransmitter;
const l2Usdc = SvmAddress.from(TOKEN_SYMBOLS_MAP.USDC.addresses[l2ChainId]);
const destinationDomain = PUBLIC_NETWORKS[hubChainId].cctpDomain;

// Before we get all the information necessary to initiate a CCTP withdrawal, we need to check whether
// the transfer liability PDA has any pending withdraw amounts.
const transferLiability = await getTransferLiabilityPda(svmSpokeProgramId, toKitAddress(l2Usdc));
const transferLiabilityAccount = await SvmSpokeClient.fetchTransferLiability(svmProvider, transferLiability);
const pendingWithdrawAmount = Math.min(
Number(transferLiabilityAccount.data.pendingToHubPool),
CCTP_MAX_SEND_AMOUNT.toNumber()
);
if (pendingWithdrawAmount === 0) {
return {
message: "Transfer liability account has no pending withdraw amount.",
};
}

// If there is an amount to withdraw, then fetch all accounts and send the transaction.
const [state, eventAuthority, cctpDepositAccounts, messageSentEventData] = await Promise.all([
getStatePda(svmSpokeProgramId),
getEventAuthority(svmSpokeProgramId),
getCCTPDepositAccounts(
hubChainId,
destinationDomain,
address(tokenMessengerAddress),
address(messageTransmitterAddress)
),
generateKeyPairSigner(),
]);
const {
tokenMessengerMinterSenderAuthority,
messageTransmitter,
tokenMessenger,
remoteTokenMessenger,
tokenMinter,
localToken,
cctpEventAuthority,
} = cctpDepositAccounts;
const vault = await getAssociatedTokenAddress(SvmAddress.from(state.toString()), l2Usdc);
const bridgeTokensToHubPoolIx = SvmSpokeClient.getBridgeTokensToHubPoolInstruction({
signer,
payer: signer,
mint: toKitAddress(l2Usdc),
state,
transferLiability,
vault,
tokenMessengerMinterSenderAuthority,
messageTransmitter,
tokenMessenger,
remoteTokenMessenger,
tokenMinter,
localToken,
cctpEventAuthority,
messageSentEventData,
eventAuthority,
program: svmSpokeProgramId,
amount: pendingWithdrawAmount,
});
const bridgeTokensToHubPoolTx = pipe(await createDefaultTransaction(svmProvider, signer), (tx) =>
appendTransactionMessageInstructions([bridgeTokensToHubPoolIx], tx)
);
const formatUsdc = createFormatFunction(2, 4, false, 6);
if ((process.env["SEND_TRANSACTIONS"] ?? "false") === "true") {
const withdrawSignature = await sendAndConfirmSolanaTransaction(bridgeTokensToHubPoolTx, signer, svmProvider);
return {
message: `Withdrew ${formatUsdc(pendingWithdrawAmount.toString())} USDC from Solana to the hub pool.`,
signature: withdrawSignature,
};
}
const withdrawSimulation = await simulateSolanaTransaction(bridgeTokensToHubPoolTx, svmProvider);
return {
message: `Simulated withdrawal of ${formatUsdc(pendingWithdrawAmount.toString())} USDC with result ${
withdrawSimulation?.value?.logs
}`,
signature: "",
};
}
3 changes: 2 additions & 1 deletion src/relayer/RelayerConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,8 @@ export class RelayerConfig extends CommonConfig {
: chainIds;

const ignoredChainIds = chainIds.filter(
(chainId) => !relayerChainIds.includes(chainId) && chainId !== CHAIN_IDs.BOBA
(chainId) =>
!relayerChainIds.includes(chainId) && (chainId !== CHAIN_IDs.BOBA || chainId !== CHAIN_IDs.ALEPH_ZERO)
);
if (ignoredChainIds.length > 0 && logger) {
logger.debug({
Expand Down
1 change: 1 addition & 0 deletions src/utils/SDKUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const {
getEventAuthority,
getClaimAccountPda,
createDefaultTransaction,
getCCTPDepositAccounts,
} = sdk.arch.svm;
export type SVMProvider = sdk.arch.svm.SVMProvider;

Expand Down
9 changes: 9 additions & 0 deletions src/utils/TransactionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,15 @@ export async function sendAndConfirmSolanaTransaction(
return txSignature;
}

export async function simulateSolanaTransaction(
unsignedTransaction: CompilableTransactionMessage,
provider: SVMProvider
) {
const signedTx = await signTransactionMessageWithSigners(unsignedTransaction);
const serializedTx = getBase64EncodedWireTransaction(signedTx);
return provider.simulateTransaction(serializedTx, { sigVerify: false, encoding: "base64" }).send();
}

export async function getGasPrice(
provider: ethers.providers.Provider,
priorityScaler = 1.2,
Expand Down