Skip to content

Conversation

@yorhodes
Copy link
Member

@yorhodes yorhodes commented Oct 27, 2025

Description

EverclearTokenBridge.transferTo() is overridden to perform no action, which prevents _calculateFeesAndCharge() from successfully executing _transferTo(_feeRecipient, feeAmount);.

Introduces new internal _transferFee for warp routes which have no-op _transferTo implementations
Implements overrides in Everclear and CCTP implementations

Drive-by changes

In EverclearEthBridge.constructor(), recommend fixing scale to 1.

Backward compatibility

Yes

Testing

Unit Tests (which assert fees are transferred to recipients)

Summary by CodeRabbit

  • New Features

    • Enhanced fee transfer mechanism with override support for routers that deliver fees differently.
    • Simplified bridge constructor parameters for easier deployment.
  • Documentation

    • Clarified token requirements for collateral usage.
  • Tests

    • Expanded tests covering complex fee-recipient scenarios and asserting correct fee delivery and balances.

@changeset-bot
Copy link

changeset-bot bot commented Oct 27, 2025

⚠️ No Changeset found

Latest commit: 8508ea1

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

@yorhodes yorhodes changed the title Override transfer to with transfer fee behavior fix: override transfer to with transfer fee behavior Oct 27, 2025
@codecov
Copy link

codecov bot commented Oct 27, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.74%. Comparing base (18c32ed) to head (8508ea1).
⚠️ Report is 15 commits behind head on audit-q3-2025.

Additional details and impacted files
@@                Coverage Diff                @@
##           audit-q3-2025    #7264      +/-   ##
=================================================
+ Coverage          76.03%   76.74%   +0.71%     
=================================================
  Files                123      123              
  Lines               2729     2718      -11     
  Branches             244      252       +8     
=================================================
+ Hits                2075     2086      +11     
+ Misses               635      614      -21     
+ Partials              19       18       -1     
Components Coverage Δ
core 87.80% <ø> (ø)
hooks 72.89% <100.00%> (-0.73%) ⬇️
isms 80.19% <100.00%> (ø)
token 87.67% <92.50%> (+3.31%) ⬆️
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.

@yorhodes yorhodes marked this pull request as ready for review October 28, 2025 15:52
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 28, 2025

📝 Walkthrough

Walkthrough

Adds an internal _transferFee hook to TokenRouter and overrides it in CCTP and Everclear bridges so fees are transferred directly from router-held wrapped tokens. Removes the _scale constructor parameter from EverclearEthBridge (hardcoded SCALE = 1). Tests updated to cover LinearFee recipient and assert balances.

Changes

Cohort / File(s) Summary
Fee Transfer Hook
solidity/contracts/token/libs/TokenRouter.sol
Added internal virtual _transferFee(address,uint256) (defaults to _transferTo) and switched _calculateFeesAndCharge to call _transferFee for fee delivery. Doc note added for _externalFeeAmount.
CCTP Bridge Override
solidity/contracts/token/TokenBridgeCctpBase.sol
Added internal override _transferFee(address,uint256) that transfers the fee from the router’s wrapped-token balance directly to recipient.
Everclear Bridge Override & Constructor
solidity/contracts/token/bridge/EverclearTokenBridge.sol
Added internal override _transferFee to transfer fees via wrapped token; removed _scale constructor parameter from EverclearEthBridge and introduced private constant SCALE = 1.
WETHCollateral Note
solidity/contracts/token/libs/TokenCollateral.sol
Added dev note: TokenRouters must have token() == address(0) to use WETHCollateral.
Tests — Everclear
solidity/test/token/EverclearTokenBridge.t.sol
Added test testTransferRemoteWithFeeRecipient() using LinearFee; updated MockEverclearEthBridge constructor signature and all instantiations to remove _scale.
Tests — CCTP assertions
solidity/test/token/TokenBridgeCctp.t.sol
Enhanced test_transferRemoteCctp_withFeeRecipient with balance assertions for fee recipient, user charge, and bridge not retaining fees.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant TokenRouter
    participant FeeHook as _transferFee()
    participant Transfer as _transferTo()

    User->>TokenRouter: initiate transfer (calculate fees)
    activate TokenRouter
    TokenRouter->>FeeHook: _transferFee(feeRecipient, feeAmount)
    activate FeeHook
    alt default
      FeeHook->>Transfer: _transferTo(feeRecipient, feeAmount)
    else override (CCTP/Everclear)
      FeeHook->>Transfer: transfer from router's wrapped token balance
    end
    deactivate FeeHook
    TokenRouter->>Transfer: _transferTo(user, transferAmount)
    deactivate TokenRouter
Loading
sequenceDiagram
    participant Constructor
    participant EverclearEthBridge

    Constructor->>EverclearEthBridge: Old: new(..., _scale, ...)
    Note over EverclearEthBridge: caller supplied scale
    Constructor->>EverclearEthBridge: New: new(...) with SCALE = 1
    Note over EverclearEthBridge: scale hardcoded as constant
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Pay special attention to TokenRouter._calculateFeesAndCharge and the _transferFee hook semantics.
  • Verify CCTP and Everclear overrides correctly use router-held wrapped-token transfers and handle allowances/balances.
  • Confirm test constructor updates match production constructors and that LinearFee test covers reentrancy/edge cases.

Possibly related PRs

Suggested reviewers

  • tkporter
  • ltyu

Poem

In the marsh where routers hum and fees take flight,
A quiet hook now guides the midnight bite.
Bridges pass coins straight from wrapped trove deep,
Tests nod approval while ogres snore and sleep. 🐸

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 pull request title "fix: override transfer to with transfer fee behavior" is directly related to the core changes in the changeset. The title references the introduction of an override mechanism for fee transfers, which aligns with the primary change: introducing a new internal _transferFee hook in TokenRouter and implementing overrides in TokenBridgeCctpBase and EverclearTokenBridge. While the phrasing could be clearer—"override transfer to with transfer fee behavior" reads a bit awkwardly—it does convey meaningful information about the changeset's purpose, which is to enable specialized fee transfer paths when the default _transferTo behavior doesn't suffice.
Description Check ✅ Passed The pull request description is well-structured and addresses all major sections of the template. It explains the problem (EverclearTokenBridge.transferTo() being a no-op breaks fee transfers), describes the solution (introducing the _transferFee hook), lists implementations affected (Everclear and CCTP), and covers backward compatibility, testing, and drive-by changes. The "Related issues" section is not explicitly filled, but since the PR objectives indicate no issues are referenced, this appears intentional rather than an oversight.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch transfer-fee-patch

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 82391aa and 1b98e09.

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

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • solidity/test/token/TokenBridgeCctp.t.sol
  • solidity/contracts/token/TokenBridgeCctpBase.sol
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/TokenBridgeCctpBase.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: cli-e2e-matrix (warp-read)
  • GitHub Check: cli-e2e-matrix (warp-send)
  • GitHub Check: cli-e2e-matrix (warp-extend-recovery)
  • GitHub Check: cli-e2e-matrix (warp-init)
  • GitHub Check: cli-e2e-matrix (core-deploy)
  • GitHub Check: cli-e2e-matrix (warp-rebalancer)
  • GitHub Check: cli-e2e-matrix (warp-extend-config)
  • GitHub Check: cli-e2e-matrix (warp-extend-basic)
  • GitHub Check: cli-e2e-matrix (warp-deploy)
  • GitHub Check: cli-e2e-matrix (core-init)
  • GitHub Check: cli-e2e-matrix (core-apply)
  • GitHub Check: cli-e2e-matrix (warp-bridge-1)
  • GitHub Check: cli-e2e-matrix (core-read)
  • GitHub Check: cli-e2e-matrix (relay)
  • GitHub Check: cli-e2e-matrix (warp-bridge-2)
  • GitHub Check: env-test-matrix (mainnet3, inevm, core)
  • GitHub Check: env-test-matrix (mainnet3, optimism, core)
  • GitHub Check: cli-e2e-matrix (warp-check)
  • GitHub Check: env-test-matrix (mainnet3, optimism, igp)
  • GitHub Check: env-test-matrix (mainnet3, inevm, igp)
  • GitHub Check: env-test-matrix (testnet4, sepolia, core)
  • GitHub Check: env-test-matrix (mainnet3, arbitrum, core)
  • GitHub Check: env-test-matrix (mainnet3, ethereum, igp)
  • GitHub Check: cli-e2e-matrix (warp-apply)
  • GitHub Check: cli-e2e-matrix (core-check)
  • GitHub Check: env-test-matrix (mainnet3, ethereum, core)
  • GitHub Check: env-test-matrix (mainnet3, arbitrum, igp)
  • GitHub Check: coverage-run
  • GitHub Check: cosmos-sdk-e2e-run
  • GitHub Check: yarn-test-run
  • GitHub Check: cli-install-test-run
  • GitHub Check: diff-check
  • GitHub Check: test-rs
  • GitHub Check: agent-configs (testnet4)
  • GitHub Check: lander-coverage
  • GitHub Check: lint-rs
  • GitHub Check: agent-configs (mainnet3)
  • GitHub Check: lint-prettier
  • GitHub Check: slither
