Skip to content

Conversation

@yorhodes
Copy link
Member

@yorhodes yorhodes commented Oct 27, 2025

Description

(#7209) TokenBridgeCctpBase.verify() still allows submitting a manipulated Hyperlane message together with a valid CCTP token message. Assume an attacker calls TokenBridgeCctpBase.verify() with CCTP metadata that contains a valid Hyperlane token message but supplies a manipulated Hyperlane message as input data. verify() runs _validateTokenMessage(). _validateTokenMessage() does not check the Hyperlane message’s nonce, originDomain, the metadata inside the message body, or the recipient field. In particular, because the recipient address is not validated, an attacker can craft the Hyperlane message to target an arbitrary recipient that uses CctpISM as its ISM. Since the CCTP message is valid, TokenBridgeCctpBase.verify() will successfully call messageTransmitter.receiveMessage(cctpMessageBytes, attestation); and then Mailbox will invoke recipient.handle(uint32 origin, bytes32 sender, bytes calldata message). Here message is abi.encodePacked(token recipient, _amount, _metadata), and the attacker can set _metadata to values unrelated to the CCTP message. As a result, any recipient that uses cctpISM can receive a manipulated message. To mitigate this, add require(hyperlaneMessage.recipient() == address(this)) in _validateTokenMessage(), also validate the Hyperlane message’s originDomain, and check the message body length to ensure there is no unexpected metadata. Note that the Hyperlane message nonce cannot be verified against the CCTP message, so a 1 to 1 pairing between the source-chain Hyperlane message and the CCTP message is not guaranteed.

Introduced constraint on hyperlane message recipient for CCTP token messages. The metadata (nonce), originDomain, and message body length checks are excluded as noted the 1:1 pairing is unenforceable.

Drive-by changes

In TokenBridgeCctpV2._bridgeViaCircle(), the call to TokenMessenger.depositForBurn() should use externalFee instead of maxFeeBps.

Backward compatibility

Yes

Testing

Unit Tests

Summary by CodeRabbit

  • Bug Fixes

    • Added validation to reject token bridge transfers targeting invalid recipients, preventing cross-chain transfer failures.
  • Tests

    • Expanded test coverage for recipient validation in bridge operations.

@changeset-bot
Copy link

changeset-bot bot commented Oct 27, 2025

⚠️ No Changeset found

Latest commit: dd05743

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@codecov
Copy link

codecov bot commented Oct 27, 2025

Codecov Report

❌ Patch coverage is 50.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 76.77%. Comparing base (bf7546b) to head (dd05743).
⚠️ Report is 1 commits behind head on audit-q3-2025.

Additional details and impacted files
@@                Coverage Diff                @@
##           audit-q3-2025    #7261      +/-   ##
=================================================
- Coverage          76.80%   76.77%   -0.03%     
=================================================
  Files                123      123              
  Lines               2712     2713       +1     
  Branches             250      251       +1     
=================================================
  Hits                2083     2083              
- Misses               611      612       +1     
  Partials              18       18              
Components Coverage Δ
core 87.80% <ø> (ø)
hooks 72.89% <ø> (ø)
isms 80.19% <ø> (ø)
token 87.87% <50.00%> (-0.13%) ⬇️
middlewares 84.98% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@nambrot nambrot left a comment

Choose a reason for hiding this comment

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

Add a test for this

@yorhodes yorhodes marked this pull request as ready for review October 28, 2025 15:20
@yorhodes
Copy link
Member Author

Add a test for this

c67a726

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 28, 2025

📝 Walkthrough

Walkthrough

This PR extends the internal _bridgeViaCircle method signature across the CCTP token bridge contracts (base, V1, and V2) to accept a new _maxFee parameter. The base contract now enforces that CCTP token messages target this contract itself via a new recipient validation check in verify. Tests are updated to validate the new safety check and adjust fee parameter references in V2 flows.

Changes

Cohort / File(s) Summary
Token Bridge Internal Signature Update
solidity/contracts/token/TokenBridgeCctpBase.sol
Added uint256 _maxFee parameter to _bridgeViaCircle internal method signature; updated transferRemote call site to pass externalFee as the new _maxFee argument.
CCTP Recipient Validation
solidity/contracts/token/TokenBridgeCctpBase.sol
Added safety check in verify function requiring that when CCTP message targets the token messenger, the hyperlane message recipient must equal address(this); reverts with "Invalid token message recipient" otherwise.
V1 & V2 Bridge Method Overrides
solidity/contracts/token/TokenBridgeCctpV1.sol, solidity/contracts/token/TokenBridgeCctpV2.sol
Updated _bridgeViaCircle signatures to include uint256 _maxFee parameter. V1 leaves parameter unused; V2 forwards _maxFee to depositForBurn call instead of previous maxFeeBps.
CCTP Test Suite Updates
solidity/test/token/TokenBridgeCctp.t.sol
Added test_verify_revertsWhen_invalidTokenMessageRecipient() test for both V1 and V2 suites; added domain configuration in V1 upgrade path; replaced maxFee references with fastFee in V2 DepositForBurn calls and event assertions.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant TokenBridgeCctpBase
    participant _bridgeViaCircle
    
    Caller->>TokenBridgeCctpBase: transferRemote()
    activate TokenBridgeCctpBase
    TokenBridgeCctpBase->>_bridgeViaCircle: _bridgeViaCircle(_destination, _recipient, _amount, externalFee)
    activate _bridgeViaCircle
    _bridgeViaCircle->>_bridgeViaCircle: depositForBurn(..., _maxFee)
    deactivate _bridgeViaCircle
    deactivate TokenBridgeCctpBase
Loading
sequenceDiagram
    participant Mailbox
    participant TokenBridgeCctpBase as Bridge verify()
    participant Validation as Recipient Check
    participant Response
    
    Mailbox->>TokenBridgeCctpBase: verify(message)
    activate TokenBridgeCctpBase
    alt CCTP Message for Token Messenger
        TokenBridgeCctpBase->>Validation: Check message.recipientAddress() == address(this)
        activate Validation
        alt Recipient is valid (this contract)
            Validation-->>TokenBridgeCctpBase: ✓ Pass
        else Recipient is invalid
            Validation-->>TokenBridgeCctpBase: ✗ Revert "Invalid token message recipient"
        end
        deactivate Validation
    end
    deactivate TokenBridgeCctpBase
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Signature changes across multiple inheritance levels: Review the _bridgeViaCircle signature extension in base, V1, and V2 to ensure all override implementations align and the _maxFee parameter flows correctly through call chains.
  • Recipient validation logic: Verify the new safety check in verify correctly identifies when a message targets the token messenger and properly enforces the recipient constraint.
  • Fee parameter handling in V2: Confirm that the transition from maxFeeBps to _maxFee in depositForBurn calls is semantically correct and doesn't introduce unintended fee calculation changes.
  • Test coverage completeness: Ensure the new test_verify_revertsWhen_invalidTokenMessageRecipient() tests adequately cover both V1 and V2 paths, and verify that fastFee replacements in V2 test assertions align with the updated fee forwarding logic.

Possibly related PRs

  • #7209: Modifies TokenBridgeCctpBase and V1/V2 overrides with changes to verify logic and _bridgeViaCircle signature/behavior
  • #6709: Updates _bridgeViaCircle signatures and CCTP token message recipient checks in the same TokenBridgeCctpBase/V1/V2 contracts
  • #7148: Addresses CCTP V2 fee handling by forwarding _maxFee parameter to depositForBurn in TokenBridgeCctpV2

Suggested reviewers

  • tkporter
  • ltyu
  • nambrot

Poem

🏰 An ogre's layers, now three layers strong,
The bridge knows where it should belong—
With fees forwarded, true and tight,
Recipients checked, all feels right. 🟢

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title "fix: prevent token message verifying GMP recipient" clearly and concisely summarizes the core security issue being addressed in this changeset. It directly references the main objective of preventing a manipulated Hyperlane message from being verified as a valid recipient, which aligns with the primary changes to add recipient validation in the _validateTokenMessage() function. The title is specific enough for someone scanning the history to understand what this PR does without being overly verbose or generic.
Description Check ✅ Passed The pull request description is mostly complete and well-structured. It thoroughly covers the Description section with a detailed explanation of the security vulnerability and proposed mitigation, and it addresses Drive-by changes by mentioning the TokenBridgeCctpV2._bridgeViaCircle() fix. However, three non-critical template sections remain unfilled: Related issues, Backward compatibility, and Testing are left as empty template comments rather than substantive answers. Since the core information is present and substantial, and the template allows for non-critical sections to be incomplete, this meets the threshold for a passing description.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch cctp-patch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
solidity/contracts/token/TokenBridgeCctpBase.sol (1)

252-295: Critical security checks are missing from the mitigation.

The recipient validation at lines 275-280 is good and addresses part of the vulnerability, but according to the PR objectives, the mitigation should also include:

  1. Origin domain validation: The Hyperlane message's originDomain should be validated to ensure it matches the expected source. Currently, there's no check that _hyperlaneMessage.origin() matches the CCTP source domain.

  2. Message body length validation: The PR description states we should "check the message body length to ensure no unexpected metadata." The token message body should have a fixed expected length, but there's no validation to prevent extra metadata from being appended.

Without these additional checks, an attacker might still be able to exploit edge cases by:

  • Sending messages from unexpected origin domains
  • Appending malicious metadata to the message body

Apply these additional validations:

         // check if CCTP message is a USDC burn message
         if (circleRecipient == address(tokenMessenger)) {
             // prevent hyperlane message recipient configured with CCTP ISM
             // from verifying and handling token messages
             require(
                 _hyperlaneMessage.recipientAddress() == address(this),
                 "Invalid token message recipient"
             );
+            
+            // validate origin domain matches CCTP source
+            uint32 expectedOrigin = circleDomainToHyperlaneDomain(
+                cctpMessage._sourceDomain()
+            );
+            require(
+                _hyperlaneMessage.origin() == expectedOrigin,
+                "Invalid token message origin"
+            );
+            
+            // validate message body length to prevent unexpected metadata
+            bytes calldata messageBody = _hyperlaneMessage.body();
+            require(
+                messageBody.length == TokenMessage.FORMAT_LENGTH,
+                "Invalid token message body length"
+            );
+            
             _validateTokenMessage(_hyperlaneMessage, cctpMessage);
         }

Note: You'll need to verify that TokenMessage.FORMAT_LENGTH is defined or calculate the expected length based on the TokenMessage format (typically 32 bytes recipient + 32 bytes amount + any metadata length).

🧹 Nitpick comments (1)
solidity/contracts/token/TokenBridgeCctpV2.sol (1)

172-187: Parameter naming could be clearer.

The _maxFee parameter name might be a bit confusing here since it's receiving the actual external fee amount (from externalFee in the base contract), not a maximum fee or basis points value. In CCTP contexts, "maxFee" often refers to basis points or a cap, but this is the calculated fee amount being passed through.

Consider whether _feeAmount or _externalFee would make the intent clearer, or add a comment explaining that this is the actual fee amount calculated by _externalFeeAmount().

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a0f777 and c67a726.

📒 Files selected for processing (4)
  • solidity/contracts/token/TokenBridgeCctpBase.sol (3 hunks)
  • solidity/contracts/token/TokenBridgeCctpV1.sol (1 hunks)
  • solidity/contracts/token/TokenBridgeCctpV2.sol (1 hunks)
  • solidity/test/token/TokenBridgeCctp.t.sol (6 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
solidity/contracts/token/**/*.sol

📄 CodeRabbit inference engine (CLAUDE.md)

Place token bridge contracts (e.g., HypERC20, HypERC20Collateral) under solidity/contracts/token/

Files:

  • solidity/contracts/token/TokenBridgeCctpV2.sol
  • solidity/contracts/token/TokenBridgeCctpBase.sol
  • solidity/contracts/token/TokenBridgeCctpV1.sol
solidity/**/*.sol

📄 CodeRabbit inference engine (CLAUDE.md)

Lint Solidity code with solhint via yarn --cwd solidity lint

Files:

  • solidity/contracts/token/TokenBridgeCctpV2.sol
  • solidity/contracts/token/TokenBridgeCctpBase.sol
  • solidity/contracts/token/TokenBridgeCctpV1.sol
  • solidity/test/token/TokenBridgeCctp.t.sol
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (40)
  • GitHub Check: infra-test
  • GitHub Check: env-test-matrix (mainnet3, inevm, core)
  • GitHub Check: env-test-matrix (mainnet3, inevm, igp)
  • GitHub Check: env-test-matrix (mainnet3, optimism, core)
  • GitHub Check: env-test-matrix (testnet4, sepolia, core)
  • GitHub Check: env-test-matrix (mainnet3, arbitrum, igp)
  • GitHub Check: env-test-matrix (mainnet3, ethereum, igp)
  • GitHub Check: env-test-matrix (mainnet3, optimism, igp)
  • GitHub Check: env-test-matrix (mainnet3, arbitrum, core)
  • GitHub Check: cli-e2e-matrix (warp-extend-config)
  • GitHub Check: cli-e2e-matrix (warp-rebalancer)
  • GitHub Check: cli-e2e-matrix (warp-init)
  • GitHub Check: cli-e2e-matrix (warp-read)
  • GitHub Check: cli-e2e-matrix (core-deploy)
  • GitHub Check: env-test-matrix (mainnet3, ethereum, core)
  • GitHub Check: cli-e2e-matrix (warp-extend-basic)
  • GitHub Check: cli-e2e-matrix (warp-send)
  • GitHub Check: cli-e2e-matrix (warp-check)
  • GitHub Check: cli-e2e-matrix (warp-extend-recovery)
  • GitHub Check: cli-e2e-matrix (warp-deploy)
  • GitHub Check: cli-e2e-matrix (relay)
  • GitHub Check: cli-e2e-matrix (warp-bridge-2)
  • GitHub Check: cli-e2e-matrix (warp-apply)
  • GitHub Check: cli-e2e-matrix (warp-bridge-1)
  • GitHub Check: cli-e2e-matrix (core-check)
  • GitHub Check: cli-e2e-matrix (core-read)
  • GitHub Check: cli-e2e-matrix (core-apply)
  • GitHub Check: cli-e2e-matrix (core-init)
  • GitHub Check: coverage-run
  • GitHub Check: yarn-test-run
  • GitHub Check: cosmos-sdk-e2e-run
  • GitHub Check: cli-install-test-run
  • GitHub Check: diff-check
  • GitHub Check: lander-coverage
  • GitHub Check: lint-rs
  • GitHub Check: test-rs
  • GitHub Check: lint-prettier
  • GitHub Check: agent-configs (mainnet3)
  • GitHub Check: agent-configs (testnet4)
  • GitHub Check: slither
🔇 Additional comments (8)
solidity/contracts/token/TokenBridgeCctpBase.sol (2)

134-165: Good implementation of fee handling.

The change correctly passes the calculated externalFee to _bridgeViaCircle, allowing CCTP V2 to know exactly how much fee is being charged. The burnAmount includes both the desired recipient amount and the fee, which aligns with the "minimum amount out" approach described in the V2 contract comments.


375-380: Signature update looks good.

The addition of the _maxFee parameter to the internal virtual function is consistent with the changes in V1 and V2 implementations. V1 ignores it (with a comment explaining it's not used), while V2 uses it for the fee parameter.

solidity/contracts/token/TokenBridgeCctpV1.sol (1)

108-120: Compatibility parameter handled correctly.

The unused _maxFee parameter is properly added for signature compatibility with the base contract. The comment clearly indicates it's not used for CCTP V1, and the V1 depositForBurn call remains unchanged, which is the correct approach.

solidity/test/token/TokenBridgeCctp.t.sol (5)

27-27: Import needed for new test.

The TokenMessage import is necessary for the new test that validates invalid token message recipients.


726-727: Domain configuration correctly added.

After the upgrade, the domain mapping needs to be configured for the test to work properly. The addition of ism.addDomain(origin, 6) ensures the upgraded contract has the necessary domain mappings.


1315-1315: Test assertion correctly updated.

The change from maxFee to fastFee correctly reflects that the deposit expects the calculated fee amount, not the fee basis points constant.


1445-1445: Test expectation correctly updated.

The fastFee variable represents the calculated external fee amount, which is what gets passed to depositForBurn in the V2 implementation.


1493-1493: Test assertion matches implementation.

Using fastFee here is correct since it represents the actual fee amount calculated for this specific transfer.

@yorhodes
Copy link
Member Author

patch coverage is wrong

@yorhodes yorhodes merged commit 3fc7383 into audit-q3-2025 Oct 28, 2025
53 of 63 checks passed
@yorhodes yorhodes deleted the cctp-patch branch October 28, 2025 18:53
@github-project-automation github-project-automation bot moved this from In Review to Done in Hyperlane Tasks Oct 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants