Skip to content

feat: finalize *tokenless* HubPool CCTP messages #2270

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 20 commits into
base: master
Choose a base branch
from

Conversation

grasphoper
Copy link
Contributor

@grasphoper grasphoper commented May 24, 2025

Current implementation of CCTP finalizer is only tracking DepositForBurn events, and thus is not considering other types of CCTP messages. CCTP can send tokenless messages via MessageTransmitter contract, which emits MessageSent(bytes message) event.

We use HubPool events TokensRelayed and MessageRelayed to find relevant tx hashes to search for such MessageTransmitter MessageSent events in addition to all DepositForBurn events.

Signed-off-by: Ihor Farion <[email protected]>
Signed-off-by: Ihor Farion <[email protected]>
Signed-off-by: Ihor Farion <[email protected]>
@grasphoper grasphoper changed the title feat: finalize *tokenless* HubPool CCTP messages in addition to all DepositForBurn messages feat: finalize *tokenless* HubPool CCTP messages May 24, 2025
@grasphoper grasphoper marked this pull request as ready for review May 28, 2025 16:02
@grasphoper
Copy link
Contributor Author

Tested the following:

  • equality between new code and old code in finding CCTP DepositForBurn events
  • v1 crosschain transfer: send, finalize
  • v1 crosschain message relay: send, finalize
  • v2 crosschain transfer: send, finalize
  • v2 crosschain message relay: send, finalize

@grasphoper grasphoper requested a review from pxrl May 28, 2025 20:21
* @returns A Set of unique transaction hashes.
*/
async function getRelevantCCTPTxHashes(
srcProvider: RetryProvider,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need RetryProvider here? I'd have expected Provider should be sufficient.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what the differences are. Just used it because it was used in other places in the file

const isCctpV2 = isCctpV2L2ChainId(l2ChainId);
senderAddresses: string[],
sourceEventSearchConfig: EventSearchConfig,
includeTokenlessHubPoolMessages: boolean
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdyt about setting a default on this? Are there scenarios where we might want to only look at tokens?

Copy link
Contributor Author

@grasphoper grasphoper May 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The naming might be better here probably. This flag:

  • enables scanning for any CCTP events in the HubPool-related txs.
  • in practice, this means querying MessageRelayed, TokenRelayed events on HubPool address
  • if HubPool address is not defined for a sourceChain, this funnction will crash. So I use includeTokenlessHubPoolMessages = false for L2 -> L1 CCTP for example

Comment on lines +217 to +221
const messageRelayedEvents = await paginatedEventQuery(hubPool, messageRelayedFilter, sourceEventSearchConfig);
messageRelayedEvents.forEach((e) => txHashesFromHubPool.push(e.transactionHash));

const tokensRelayedFilter = hubPool.filters.TokensRelayed();
const tokensRelayedEvents = await paginatedEventQuery(hubPool, tokensRelayedFilter, sourceEventSearchConfig);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It probably makes sense to do these in parallel via Promise.all().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. Interestingly, I tried to just create a correct combined topic filter but failed. Can tell if I did it wrong or paginatedEventQuery doesn't support that

Comment on lines +347 to +368
let lastMessageSentEventIdx = -1;
receipt.logs.forEach((log, i) => {
if (_isMessageSentEvent(log)) {
if (lastMessageSentEventIdx == -1) {
lastMessageSentEventIdx = i;
} else {
_addCommonMessageEventIfRelevant(receipt.logs[lastMessageSentEventIdx]);
lastMessageSentEventIdx = i;
}
} else {
const depositForBurnVersion = _getDepositForBurnVersion(log);
if (depositForBurnVersion == -1) {
// Skip non-`DepositForBurn` events
return;
}
if (lastMessageSentEventIdx == -1) {
throw new Error(
"DepositForBurn event found without corresponding MessageSent event. " +
"Each DepositForBurn event must have a preceding MessageSent event in the same transaction. " +
`Transaction: ${receipt.transactionHash}, DepositForBurn log index: ${i}`
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you comment on what this loop is doing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. Here, we go one by one over a receipts that we "suspect might have CCTP messages for finalization".

It's important to understand what we're looking for in the logs. It's either of 2 things:

  • MessageSent + DepositForBurn == a single USDC transfer from one chain to another (1)
  • solo MessageSent with no DepositForBurn -> a single crosschain message (2)

Then for each receipt we traverse all logs in order, and keep the latest encountered MessageSent index in lastMessageSentEventIdx . Then if we encounter next MessageSent -> the one stored in lastMessageSentEventIdx was (2). If we encounter DepositForBurn, then we bundle stored lastMessageSentEventIdx with it to receive (1)

}

function _getPendingV2AttestationStatus(attestation: string): CCTPMessageStatus {
return attestation === "pending_confirmation" ? "pending" : "ready";
function _getPendingV2AttestationStatus(attestation: CCTPV2APIAttestation): CCTPMessageStatus {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks identical to _getPendingAttestationStatus() - should we make attestation a union of CCTPV2APIAttestation | CCTPAPIGetAttestatuibResponse and drop the 2nd implementation? Could alternatively do something like Pick<CCTPV2APIAttestation, "attestation" | "status">.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants