Skip to content

[Fix] Strand-free USDC+message offramp: use pool local_amount, not source_pool_data#407

Draft
m-j-e wants to merge 1 commit into
developfrom
mje/fix-f01-usdc-message-offramp
Draft

[Fix] Strand-free USDC+message offramp: use pool local_amount, not source_pool_data#407
m-j-e wants to merge 1 commit into
developfrom
mje/fix-f01-usdc-message-offramp

Conversation

@m-j-e

@m-j-e m-j-e commented Jun 12, 2026

Copy link
Copy Markdown

Describe your changes

complete_token_transfer re-derived the destination amount by parsing the token's source_pool_data as a 32-byte ABI decimals word (assert!(data_len == 32, EInvalidRemoteChainDecimals)). The USDC/CCTP pool encodes source_pool_data as encode_u64(nonce) ++ encode_u32(domain) = 64 bytes, so every inbound USDC transfer carrying a programmatic message aborted on that assert. The source USDC is already CCTP-burned, so funds were stranded and the whole USDC+message flow was unexecutable per lane.

Fix: make the pool the single source of truth. Each pool already computes the exact local_amount it mints/releases (standard pools via calculate_release_or_mint_amount, USDC via the CCTP-attested burn_message::amount) and uses it for the mint and the emitted event. complete_token_transfer now takes that local_amount and writes it straight to the message, with no re-parse of source_pool_data. This also removes the risk of the message amount diverging from the minted amount.

Describe highly relevant files or code snippets that are critical in the review

  • offramp_state_helper: add local_amount: u64 param to complete_token_transfer; drop parse_remote_decimals / calculate_local_amount / get_local_decimals_for_token from the message branch, plus the now-dead helpers, constants, and import.
  • burn_mint / lock_release / managed_token / usdc pools: pass their computed local amount into complete_token_transfer.
  • tests: add f01_usdc_message_regression_test (64-byte USDC + message now completes and surfaces the pool's amount); update existing callers for the new signature; remove decimal_parity_test (it only covered the deleted duplicate; the math is unchanged in each pool's token_pool.move).

Verified: 487 tests pass across ccip, ccip_offramp, ccip_onramp, the three standard token pools, ccip_router, and managed_token. usdc_token_pool is not built standalone (pre-existing vendored Circle manifest issue); its call-site change mirrors the three pools that pass.

…urce_pool_data

complete_token_transfer re-derived the destination amount by parsing the
token's source_pool_data as a 32-byte ABI decimals word
(assert!(data_len == 32, EInvalidRemoteChainDecimals)). The USDC/CCTP pool
encodes source_pool_data as encode_u64(nonce) ++ encode_u32(domain) = 64
bytes, so every inbound USDC transfer carrying a programmatic message
aborted on that assert. The source USDC is already CCTP-burned, so funds
were stranded and the whole USDC+message flow was unexecutable per lane.

Introduced by #391 (NONEVM-5192): the conversion math is correct, but the
"source_pool_data == abi(decimals)" interface assumption is false for USDC.

Fix: make the pool the single source of truth. Each pool already computes
the exact local_amount it mints/releases (standard pools via
calculate_release_or_mint_amount, USDC via the CCTP-attested
burn_message::amount) and uses it for the mint and the emitted event.
complete_token_transfer now takes that local_amount and writes it straight
to the message, with no re-parse of source_pool_data. This also removes the
risk of the message amount diverging from the minted amount.

- offramp_state_helper: add local_amount: u64 param to complete_token_transfer;
  drop parse_remote_decimals / calculate_local_amount / get_local_decimals_for_token
  from the message branch, plus the now-dead helpers, constants, and import.
- burn_mint / lock_release / managed_token / usdc pools: pass their computed
  local amount into complete_token_transfer.
- tests: add f01_usdc_message_regression_test (64-byte USDC + message now
  completes and surfaces the pool's amount); update existing callers for the
  new signature; remove decimal_parity_test (it only covered the deleted
  duplicate; the math is unchanged in each pool's token_pool.move).

Verified: 487 tests pass across ccip, ccip_offramp, ccip_onramp, the three
standard token pools, ccip_router, and managed_token. usdc_token_pool is not
built standalone (pre-existing vendored Circle manifest issue); its call-site
change mirrors the three pools that pass.
@github-actions

Copy link
Copy Markdown

Hello, @m-j-e! 👋
This Pull Request requires regenerating the contract bindings
Run:

  • nix develop -c task bindings:generate
  • Commit the updates

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.

1 participant