🔇 Additional comments (2)
solidity/contracts/token/TokenBridgeCctpBase.sol (1)

369-378: Nice fix! Fee transfer path is properly aligned with CCTP flow.

The override makes sense here. Since _transferTo does nothing (CCTP delivers tokens directly), fees were stuck. Now they're transferred from the router's balance to the recipient. The use of safeTransfer is spot-on for safety.

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

342-346: Solid test coverage! Balance assertions properly validate the fee transfer fix.

The additions capture initial balances and verify the expected state after transfer. All three assertions are mathematically correct and directly test that fee recipients actually receive their fees while the bridge doesn't hoard 'em.

Also applies to: 367-387


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: 0

♻️ Duplicate comments (1)
solidity/contracts/token/libs/TokenRouter.sol (1)

189-197: Switching to _transferFee fixes the fee black hole on no‑op _transferTo bridges. Nice.

This cleanly routes fee payouts even when delivery is handled elsewhere (CCTP/Everclear). Please double‑check every router where _transferTo is intentionally no‑op also overrides _transferFee to avoid silent drops.

#!/bin/bash
# Find files that override _transferTo with an empty/no-op body, and ensure they also override _transferFee.

set -euo pipefail

# 1) Collect no-op _transferTo overrides
tmp_noop="$(mktemp)"
fd -t f -e sol solidity | while read -r f; do
  if rg -nUP '(?s)function\s+_transferTo\s*\([^)]*\)\s*internal\s+override[^{]*\{\s*(//[^\n]*\n)?\s*\}' "$f" >/dev/null; then
    echo "$f" >> "$tmp_noop"
  fi
done

echo "No-op _transferTo overrides:"
cat "$tmp_noop" || true
echo

# 2) Ensure those files also override _transferFee
echo "Files missing _transferFee override:"
while read -r f; do
  rg -nP 'function\s+_transferFee\s*\(' "$f" >/dev/null || echo "$f"
done < "$tmp_noop" || true
🧹 Nitpick comments (3)
solidity/contracts/token/libs/TokenRouter.sol (1)

366-384: New _transferFee hook reads well; consider an event (optional).

Defaulting to _transferTo keeps synthetic/collateral routes tidy. If you want easier analytics later, an optional FeeTransferred(recipient, amount) event here would be handy, but not required.

solidity/contracts/token/libs/TokenCollateral.sol (1)

27-28: Clarify the WETHCollateral doc (tiny).

Suggest spelling out that routers quote as native (token() == address(0)) while still custodying WETH internally.

- * @dev TokenRouters must have `token() == address(0)` to use this library.
+ * @dev Intended for routers that custody WETH but quote/route as native ETH:
+ * token() MUST return address(0) while the router still holds an IWETH instance.
solidity/test/token/EverclearTokenBridge.t.sol (1)

404-452: Solid regression: assert fee recipient actually gets paid; make quote deterministic.

Nice coverage—this catches the old “fee swallowed by bridge” behavior and proves the fee lands with the recipient contract. One tweak to keep the mud from splashing: quote the fee under the same caller the router/bridge would use, in case the fee calc depends on msg.sender or context.

Suggested changes:

  • Prank as the bridge when calling feeContract.quoteTransferRemote.
  • Optionally ensure the fee is non‑zero to avoid a vacuous pass.

Based on learnings

-        // Get the expected fee from the feeContract
-        uint256 expectedFeeRecipientFee = feeContract
-        .quoteTransferRemote(DESTINATION, RECIPIENT, TRANSFER_AMT)[0].amount;
+        // Get the expected fee from the feeContract as if called by the bridge
+        vm.prank(address(bridge));
+        uint256 expectedFeeRecipientFee =
+            feeContract.quoteTransferRemote(DESTINATION, RECIPIENT, TRANSFER_AMT)[0].amount;
+        // Optional: ensure test isn't vacuous
+        // assertGt(expectedFeeRecipientFee, 0);

Also, per repo guidelines, give solhint a quick run to keep everything smelling fresh:

  • yarn --cwd solidity lint
📜 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 82391aa.

