From 7ab28e4b65f9630b53166141f286df689781e7e4 Mon Sep 17 00:00:00 2001 From: Reinis Martinsons Date: Wed, 17 Dec 2025 12:17:49 +0000 Subject: [PATCH 1/3] fix: add destination dex and account creation mode Signed-off-by: Reinis Martinsons --- .../src/utils/sponsored_cctp_quote.rs | 18 +++++--- test/svm/SponsoredCctpSrc.Deposit.ts | 45 +++++++++++++++++-- test/svm/SponsoredCctpSrc.types.ts | 6 +++ 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/programs/sponsored-cctp-src-periphery/src/utils/sponsored_cctp_quote.rs b/programs/sponsored-cctp-src-periphery/src/utils/sponsored_cctp_quote.rs index 124587c7d..395d10c31 100644 --- a/programs/sponsored-cctp-src-periphery/src/utils/sponsored_cctp_quote.rs +++ b/programs/sponsored-cctp-src-periphery/src/utils/sponsored_cctp_quote.rs @@ -16,12 +16,14 @@ pub struct SponsoredCCTPQuote { pub max_user_slippage_bps: u64, pub final_recipient: Pubkey, pub final_token: Pubkey, + pub destination_dex: u32, + pub account_creation_mode: u8, pub execution_mode: u8, pub action_data: Vec, } impl SponsoredCCTPQuote { - const HOOK_DATA_STATIC_FIELDS: usize = 7; // Number of static fields in the hook data. + const HOOK_DATA_STATIC_FIELDS: usize = 9; // Number of static fields in the hook data. /// EVM-compatible typed hash used for signature verification. /// @@ -60,13 +62,13 @@ impl SponsoredCCTPQuote { } /// `hash2` (EVM: second part) = keccak256(abi.encode(nonce, deadline, maxBpsToSponsor, maxUserSlippageBps, - /// finalRecipient, finalToken, executionMode, keccak256(actionData))) + /// finalRecipient, finalToken, destinationDex, accountCreationMode, executionMode, keccak256(actionData))) /// /// We hash the static words (`nonce..executionMode`) directly from the head with appended `keccak(actionData)` as a /// `bytes32` (static) value. fn hash2(&self) -> [u8; 32] { - // Encode the following 7 static quote data fields + action_data hash. - let mut encoded = Vec::with_capacity(8 * 32); + // Encode the following 9 static quote data fields + action_data hash. + let mut encoded = Vec::with_capacity(10 * 32); Self::encode_bytes32(&mut encoded, &self.nonce); Self::encode_u64(&mut encoded, self.deadline); @@ -74,6 +76,8 @@ impl SponsoredCCTPQuote { Self::encode_u64(&mut encoded, self.max_user_slippage_bps); Self::encode_pubkey(&mut encoded, &self.final_recipient); Self::encode_pubkey(&mut encoded, &self.final_token); + Self::encode_u32(&mut encoded, self.destination_dex); + Self::encode_u8(&mut encoded, self.account_creation_mode); Self::encode_u8(&mut encoded, self.execution_mode); Self::encode_bytes32(&mut encoded, &keccak::hash(&self.action_data).to_bytes()); @@ -81,7 +85,7 @@ impl SponsoredCCTPQuote { } pub fn encode_hook_data(&self) -> Vec { - // ABI encoded hookData on EVM holds 7 static 32-byte words followed by the actionData offset that points to the + // ABI encoded hookData on EVM holds 9 static 32-byte words followed by the actionData offset that points to the // length-prefixed actionData bytes. The actionData bytes are padded to 32-byte word length. let action_data_offset = (Self::HOOK_DATA_STATIC_FIELDS + 1) * 32; let mut hook_data = Vec::with_capacity(self.encoded_hook_data_len()); @@ -92,6 +96,8 @@ impl SponsoredCCTPQuote { Self::encode_u64(&mut hook_data, self.max_user_slippage_bps); Self::encode_pubkey(&mut hook_data, &self.final_recipient); Self::encode_pubkey(&mut hook_data, &self.final_token); + Self::encode_u32(&mut hook_data, self.destination_dex); + Self::encode_u8(&mut hook_data, self.account_creation_mode); Self::encode_u8(&mut hook_data, self.execution_mode); Self::encode_bytes(&mut hook_data, &self.action_data, action_data_offset as u64); @@ -107,7 +113,7 @@ impl SponsoredCCTPQuote { action_data_len + (32 - remainder) }; - // ABI encoded hookData on EVM holds 7 static 32-byte words followed by the actionData offset (32 bytes), the + // ABI encoded hookData on EVM holds 9 static 32-byte words followed by the actionData offset (32 bytes), the // length of the actionData (32 bytes), and the padded actionData bytes. (Self::HOOK_DATA_STATIC_FIELDS + 2) * 32 + padded_action_data_len } diff --git a/test/svm/SponsoredCctpSrc.Deposit.ts b/test/svm/SponsoredCctpSrc.Deposit.ts index f809d0e46..6626402fe 100644 --- a/test/svm/SponsoredCctpSrc.Deposit.ts +++ b/test/svm/SponsoredCctpSrc.Deposit.ts @@ -50,6 +50,8 @@ describe("sponsored_cctp_src_periphery.deposit", () => { const minFinalityThreshold = 5; const maxBpsToSponsor = 500; const maxUserSlippageBps = 1000; + const destinationDex = 0xffff_ffff; // CORE_SPOT_DEX_ID = type(uint32).max; + const accountCreationMode = 0; // Standard const executionMode = 0; // DirectToCore const actionData = "0x"; // Empty in DirectToCore mode @@ -96,7 +98,7 @@ describe("sponsored_cctp_src_periphery.deposit", () => { ] ); const encodedPart2 = ethers.utils.defaultAbiCoder.encode( - ["bytes32", "uint256", "uint256", "uint256", "bytes32", "bytes32", "uint8", "bytes32"], + ["bytes32", "uint256", "uint256", "uint256", "bytes32", "bytes32", "uint32", "uint8", "uint8", "bytes32"], [ quoteData.nonce, quoteData.deadline, @@ -104,6 +106,8 @@ describe("sponsored_cctp_src_periphery.deposit", () => { quoteData.maxUserSlippageBps, quoteData.finalRecipient, quoteData.finalToken, + quoteData.destinationDex, + quoteData.accountCreationMode, quoteData.executionMode, ethers.utils.keccak256(quoteData.actionData), ] @@ -134,6 +138,8 @@ describe("sponsored_cctp_src_periphery.deposit", () => { maxUserSlippageBps: new BN(quote.maxUserSlippageBps.toString()), finalRecipient: new PublicKey(ethers.utils.arrayify(quote.finalRecipient)), finalToken: new PublicKey(ethers.utils.arrayify(quote.finalToken)), + destinationDex: quote.destinationDex, + accountCreationMode: quote.accountCreationMode, executionMode: quote.executionMode, actionData: Buffer.from(ethers.utils.arrayify(quote.actionData)), }; @@ -152,7 +158,7 @@ describe("sponsored_cctp_src_periphery.deposit", () => { const getHookDataFromQuote = (quoteData: SponsoredCCTPQuote): Buffer => { const encodedHexString = ethers.utils.defaultAbiCoder.encode( - ["bytes32", "uint256", "uint256", "uint256", "bytes32", "bytes32", "uint8", "bytes"], + ["bytes32", "uint256", "uint256", "uint256", "bytes32", "bytes32", "uint32", "uint8", "uint8", "bytes"], [ quoteData.nonce, quoteData.deadline, @@ -160,6 +166,8 @@ describe("sponsored_cctp_src_periphery.deposit", () => { quoteData.maxUserSlippageBps, quoteData.finalRecipient, quoteData.finalToken, + quoteData.destinationDex, + quoteData.accountCreationMode, quoteData.executionMode, quoteData.actionData, ] @@ -176,6 +184,8 @@ describe("sponsored_cctp_src_periphery.deposit", () => { "uint256", // maxUserSlippageBps "bytes32", // finalRecipient "bytes32", // finalToken + "uint32", // destinationDex + "uint8", // accountCreationMode "uint8", // executionMode "bytes", // actionData ] as const; @@ -189,9 +199,22 @@ describe("sponsored_cctp_src_periphery.deposit", () => { maxUserSlippageBps, finalRecipient, finalToken, + destinationDex, + accountCreationMode, executionMode, actionData, - ] = decoded as [string, ethers.BigNumber, ethers.BigNumber, ethers.BigNumber, string, string, number, string]; + ] = decoded as [ + string, + ethers.BigNumber, + ethers.BigNumber, + ethers.BigNumber, + string, + string, + number, + number, + number, + string + ]; return { nonce, @@ -200,6 +223,8 @@ describe("sponsored_cctp_src_periphery.deposit", () => { maxUserSlippageBps, finalRecipient, finalToken, + destinationDex, + accountCreationMode, executionMode, actionData, }; @@ -411,6 +436,8 @@ describe("sponsored_cctp_src_periphery.deposit", () => { maxUserSlippageBps, finalRecipient: ethers.utils.hexlify(finalRecipient), finalToken: ethers.utils.hexlify(finalToken), + destinationDex, + accountCreationMode, executionMode, actionData, }; @@ -537,6 +564,8 @@ describe("sponsored_cctp_src_periphery.deposit", () => { maxUserSlippageBps, finalRecipient: ethers.utils.hexlify(finalRecipient), finalToken: ethers.utils.hexlify(finalToken), + destinationDex, + accountCreationMode, executionMode, actionData, }; @@ -616,7 +645,7 @@ describe("sponsored_cctp_src_periphery.deposit", () => { const usedNonce = getUsedNonce(nonce); const deadline = ethers.BigNumber.from(Math.floor(Date.now() / 1000) + 3600); const executionMode = 1; // ArbitraryActionsToCore - const actionDataLenth = 442; // Larger actionData would exceed the transaction message size limits on Solana. + const actionDataLenth = 437; // Larger actionData would exceed the transaction message size limits on Solana. const actionData = crypto.randomBytes(actionDataLenth); const quoteData: SponsoredCCTPQuote = { @@ -634,6 +663,8 @@ describe("sponsored_cctp_src_periphery.deposit", () => { maxUserSlippageBps, finalRecipient: ethers.utils.hexlify(finalRecipient), finalToken: ethers.utils.hexlify(finalToken), + destinationDex, + accountCreationMode, executionMode, actionData: ethers.utils.hexlify(actionData), }; @@ -702,6 +733,8 @@ describe("sponsored_cctp_src_periphery.deposit", () => { maxUserSlippageBps, finalRecipient: ethers.utils.hexlify(finalRecipient), finalToken: ethers.utils.hexlify(finalToken), + destinationDex, + accountCreationMode, executionMode, actionData, }; @@ -769,6 +802,8 @@ describe("sponsored_cctp_src_periphery.deposit", () => { maxUserSlippageBps, finalRecipient: ethers.utils.hexlify(finalRecipient), finalToken: ethers.utils.hexlify(finalToken), + destinationDex, + accountCreationMode, executionMode, actionData, }; @@ -883,6 +918,8 @@ describe("sponsored_cctp_src_periphery.deposit", () => { maxUserSlippageBps, finalRecipient: ethers.utils.hexlify(finalRecipient), finalToken: ethers.utils.hexlify(finalToken), + destinationDex, + accountCreationMode, executionMode, actionData, }; diff --git a/test/svm/SponsoredCctpSrc.types.ts b/test/svm/SponsoredCctpSrc.types.ts index 594097a40..c402164ea 100644 --- a/test/svm/SponsoredCctpSrc.types.ts +++ b/test/svm/SponsoredCctpSrc.types.ts @@ -17,6 +17,8 @@ export interface SponsoredCCTPQuote { maxUserSlippageBps: ethers.BigNumberish; // uint256 finalRecipient: string; // bytes32 finalToken: string; // bytes32 + destinationDex: number; // uint32 + accountCreationMode: number; // uint8 executionMode: number; // uint8 actionData: ethers.BytesLike; // bytes } @@ -36,6 +38,8 @@ export interface SponsoredCCTPQuoteSVM { maxUserSlippageBps: BN; // u64 finalRecipient: PublicKey; // Pubkey finalToken: PublicKey; // Pubkey + destinationDex: number; // u32 + accountCreationMode: number; // u8 executionMode: number; // u8 actionData: Buffer; // Vec } @@ -47,6 +51,8 @@ export interface HookData { maxUserSlippageBps: ethers.BigNumber; // uint256 finalRecipient: string; // bytes32 finalToken: string; // bytes32 + destinationDex: number; // uint32 + accountCreationMode: number; // uint8 executionMode: number; // uint8 actionData: string; // bytes } From 6269558bfeafc7cf7839b6eaa70fa42e468157a8 Mon Sep 17 00:00:00 2001 From: Reinis Martinsons Date: Wed, 17 Dec 2025 12:47:15 +0000 Subject: [PATCH 2/3] fix: deposit script Signed-off-by: Reinis Martinsons --- .../svm/SponsoredCctpSrc/depositForBurn.ts | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/scripts/svm/SponsoredCctpSrc/depositForBurn.ts b/scripts/svm/SponsoredCctpSrc/depositForBurn.ts index 45cd1e167..52f563a43 100644 --- a/scripts/svm/SponsoredCctpSrc/depositForBurn.ts +++ b/scripts/svm/SponsoredCctpSrc/depositForBurn.ts @@ -42,6 +42,11 @@ enum ExecutionMode { ArbitraryActionsToEVM, } +enum AccountCreationMode { + Standard, + FromUserFunds, +} + // Parse arguments const argv = yargs(hideBin(process.argv)) .option("amount", { @@ -98,6 +103,19 @@ const argv = yargs(hideBin(process.argv)) default: 1000, // 10 bps describe: "Maximum user slippage in bps, defaults to 1000 (10 bps)", }) + .option("destinationDex", { + type: "number", + demandOption: false, + default: 0xffff_ffff, + describe: "Destination DEX for the sponsored CCTP flow, defaults to CORE_SPOT_DEX_ID = type(uint32).max", + }) + .option("accountCreationMode", { + type: "number", + demandOption: false, + default: AccountCreationMode.Standard, + choices: Object.values(AccountCreationMode), + describe: "Account creation mode for the sponsored CCTP flow", + }) .option("executionMode", { type: "number", demandOption: false, @@ -136,6 +154,8 @@ async function depositForBurn(): Promise { const minFinalityThreshold = resolvedArgv.minFinalityThreshold; const maxBpsToSponsor = resolvedArgv.maxBpsToSponsor; const maxUserSlippageBps = resolvedArgv.maxUserSlippageBps; + const destinationDex = resolvedArgv.destinationDex; + const accountCreationMode = Number(resolvedArgv.accountCreationMode); const executionMode = Number(resolvedArgv.executionMode); const actionData = ethers.utils.hexlify(resolvedArgv.actionData); const deadline = resolvedArgv.deadline; @@ -197,6 +217,8 @@ async function depositForBurn(): Promise { maxUserSlippageBps, finalRecipient: ethers.utils.hexZeroPad(finalRecipient, 32), finalToken: ethers.utils.hexZeroPad(finalToken, 32), + destinationDex, + accountCreationMode, executionMode, actionData, }; @@ -219,7 +241,7 @@ async function depositForBurn(): Promise { ); const hash2 = ethers.utils.keccak256( ethers.utils.defaultAbiCoder.encode( - ["bytes32", "uint256", "uint256", "uint256", "bytes32", "bytes32", "uint8", "bytes32"], + ["bytes32", "uint256", "uint256", "uint256", "bytes32", "bytes32", "uint32", "uint8", "uint8", "bytes32"], [ quoteDataEvm.nonce, quoteDataEvm.deadline, @@ -227,6 +249,8 @@ async function depositForBurn(): Promise { quoteDataEvm.maxUserSlippageBps, quoteDataEvm.finalRecipient, quoteDataEvm.finalToken, + quoteDataEvm.destinationDex, + quoteDataEvm.accountCreationMode, quoteDataEvm.executionMode, ethers.utils.keccak256(quoteDataEvm.actionData), ] @@ -255,6 +279,8 @@ async function depositForBurn(): Promise { maxUserSlippageBps: new BN(quoteDataEvm.maxUserSlippageBps.toString()), finalRecipient: new PublicKey(ethers.utils.arrayify(quoteDataEvm.finalRecipient)), finalToken: new PublicKey(ethers.utils.arrayify(quoteDataEvm.finalToken)), + destinationDex: quoteDataEvm.destinationDex, + accountCreationMode: quoteDataEvm.accountCreationMode, executionMode: quoteDataEvm.executionMode, actionData: Buffer.from(ethers.utils.arrayify(quoteDataEvm.actionData)), }; @@ -296,6 +322,8 @@ async function depositForBurn(): Promise { { Property: "minFinalityThreshold", Value: minFinalityThreshold.toString() }, { Property: "maxBpsToSponsor", Value: maxBpsToSponsor.toString() }, { Property: "maxUserSlippageBps", Value: maxUserSlippageBps.toString() }, + { Property: "destinationDex", Value: destinationDex.toString() }, + { Property: "accountCreationMode", Value: accountCreationMode.toString() }, { Property: "executionMode", Value: executionMode.toString() }, { Property: "actionData", Value: actionData }, { Property: "depositorTokenAccount", Value: depositorTokenAccount.toString() }, From e3187f9f779b3ed7adac05aa44659cab19526624 Mon Sep 17 00:00:00 2001 From: Reinis Martinsons Date: Fri, 19 Dec 2025 07:08:55 +0000 Subject: [PATCH 3/3] fix: update event Signed-off-by: Reinis Martinsons --- programs/sponsored-cctp-src-periphery/src/event.rs | 2 ++ .../sponsored-cctp-src-periphery/src/instructions/deposit.rs | 2 ++ test/svm/SponsoredCctpSrc.Deposit.ts | 2 ++ 3 files changed, 6 insertions(+) diff --git a/programs/sponsored-cctp-src-periphery/src/event.rs b/programs/sponsored-cctp-src-periphery/src/event.rs index 10f3b082e..66a015463 100644 --- a/programs/sponsored-cctp-src-periphery/src/event.rs +++ b/programs/sponsored-cctp-src-periphery/src/event.rs @@ -27,6 +27,8 @@ pub struct SponsoredDepositForBurn { pub max_bps_to_sponsor: u64, pub max_user_slippage_bps: u64, pub final_token: Pubkey, + pub destination_dex: u32, + pub account_creation_mode: u8, pub signature: Vec, // Signature is fixed 65 bytes, but it is more readable in logs expressed as encoded data blob. } diff --git a/programs/sponsored-cctp-src-periphery/src/instructions/deposit.rs b/programs/sponsored-cctp-src-periphery/src/instructions/deposit.rs index 9d05f40d5..bc85b5901 100644 --- a/programs/sponsored-cctp-src-periphery/src/instructions/deposit.rs +++ b/programs/sponsored-cctp-src-periphery/src/instructions/deposit.rs @@ -180,6 +180,8 @@ pub fn deposit_for_burn(mut ctx: Context, params: &DepositForBur max_bps_to_sponsor: quote.max_bps_to_sponsor, max_user_slippage_bps: quote.max_user_slippage_bps, final_token: quote.final_token, + destination_dex: quote.destination_dex, + account_creation_mode: quote.account_creation_mode, signature: params.signature.to_vec(), }); diff --git a/test/svm/SponsoredCctpSrc.Deposit.ts b/test/svm/SponsoredCctpSrc.Deposit.ts index 6626402fe..b97de3860 100644 --- a/test/svm/SponsoredCctpSrc.Deposit.ts +++ b/test/svm/SponsoredCctpSrc.Deposit.ts @@ -504,6 +504,8 @@ describe("sponsored_cctp_src_periphery.deposit", () => { ); assert.strictEqual(depositEvent.finalToken.toString(), new PublicKey(finalToken).toString(), "Invalid finalToken"); assert.strictEqual(depositEvent.finalToken.toString(), new PublicKey(finalToken).toString(), "Invalid finalToken"); + assert.strictEqual(depositEvent.destinationDex, destinationDex, "Invalid destinationDex"); + assert.strictEqual(depositEvent.accountCreationMode, accountCreationMode, "Invalid accountCreationMode"); assert.isTrue(depositEvent.signature.equals(Buffer.from(signature)), "Invalid signature"); const createdEventAccountEvent = events.find((event) => event.name === "createdEventAccount")?.data;