<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lake Sky</title>
    <description>The latest articles on DEV Community by Lake Sky (@lake_sky_future).</description>
    <link>https://dev.to/lake_sky_future</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4121667%2Fd26df3ec-c329-47a7-92d7-19e76fd54c38.png</url>
      <title>DEV Community: Lake Sky</title>
      <link>https://dev.to/lake_sky_future</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lake_sky_future"/>
    <language>en</language>
    <item>
      <title>CCTP V1 vs V2: The Nonce Mismatch That Breaks Cross-Chain Arrival Checks</title>
      <dc:creator>Lake Sky</dc:creator>
      <pubDate>Sat, 12 Sep 2026 04:40:40 +0000</pubDate>
      <link>https://dev.to/lake_sky_future/cctp-v1-vs-v2-the-nonce-mismatch-that-breaks-cross-chain-arrival-checks-56l9</link>
      <guid>https://dev.to/lake_sky_future/cctp-v1-vs-v2-the-nonce-mismatch-that-breaks-cross-chain-arrival-checks-56l9</guid>
      <description>&lt;h1&gt;
  
  
  CCTP V1 vs V2: The Nonce Mismatch That Breaks Cross-Chain Arrival Checks
&lt;/h1&gt;

&lt;p&gt;While building a cross-chain transaction tracker on Base, I hit a subtle gotcha in Circle's CCTP (Cross-Chain Transfer Protocol) that took me a while to figure out. If you're building anything that tracks "did my USDC bridge transfer arrive on the destination chain?", this will save you a few hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Goal: Check If a Transfer Arrived
&lt;/h2&gt;

&lt;p&gt;The obvious way to verify a CCTP transfer completed is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On the source chain, decode the &lt;code&gt;DepositForBurn&lt;/code&gt; event to get the deposit's &lt;strong&gt;nonce&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;On the destination chain, query &lt;code&gt;MessageTransmitter.usedNonces(...)&lt;/code&gt; - if the nonce is marked used, the transfer arrived&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Simple, right? That's exactly what the docs imply. It's also wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;On Base, the &lt;code&gt;TokenMessenger&lt;/code&gt; (the contract you call &lt;code&gt;depositForBurn&lt;/code&gt; on) emits this event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;event DepositForBurn(
    uint64 indexed nonce,        // &amp;lt;-- uint64 nonce
    address indexed burnToken,
    uint256 amount,
    address indexed depositor,
    bytes32 mintRecipient,
    uint32 destinationDomain,
    bytes32 destinationTokenMessenger,
    bytes32 destinationCaller
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The nonce is a &lt;strong&gt;&lt;code&gt;uint64&lt;/code&gt;&lt;/strong&gt;, scoped per source domain.&lt;/p&gt;

&lt;p&gt;But when I queried the destination chain's &lt;code&gt;MessageTransmitter.usedNonces(bytes32, uint64)&lt;/code&gt; with that nonce, it &lt;strong&gt;reverted&lt;/strong&gt;. No error message, just a revert.&lt;/p&gt;

&lt;p&gt;Here's what I found after reading the contract source:&lt;/p&gt;

&lt;p&gt;The deployed &lt;code&gt;MessageTransmitter&lt;/code&gt; is &lt;strong&gt;V2&lt;/strong&gt;, and V2 keys &lt;code&gt;usedNonces&lt;/code&gt; by a &lt;strong&gt;&lt;code&gt;bytes32&lt;/code&gt; nonce&lt;/strong&gt; - not a &lt;code&gt;uint64&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// MessageTransmitterV2
mapping(bytes32 =&amp;gt; uint256) public usedNonces;  // single bytes32 key

function usedNonces(bytes32 nonce) external view returns (uint256);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And crucially, in V2 the nonce is &lt;strong&gt;computed inside &lt;code&gt;sendMessage&lt;/code&gt;&lt;/strong&gt; - it's &lt;em&gt;not even present&lt;/em&gt; in the &lt;code&gt;DepositForBurn&lt;/code&gt; event. The V2 &lt;code&gt;DepositForBurn&lt;/code&gt; event looks completely different:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;event DepositForBurn(
    address indexed burnToken,
    uint256 amount,
    address indexed depositor,
    bytes32 mintRecipient,
    uint32 destinationDomain,
    bytes32 destinationTokenMessenger,
    bytes32 destinationCaller,
    uint256 maxFee,
    uint32 indexed minFinalityThreshold,
    bytes hookData
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No nonce at all. It's derived internally from the message.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mismatch
&lt;/h2&gt;