📒 Files selected for processing (6)
  • solidity/contracts/token/TokenBridgeCctpBase.sol (1 hunks)
  • solidity/contracts/token/bridge/EverclearTokenBridge.sol (2 hunks)
  • solidity/contracts/token/libs/TokenCollateral.sol (1 hunks)
  • solidity/contracts/token/libs/TokenRouter.sol (3 hunks)
  • solidity/test/token/EverclearTokenBridge.t.sol (3 hunks)
  • solidity/test/token/TokenBridgeCctp.t.sol (2 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/TokenBridgeCctpBase.sol
  • solidity/contracts/token/libs/TokenRouter.sol
  • solidity/contracts/token/libs/TokenCollateral.sol
  • solidity/contracts/token/bridge/EverclearTokenBridge.sol
solidity/**/*.sol

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • solidity/contracts/token/TokenBridgeCctpBase.sol
  • solidity/contracts/token/libs/TokenRouter.sol
  • solidity/test/token/EverclearTokenBridge.t.sol
  • solidity/contracts/token/libs/TokenCollateral.sol
  • solidity/test/token/TokenBridgeCctp.t.sol
  • solidity/contracts/token/bridge/EverclearTokenBridge.sol
🧠 Learnings (2)
📓 Common learnings
Learnt from: yorhodes
PR: hyperlane-xyz/hyperlane-monorepo#6750
File: solidity/contracts/token/bridge/EverclearTokenBridge.sol:212-212
Timestamp: 2025-09-18T15:49:20.478Z
Learning: EverclearTokenBridge sends funds directly to recipients for ERC20 transfers, while EverclearEthBridge sends funds to remote routers that handle WETH unwrapping and ETH delivery. The virtual _getReceiver and _getIntentCalldata functions enable this architectural difference between the two bridge implementations.
📚 Learning: 2025-09-18T15:49:20.478Z
Learnt from: yorhodes
PR: hyperlane-xyz/hyperlane-monorepo#6750
File: solidity/contracts/token/bridge/EverclearTokenBridge.sol:212-212
Timestamp: 2025-09-18T15:49:20.478Z
Learning: EverclearTokenBridge sends funds directly to recipients for ERC20 transfers, while EverclearEthBridge sends funds to remote routers that handle WETH unwrapping and ETH delivery. The virtual _getReceiver and _getIntentCalldata functions enable this architectural difference between the two bridge implementations.

Applied to files:

  • solidity/test/token/EverclearTokenBridge.t.sol
  • solidity/contracts/token/bridge/EverclearTokenBridge.sol
🔇 Additional comments (7)
solidity/contracts/token/libs/TokenRouter.sol (1)

252-254: Doc nit: fee denomination note is helpful.

Stating external fees must be in token() saves folks from muddy waters later. All good here.

solidity/contracts/token/bridge/EverclearTokenBridge.sol (2)

407-416: ETB overriding _transferFee is the right move.

Direct ERC20 transfer from the router balance ensures the fee doesn’t sink when _transferTo is a no‑op. Fits the Everclear split‑delivery model nicely. Based on learnings.


443-454: Verified: all callsites comply with the new constructor signature.

Both the test instantiation (line 977) and MockEverclearEthBridge (line 853) correctly invoke the updated constructor with only 3 parameters (_weth, _mailbox, _everclearAdapter) and do not pass a _scale argument. The hardcoding of SCALE=1 in the EverclearEthBridge constructor is sound, and no deploy or test code is trying to override it.

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

341-386: Tests nail the fee path behavior.

Capturing pre-balances and asserting fee recipient gets paid, user charged charge, and bridge holds no fee makes the change airtight. Smells like fresh pine.

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

369-378: CCTP fee transfer override is spot on.

Paying fees via wrappedToken.safeTransfer aligns with burning only amount + externalFee. Matches the updated tests and avoids fee residue on the bridge.

solidity/test/token/EverclearTokenBridge.t.sol (2)

31-31: Import scope looks good.

Pulling in LinearFee just for tests keeps things tidy. No nits here.


849-854: Constructor signature change verified—no lingering 4‑arg calls remain.

The swamp's been searched from top to bottom, and it's clean. No old 4‑arg instantiations of EverclearEthBridge or MockEverclearEthBridge left lurking about—the _scale parameter's been properly pruned throughout.

@yorhodes yorhodes merged commit 7f5656b into audit-q3-2025 Oct 28, 2025
7 checks passed
@yorhodes yorhodes deleted the transfer-fee-patch branch October 28, 2025 19:51
@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