From 26aa87213b6611f7ca6c82fb228e5c8838d32db5 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:50:27 +0530 Subject: [PATCH 1/4] Add support for handling multiple CCTP messages in a single tx --- contracts/tasks/crossChain.js | 169 +++++++++++++++++++++++----------- 1 file changed, 116 insertions(+), 53 deletions(-) diff --git a/contracts/tasks/crossChain.js b/contracts/tasks/crossChain.js index 076056291a..ad1dab65de 100644 --- a/contracts/tasks/crossChain.js +++ b/contracts/tasks/crossChain.js @@ -34,7 +34,7 @@ const cctpOperationsConfig = async ({ }; }; -const fetchAttestation = async ({ transactionHash, cctpChainId }) => { +const fetchAttestations = async ({ transactionHash, cctpChainId }) => { console.log( `Fetching attestation for transaction hash: ${transactionHash} on cctp chain id: ${cctpChainId}` ); @@ -49,28 +49,58 @@ const fetchAttestation = async ({ transactionHash, cctpChainId }) => { ); } const resultJson = await response.json(); - - if (resultJson.messages.length !== 1) { + const fetchedMessages = resultJson.messages; + if (!Array.isArray(fetchedMessages)) { throw new Error( - `Expected 1 attestation, got ${resultJson.messages.length}` + `Invalid attestation payload for tx ${transactionHash}: messages is not an array` ); } - const message = resultJson.messages[0]; - const status = message.status; - if (status !== "complete") { - throw new Error(`Attestation is not complete, status: ${status}`); - } - - return { + return fetchedMessages.map((message, index) => ({ attestation: message.attestation, message: message.message, - status: "ok", - }; + status: message.status, + eventNonce: message.eventNonce, + decodedMessage: message.decodedMessage, + index, + })); }; -// TokensBridged & MessageTransmitted are the 2 events that are emitted when a transaction is published to the CCTP contract -// One transaction containing such message can at most only contain one of these events +const normalizeAddress = (address) => { + try { + return ethers.utils.getAddress(address); + } catch (error) { + return null; + } +}; + +const isMessageForDestination = ({ + decodedMessage, + destinationCaller, + destinationDomainId, +}) => { + if (!decodedMessage) { + return false; + } + + const decodedDestinationCaller = normalizeAddress( + decodedMessage.destinationCaller + ); + const normalizedDestinationCaller = normalizeAddress(destinationCaller); + const decodedDestinationDomain = String(decodedMessage.destinationDomain); + + if (!decodedDestinationCaller || !normalizedDestinationCaller) { + return false; + } + + return ( + decodedDestinationCaller === normalizedDestinationCaller && + decodedDestinationDomain === String(destinationDomainId) + ); +}; + +// TokensBridged & MessageTransmitted are emitted when a CCTP message is posted. +// A single source transaction can emit multiple CCTP messages. const fetchTxHashesFromCctpTransactions = async ({ config, blockLookback, @@ -157,54 +187,87 @@ const processCctpBridgeTransactions = async ({ blockLookback, }); for (const txHash of allTxHashes) { - const storeKey = `cctp_message_${txHash}`; - const storedValue = await store.get(storeKey); - - if (storedValue === "processed") { - console.log( - `Transaction with hash: ${txHash} has already been processed. Skipping...` - ); - continue; - } - - const { attestation, message, status } = await fetchAttestation({ + const cctpMessages = await fetchAttestations({ transactionHash: txHash, cctpChainId: cctpSourceDomainId, }); - if (status !== "ok") { - console.log( - `Attestation from tx hash: ${txHash} on cctp chain id: ${config.cctpSourceDomainId} is not attested yet, status: ${status}. Skipping...` - ); - } console.log( - `Attempting to relay attestation with tx hash: ${txHash} and message: ${message} to cctp chain id: ${cctpDestinationDomainId}` + `Found ${cctpMessages.length} CCTP messages for transaction hash: ${txHash}` ); - if (dryrun) { + const destinationAddress = + config.cctpIntegrationContractDestination.address || ""; + + for (const cctpMessage of cctpMessages) { + const messageId = + cctpMessage.eventNonce || `${txHash}_index_${cctpMessage.index}`; + const storeKey = `cctp_message_${messageId}`; + const storedValue = await store.get(storeKey); + + if (storedValue === "processed") { + console.log( + `Message with key ${storeKey} has already been processed. Skipping...` + ); + continue; + } + + if ( + !isMessageForDestination({ + decodedMessage: cctpMessage.decodedMessage, + destinationCaller: destinationAddress, + destinationDomainId: cctpDestinationDomainId, + }) + ) { + console.log( + `Skipping message ${messageId} from tx ${txHash} because it does not target destination caller ${destinationAddress} on domain ${cctpDestinationDomainId}` + ); + continue; + } + + if (cctpMessage.status !== "complete") { + console.log( + `Message ${messageId} from tx ${txHash} is not attested yet (status: ${cctpMessage.status}). Skipping...` + ); + continue; + } + + if (!cctpMessage.message || !cctpMessage.attestation) { + console.log( + `Message ${messageId} from tx ${txHash} is missing message payload or attestation. Skipping...` + ); + continue; + } + console.log( - `Dryrun: Would have relayed attestation with tx hash: ${txHash} to cctp chain id: ${cctpDestinationDomainId}` + `Attempting to relay message ${messageId} from tx hash: ${txHash} to cctp chain id: ${cctpDestinationDomainId}` ); - continue; - } - const relayTx = await config.cctpIntegrationContractDestination.relay( - message, - attestation, - { gasLimit: 4000000 } - ); - console.log( - `Relay transaction with hash ${relayTx.hash} sent to cctp chain id: ${cctpDestinationDomainId}` - ); - const receipt = await logTxDetails(relayTx, "CCTP relay"); - - // Final verification - if (receipt.status === 1) { - console.log("SUCCESS: Transaction executed successfully!"); - await store.put(storeKey, "processed"); - } else { - console.log("FAILURE: Transaction reverted!"); - throw new Error(`Transaction reverted - status: ${receipt.status}`); + if (dryrun) { + console.log( + `Dryrun: Would have relayed message ${messageId} from tx hash: ${txHash} to cctp chain id: ${cctpDestinationDomainId}` + ); + continue; + } + + const relayTx = await config.cctpIntegrationContractDestination.relay( + cctpMessage.message, + cctpMessage.attestation, + { gasLimit: 4000000 } + ); + console.log( + `Relay transaction with hash ${relayTx.hash} sent to cctp chain id: ${cctpDestinationDomainId}` + ); + const receipt = await logTxDetails(relayTx, "CCTP relay"); + + // Final verification + if (receipt.status === 1) { + console.log("SUCCESS: Transaction executed successfully!"); + await store.put(storeKey, "processed"); + } else { + console.log("FAILURE: Transaction reverted!"); + throw new Error(`Transaction reverted - status: ${receipt.status}`); + } } } }; From fa93874e6663f604b118aef3dabfdd85a74e728f Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:06:44 +0530 Subject: [PATCH 2/4] Change gas limit --- contracts/tasks/crossChain.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/tasks/crossChain.js b/contracts/tasks/crossChain.js index ad1dab65de..10f3a851a9 100644 --- a/contracts/tasks/crossChain.js +++ b/contracts/tasks/crossChain.js @@ -253,7 +253,7 @@ const processCctpBridgeTransactions = async ({ const relayTx = await config.cctpIntegrationContractDestination.relay( cctpMessage.message, cctpMessage.attestation, - { gasLimit: 4000000 } + { gasLimit: 2000000 } ); console.log( `Relay transaction with hash ${relayTx.hash} sent to cctp chain id: ${cctpDestinationDomainId}` From 9e7774830b5aa131e6cf361f3dbe1c1e711b65ef Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:53:25 +0530 Subject: [PATCH 3/4] Restore tx level cache --- contracts/tasks/crossChain.js | 46 +++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/contracts/tasks/crossChain.js b/contracts/tasks/crossChain.js index 10f3a851a9..991fec3ec8 100644 --- a/contracts/tasks/crossChain.js +++ b/contracts/tasks/crossChain.js @@ -187,6 +187,15 @@ const processCctpBridgeTransactions = async ({ blockLookback, }); for (const txHash of allTxHashes) { + const txStoreKey = `cctp_message_${txHash}`; + const txStoredValue = await store.get(txStoreKey); + if (txStoredValue === "processed") { + console.log( + `Transaction with hash ${txHash} has already been processed via legacy tx-level key ${txStoreKey}. Skipping...` + ); + continue; + } + const cctpMessages = await fetchAttestations({ transactionHash: txHash, cctpChainId: cctpSourceDomainId, @@ -198,6 +207,8 @@ const processCctpBridgeTransactions = async ({ const destinationAddress = config.cctpIntegrationContractDestination.address || ""; + let hasEligibleMessage = false; + let hasEligibleMessageProcessed = false; for (const cctpMessage of cctpMessages) { const messageId = @@ -205,23 +216,24 @@ const processCctpBridgeTransactions = async ({ const storeKey = `cctp_message_${messageId}`; const storedValue = await store.get(storeKey); - if (storedValue === "processed") { + const messageTargetsDestination = isMessageForDestination({ + decodedMessage: cctpMessage.decodedMessage, + destinationCaller: destinationAddress, + destinationDomainId: cctpDestinationDomainId, + }); + if (!messageTargetsDestination) { console.log( - `Message with key ${storeKey} has already been processed. Skipping...` + `Skipping message ${messageId} from tx ${txHash} because it does not target destination caller ${destinationAddress} on domain ${cctpDestinationDomainId}` ); continue; } + hasEligibleMessage = true; - if ( - !isMessageForDestination({ - decodedMessage: cctpMessage.decodedMessage, - destinationCaller: destinationAddress, - destinationDomainId: cctpDestinationDomainId, - }) - ) { + if (storedValue === "processed") { console.log( - `Skipping message ${messageId} from tx ${txHash} because it does not target destination caller ${destinationAddress} on domain ${cctpDestinationDomainId}` + `Message with key ${storeKey} has already been processed. Skipping...` ); + hasEligibleMessageProcessed = true; continue; } @@ -264,11 +276,25 @@ const processCctpBridgeTransactions = async ({ if (receipt.status === 1) { console.log("SUCCESS: Transaction executed successfully!"); await store.put(storeKey, "processed"); + hasEligibleMessageProcessed = true; } else { console.log("FAILURE: Transaction reverted!"); throw new Error(`Transaction reverted - status: ${receipt.status}`); } } + + const shouldMarkTxProcessed = + !hasEligibleMessage || hasEligibleMessageProcessed; + if (shouldMarkTxProcessed) { + await store.put(txStoreKey, "processed"); + console.log( + `Marked tx ${txHash} as processed using tx-level key ${txStoreKey}` + ); + } else { + console.log( + `Did not mark tx-level key ${txStoreKey} because eligible messages exist but none were fully processed for tx ${txHash}` + ); + } } }; From bc917ef8882560645ec7db8af3df75058a42755d Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:12:30 +0530 Subject: [PATCH 4/4] Fix bug --- contracts/tasks/crossChain.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/contracts/tasks/crossChain.js b/contracts/tasks/crossChain.js index 991fec3ec8..84654a80cf 100644 --- a/contracts/tasks/crossChain.js +++ b/contracts/tasks/crossChain.js @@ -208,7 +208,7 @@ const processCctpBridgeTransactions = async ({ const destinationAddress = config.cctpIntegrationContractDestination.address || ""; let hasEligibleMessage = false; - let hasEligibleMessageProcessed = false; + let hasUnprocessedEligibleMessages = false; for (const cctpMessage of cctpMessages) { const messageId = @@ -216,6 +216,15 @@ const processCctpBridgeTransactions = async ({ const storeKey = `cctp_message_${messageId}`; const storedValue = await store.get(storeKey); + if (cctpMessage.status !== "complete") { + console.log( + `Message ${messageId} from tx ${txHash} is not attested yet (status: ${cctpMessage.status}). Skipping...` + ); + hasEligibleMessage = true; + hasUnprocessedEligibleMessages = true; + continue; + } + const messageTargetsDestination = isMessageForDestination({ decodedMessage: cctpMessage.decodedMessage, destinationCaller: destinationAddress, @@ -233,14 +242,6 @@ const processCctpBridgeTransactions = async ({ console.log( `Message with key ${storeKey} has already been processed. Skipping...` ); - hasEligibleMessageProcessed = true; - continue; - } - - if (cctpMessage.status !== "complete") { - console.log( - `Message ${messageId} from tx ${txHash} is not attested yet (status: ${cctpMessage.status}). Skipping...` - ); continue; } @@ -248,6 +249,7 @@ const processCctpBridgeTransactions = async ({ console.log( `Message ${messageId} from tx ${txHash} is missing message payload or attestation. Skipping...` ); + hasUnprocessedEligibleMessages = true; continue; } @@ -276,7 +278,6 @@ const processCctpBridgeTransactions = async ({ if (receipt.status === 1) { console.log("SUCCESS: Transaction executed successfully!"); await store.put(storeKey, "processed"); - hasEligibleMessageProcessed = true; } else { console.log("FAILURE: Transaction reverted!"); throw new Error(`Transaction reverted - status: ${receipt.status}`); @@ -284,7 +285,7 @@ const processCctpBridgeTransactions = async ({ } const shouldMarkTxProcessed = - !hasEligibleMessage || hasEligibleMessageProcessed; + !hasEligibleMessage || !hasUnprocessedEligibleMessages; if (shouldMarkTxProcessed) { await store.put(txStoreKey, "processed"); console.log(