Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions programs/sponsored-cctp-src-periphery/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>, // Signature is fixed 65 bytes, but it is more readable in logs expressed as encoded data blob.
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ pub fn deposit_for_burn(mut ctx: Context<DepositForBurn>, 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(),
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>,
}

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.
///
Expand Down Expand Up @@ -60,28 +62,30 @@ 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);
Self::encode_u64(&mut encoded, self.max_bps_to_sponsor);
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());

keccak::hash(&encoded).to_bytes()
}

pub fn encode_hook_data(&self) -> Vec<u8> {
// 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());
Expand All @@ -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);

Expand All @@ -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
}
Expand Down
30 changes: 29 additions & 1 deletion scripts/svm/SponsoredCctpSrc/depositForBurn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ enum ExecutionMode {
ArbitraryActionsToEVM,
}

enum AccountCreationMode {
Standard,
FromUserFunds,
}

// Parse arguments
const argv = yargs(hideBin(process.argv))
.option("amount", {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -136,6 +154,8 @@ async function depositForBurn(): Promise<void> {
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;
Expand Down Expand Up @@ -197,6 +217,8 @@ async function depositForBurn(): Promise<void> {
maxUserSlippageBps,
finalRecipient: ethers.utils.hexZeroPad(finalRecipient, 32),
finalToken: ethers.utils.hexZeroPad(finalToken, 32),
destinationDex,
accountCreationMode,
executionMode,
actionData,
};
Expand All @@ -219,14 +241,16 @@ async function depositForBurn(): Promise<void> {
);
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,
quoteDataEvm.maxBpsToSponsor,
quoteDataEvm.maxUserSlippageBps,
quoteDataEvm.finalRecipient,
quoteDataEvm.finalToken,
quoteDataEvm.destinationDex,
quoteDataEvm.accountCreationMode,
quoteDataEvm.executionMode,
ethers.utils.keccak256(quoteDataEvm.actionData),
]
Expand Down Expand Up @@ -255,6 +279,8 @@ async function depositForBurn(): Promise<void> {
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)),
};
Expand Down Expand Up @@ -296,6 +322,8 @@ async function depositForBurn(): Promise<void> {
{ 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() },
Expand Down
47 changes: 43 additions & 4 deletions test/svm/SponsoredCctpSrc.Deposit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -96,14 +98,16 @@ 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,
quoteData.maxBpsToSponsor,
quoteData.maxUserSlippageBps,
quoteData.finalRecipient,
quoteData.finalToken,
quoteData.destinationDex,
quoteData.accountCreationMode,
quoteData.executionMode,
ethers.utils.keccak256(quoteData.actionData),
]
Expand Down Expand Up @@ -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)),
};
Expand All @@ -152,14 +158,16 @@ 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,
quoteData.maxBpsToSponsor,
quoteData.maxUserSlippageBps,
quoteData.finalRecipient,
quoteData.finalToken,
quoteData.destinationDex,
quoteData.accountCreationMode,
quoteData.executionMode,
quoteData.actionData,
]
Expand All @@ -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;
Expand All @@ -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,
Expand All @@ -200,6 +223,8 @@ describe("sponsored_cctp_src_periphery.deposit", () => {
maxUserSlippageBps,
finalRecipient,
finalToken,
destinationDex,
accountCreationMode,
executionMode,
actionData,
};
Expand Down Expand Up @@ -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,
};
Expand Down Expand Up @@ -477,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;
Expand Down Expand Up @@ -537,6 +566,8 @@ describe("sponsored_cctp_src_periphery.deposit", () => {
maxUserSlippageBps,
finalRecipient: ethers.utils.hexlify(finalRecipient),
finalToken: ethers.utils.hexlify(finalToken),
destinationDex,
accountCreationMode,
executionMode,
actionData,
};
Expand Down Expand Up @@ -616,7 +647,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 = {
Expand All @@ -634,6 +665,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),
};
Expand Down Expand Up @@ -702,6 +735,8 @@ describe("sponsored_cctp_src_periphery.deposit", () => {
maxUserSlippageBps,
finalRecipient: ethers.utils.hexlify(finalRecipient),
finalToken: ethers.utils.hexlify(finalToken),
destinationDex,
accountCreationMode,
executionMode,
actionData,
};
Expand Down Expand Up @@ -769,6 +804,8 @@ describe("sponsored_cctp_src_periphery.deposit", () => {
maxUserSlippageBps,
finalRecipient: ethers.utils.hexlify(finalRecipient),
finalToken: ethers.utils.hexlify(finalToken),
destinationDex,
accountCreationMode,
executionMode,
actionData,
};
Expand Down Expand Up @@ -883,6 +920,8 @@ describe("sponsored_cctp_src_periphery.deposit", () => {
maxUserSlippageBps,
finalRecipient: ethers.utils.hexlify(finalRecipient),
finalToken: ethers.utils.hexlify(finalToken),
destinationDex,
accountCreationMode,
executionMode,
actionData,
};
Expand Down
6 changes: 6 additions & 0 deletions test/svm/SponsoredCctpSrc.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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<u8>
}
Expand All @@ -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
}
Loading