&lt;p&gt;So you have two coexisting, incompatible systems:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;nonce type&lt;/th&gt;
&lt;th&gt;where it's stored&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;usedNonces&lt;/code&gt; key&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;V1 TokenMessenger&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;uint64&lt;/code&gt;, per source domain&lt;/td&gt;
&lt;td&gt;in the event&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bytes32 sourceDomain, uint64 nonce&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;V2 MessageTransmitter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bytes32&lt;/code&gt;, global&lt;/td&gt;
&lt;td&gt;NOT in the event&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bytes32 nonce&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you're on a chain with a V1 TokenMessenger but a V2 MessageTransmitter (which is what I hit on Base ? Ethereum), you &lt;strong&gt;cannot&lt;/strong&gt; go from the V1 &lt;code&gt;uint64&lt;/code&gt; nonce to the V2 &lt;code&gt;bytes32&lt;/code&gt; nonce directly. The mapping isn't &lt;code&gt;bytes32(uint64(nonce))&lt;/code&gt; - it's derived from the message hash internally.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Debugged It
&lt;/h2&gt;

&lt;p&gt;The tell-tale sign was that &lt;code&gt;localDomain()&lt;/code&gt; worked but &lt;code&gt;usedNonces(...)&lt;/code&gt; reverted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// works&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;transmitter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readContract&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;functionName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;localDomain&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;  &lt;span class="c1"&gt;// returns 0 (Ethereum)&lt;/span&gt;

&lt;span class="c1"&gt;// reverts - wrong V1 signature&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;transmitter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readContract&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;functionName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;usedNonces&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;sourceDomain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// works - V2 signature&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;transmitter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readContract&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;functionName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;usedNonces&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;bytes32Nonce&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;localDomain()&lt;/code&gt; succeeds but &lt;code&gt;usedNonces(bytes32, uint64)&lt;/code&gt; reverts, you're talking to a V2 contract with a V1 assumption.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Means for Builders
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Don't assume V1 and V2 are interchangeable.&lt;/strong&gt; They're two separate systems with different message formats, nonce types, and event signatures.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;To check arrival correctly&lt;/strong&gt;, you need to use the same version on both sides. If you're decoding a V1 &lt;code&gt;DepositForBurn&lt;/code&gt;, find the &lt;em&gt;V1&lt;/em&gt; &lt;code&gt;MessageTransmitter&lt;/code&gt; (with the nested &lt;code&gt;usedNonces(bytes32, uint64)&lt;/code&gt; mapping), not the V2 one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Always verify which contract version you're actually talking to&lt;/strong&gt; before assuming a function signature. &lt;code&gt;localDomain()&lt;/code&gt; working ? the rest of the ABI is what you expect.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;CCTP V1 uses &lt;code&gt;uint64&lt;/code&gt; nonces (per source domain, in the event); V2 uses &lt;code&gt;bytes32&lt;/code&gt; nonces (global, computed internally, not in the event). They don't map to each other directly. If you're checking "did my bridge transfer arrive," make sure the TokenMessenger and MessageTransmitter versions match - otherwise your &lt;code&gt;usedNonces&lt;/code&gt; call silently reverts.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I hit this while building &lt;a href="https://github.com/LakeSky/bridge-watch" rel="noopener noreferrer"&gt;bridge-watch&lt;/a&gt;, an open-source on-chain intel API. The cross-chain arrival check is the one piece I had to leave as "source-side only" until I reconcile the V1/V2 nonce mapping - if anyone has a clean way to map a V1 &lt;code&gt;uint64&lt;/code&gt; nonce to the V2 &lt;code&gt;bytes32&lt;/code&gt; nonce, I'd love to hear it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cctp</category>
      <category>blockchain</category>
      <category>crosschain</category>
      <category>ethereum</category>
    </item>
  </channel>
</rss>
