Skip to content

feat(multicollateral): add TokenBridgeOft warp route adapter for LayerZero OFTs#8306

Merged
nambrot merged 27 commits intomainfrom
nam/token-bridge-oft
Mar 13, 2026
Merged

feat(multicollateral): add TokenBridgeOft warp route adapter for LayerZero OFTs#8306
nambrot merged 27 commits intomainfrom
nam/token-bridge-oft

Conversation

@nambrot-agent
Copy link
Copy Markdown
Contributor

@nambrot-agent nambrot-agent commented Mar 8, 2026

Summary

  • Added TokenBridgeOft contract in solidity/multicollateral/ that extends TokenRouter to bridge tokens via LayerZero OFT.send() instead of Hyperlane message dispatch
  • Supports all OFT patterns: native OFT (burn/mint), OFTAdapter (lock/unlock), and OFTWrapper (Paxos-style burn/mint)
  • Wired into the SDK as TokenType.collateralOft with deployer, reader, and CLI support following the CCTP pattern
  • Fork tests against live USDT0 and pyUSD OFT contracts on Arbitrum and Ethereum mainnet

Details

Solidity:

  • TokenBridgeOft.sol — extends TokenRouter, overrides _quoteGasPayment (OFT native fee) and _transferRemote (OFT.send)
  • IOFT.sol — minimal LayerZero OFT interface (SendParam, MessagingFee, OFTReceipt, etc.)
  • TokenBridgeOft.fork.t.sol — 4 fork test contracts covering USDT0 OFT (Arb), USDT0 OFT Adapter (Eth), pyUSD OFTWrapper (Arb), pyUSD OFTWrapper (Eth)
  • Uses EnumerableMap for domain→LZ EID mappings (enumerable on-chain for SDK reader)

Refund handling:

  • refundAddress storage was removed. Both the LZ refund and any excess msg.value are returned to msg.sender directly, with a hard revert if the refund transfer fails.
  • MCR (MovableCollateralRouter) does not have receive(), so the refund call will revert if there's any excess. We accept this because MCR quotes atomically right before calling transferRemote, so the msg.value passed should exactly match the required fee with no excess to refund.
  • removeDomain now reverts on non-existent keys instead of silently no-oping.

SDK:

  • TokenType.collateralOft added to config, contracts, types, deploy, reader, TokenStandard
  • OftTokenConfigSchema with fields: oft, domainMappings, extraOptions
  • configureOftDomains() in deployer maps Hyperlane domain IDs → LZ endpoint IDs post-deploy
  • deriveHypCollateralOftTokenConfig() in EvmWarpRouteReader for on-chain config derivation
  • Type detection via oft() method in deriveTokenType()

CI:

  • Fork tests now use vm.createSelectFork() with RPC endpoint names from foundry.toml
  • Added multicollateral fork test step to solidity-fork-tests.yml workflow

Test plan

  • forge build passes in both solidity/ and solidity/multicollateral/
  • Full repo pnpm build passes
  • Unit tests pass (58 total: 35 MultiCollateral + 23 TokenBridgeOft)
  • Fork tests use vm.createSelectFork() and run in CI via solidity-fork-tests.yml

🤖 Generated with Claude Code


Open with Devin

Summary by CodeRabbit

  • New Features

    • OFT-based bridge for collateral tokens: cross-chain transfers, domain mapping, adjustable extra-options, and native fee quoting/refunds
    • SDK support to detect, read, derive, and deploy OFT-backed collateral configs
  • Tests

    • Added extensive unit and fork test suites covering OFT flows, adapters, fees, dust handling, and domain management
  • Chores

    • Updated build/test scripts, CI workflow, and local fork endpoints to support OFT workflows
  • CLI

    • Added user-facing description for OFT collateral token type

…rZero OFTs

Added TokenBridgeOft contract that extends TokenRouter to bridge tokens via
LayerZero OFT.send() instead of Hyperlane message dispatch. Supports all OFT
patterns: native OFT (burn/mint), OFTAdapter (lock/unlock), and OFTWrapper
(Paxos-style burn/mint).

Wired into the SDK as TokenType.collateralOft with full deployer, reader, and
CLI support following the same pattern as CCTP.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 8, 2026

📝 Walkthrough

Walkthrough

Adds a LayerZero OFT bridge: new TokenBridgeOft contract and IOFT interface, comprehensive unit and fork tests, SDK and CLI integration (token schema, reader, deployer, typechain, CI), and tooling to manage OFT-backed collateral, domain↔LZ EID mappings, extraOptions, fee quoting, and cross-chain send/refund flows.

Changes

Cohort / File(s) Summary
Core OFT Bridge & Interface
solidity/multicollateral/contracts/TokenBridgeOft.sol, solidity/multicollateral/contracts/interfaces/layerzero/IOFT.sol
Adds TokenBridgeOft (implements ITokenBridge, OwnableUpgradeable) integrating LayerZero OFT: send/quote flows, decimal conversion, gross/gas fee inversion, domain→LZ EID map, extraOptions, refunds, and IOFT interface + structs for quoting/sending and receipts.
Solidity Tests (unit & fork)
solidity/multicollateral/test/TokenBridgeOft.t.sol, solidity/multicollateral/test/TokenBridgeOft.fork.t.sol
Adds MockOFT/MockOFTAdapter and extensive unit & fork tests covering constructor, adapter vs non‑adapter flows, fee inversion, dust handling, quotes, transfers, admin actions, and failure cases.
Build / Package & CI
solidity/multicollateral/package.json, .github/workflows/solidity-fork-tests.yml
Adds TokenBridgeOft to TypeChain outputs, updates test scripts (test:ci, test:fork), and adds workflow step to run multicollateral fork tests.
Foundry Config
solidity/multicollateral/foundry.toml
Adds RPC endpoints (mainnet, arbitrum) via env vars to support fork tests.
SDK: Types & Config
typescript/sdk/src/token/types.ts, typescript/sdk/src/token/config.ts, typescript/sdk/src/token/TokenStandard.ts
Introduces collateralOft token type, OftTokenConfig schema/type, isOftTokenConfig guard; maps type to token standards and movability.
SDK: Reader, Contracts & Exports
typescript/sdk/src/token/EvmWarpRouteReader.ts, typescript/sdk/src/token/contracts.ts, typescript/sdk/src/index.ts
Registers TokenBridgeOft__factory, wires collateralOft into derivation (adds deriveHypCollateralOftTokenConfig), and exports OFT config types/guards.
SDK: Deployer & Runtime Guards
typescript/sdk/src/token/deploy.ts, typescript/sdk/src/token/EvmWarpModule.ts
Deploy flow now handles OFT contracts and calls new configureOftDomains to set domain mappings/extraOptions; runtime guards skip router/gas enrollment for OFT tokens.
CLI / UX
typescript/cli/src/config/warp.ts, typescript/cli/src/tests/ethereum/commands/warp.ts
Documents TokenType.collateralOft, marks it YAML-only and excluded from random warp config generation.
Typechain / Contracts Mapping
typescript/sdk/src/token/contracts.ts, solidity/multicollateral/package.json
Adds factory mapping and ensures TokenBridgeOft typechain artifact is emitted.

Sequence Diagram

sequenceDiagram
    rect rgba(60,179,113,0.5)
    actor User
    end
    rect rgba(65,105,225,0.5)
    participant TokenBridgeOft
    participant OFT
    end
    rect rgba(255,165,0,0.5)
    participant LayerZero
    participant Destination
    end

    User->>TokenBridgeOft: transferRemote(hyperlaneDomain, to, amount)
    TokenBridgeOft->>TokenBridgeOft: compute gross amount, pull tokens (incl external fee)
    TokenBridgeOft->>OFT: quoteSend(sendParam, payInLzToken=false)
    OFT-->>TokenBridgeOft: MessagingFee(nativeFee, lzTokenFee)
    TokenBridgeOft->>OFT: send(sendParam, MessagingFee, refundAddress) payable
    OFT->>LayerZero: emit message (token transfer + payload)
    LayerZero->>Destination: deliver message
    Destination-->>OFT: mint/credit tokens on remote chain
    OFT-->>TokenBridgeOft: MessagingReceipt / OFTReceipt
    TokenBridgeOft-->>User: return messageId / emit SentTransferRemote
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

A wee bridge o' OFT and careful math,
Domains set true, no unexpected path.
Tokens hauled with fees and dust made neat,
Extra options locked, refunds complete.
Chains sing soft as ledgers meet.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely describes the main addition: a TokenBridgeOft adapter for LayerZero OFT integration in the multicollateral system, which aligns with the substantial changes across Solidity and TypeScript components.
Description check ✅ Passed The PR description is comprehensive, covering summary, detailed technical changes, refund handling, SDK integration, CI updates, and test results. All required sections from the template are present with substantive content.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch nam/token-bridge-oft
📝 Coding Plan
  • Generate coding plan for human review comments

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.

coderabbitai[bot]

This comment was marked as resolved.

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 8, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.49%. Comparing base (b9c6844) to head (e224644).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #8306   +/-   ##
=======================================
  Coverage   76.49%   76.49%           
=======================================
  Files         128      128           
  Lines        3416     3416           
  Branches      290      290           
=======================================
  Hits         2613     2613           
  Misses        786      786           
  Partials       17       17           
Components Coverage Δ
core 87.80% <ø> (ø)
hooks 74.55% <ø> (ø)
isms 81.46% <ø> (ø)
token 87.33% <ø> (ø)
middlewares 85.47% <ø> (ø)
🚀 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.

- Add mock-based unit tests (23 tests) covering construction, domain
  mapping, quoting, transfers, admin, handle, and slippage protection
- Expand NatSpec docs explaining TokenRouter coupling and token support
- Add zero-EID validation in addDomain
- Fix CI by excluding fork tests from test:ci
- Bound domainMappings schema to uint32 range
- Add ERC20 metadata fetching in warp route reader
Copy link
Copy Markdown
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.

🧹 Nitpick comments (2)
solidity/multicollateral/contracts/TokenBridgeOft.sol (1)

174-181: Consider emitting an event when overwriting an existing domain mapping.

Right now, addDomain will silently overwrite an existing mapping without any indication. While the DomainAdded event fires regardless, it might be helpful to distinguish between "add new" and "update existing" for off-chain indexers. Not a blocker — just somethin' to think about if domain management becomes more complex later.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@solidity/multicollateral/contracts/TokenBridgeOft.sol` around lines 174 -
181, The addDomain function currently overwrites
hyperlaneDomainToLzEid[_hyperlaneDomain] and always emits DomainAdded, which
hides whether this was a new mapping or an update; modify addDomain to check the
existing value (hyperlaneDomainToLzEid[_hyperlaneDomain]) and if non-zero emit a
distinct event (e.g., DomainUpdated with old and new _lzEid) or include the
previous value in DomainAdded so off‑chain indexers can tell adds from updates;
ensure the check happens before assignment and that the chosen event signature
is declared and emitted (reference addDomain, hyperlaneDomainToLzEid, and
DomainAdded/DomainUpdated).
solidity/multicollateral/test/TokenBridgeOft.t.sol (1)

18-126: MockOFT is well-designed for deterministic testing.

The mock covers all the IOFT interface methods with configurable return values. Nice touch with shouldRevertOnSend for failure path testing — though I notice it ain't used in any tests yet. Might want to add a test that exercises that path to ensure transfer failures from the OFT layer are handled gracefully.

Consider adding a test for OFT.send failure
function test_transferRemote_oftSendReverts() public {
    mockOft.setShouldRevertOnSend(true);
    
    vm.startPrank(caller);
    token.approve(address(bridge), type(uint256).max);
    
    vm.expectRevert("MockOFT: send reverted");
    bridge.transferRemote{value: 0.001 ether}(DOMAIN_ETH, recipient, 100e6);
    vm.stopPrank();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@solidity/multicollateral/test/TokenBridgeOft.t.sol` around lines 18 - 126,
Add a unit test that exercises the OFT failure path by calling
MockOFT.setShouldRevertOnSend(true), then in a prank as caller approve the
bridge via token.approve(address(bridge), type(uint256).max) and call
bridge.transferRemote{value: 0.001 ether}(DOMAIN_ETH, recipient, 100e6) while
asserting the revert with vm.expectRevert("MockOFT: send reverted"); reference
MockOFT.setShouldRevertOnSend, bridge.transferRemote, token.approve,
vm.expectRevert, DOMAIN_ETH, recipient, and caller to locate where to add the
test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@solidity/multicollateral/contracts/TokenBridgeOft.sol`:
- Around line 174-181: The addDomain function currently overwrites
hyperlaneDomainToLzEid[_hyperlaneDomain] and always emits DomainAdded, which
hides whether this was a new mapping or an update; modify addDomain to check the
existing value (hyperlaneDomainToLzEid[_hyperlaneDomain]) and if non-zero emit a
distinct event (e.g., DomainUpdated with old and new _lzEid) or include the
previous value in DomainAdded so off‑chain indexers can tell adds from updates;
ensure the check happens before assignment and that the chosen event signature
is declared and emitted (reference addDomain, hyperlaneDomainToLzEid, and
DomainAdded/DomainUpdated).

In `@solidity/multicollateral/test/TokenBridgeOft.t.sol`:
- Around line 18-126: Add a unit test that exercises the OFT failure path by
calling MockOFT.setShouldRevertOnSend(true), then in a prank as caller approve
the bridge via token.approve(address(bridge), type(uint256).max) and call
bridge.transferRemote{value: 0.001 ether}(DOMAIN_ETH, recipient, 100e6) while
asserting the revert with vm.expectRevert("MockOFT: send reverted"); reference
MockOFT.setShouldRevertOnSend, bridge.transferRemote, token.approve,
vm.expectRevert, DOMAIN_ETH, recipient, and caller to locate where to add the
test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 40980632-8ddf-47c5-9fd9-6ef8541a7bcb

📥 Commits

Reviewing files that changed from the base of the PR and between 9b72bca and 91d21a4.

📒 Files selected for processing (5)
  • solidity/multicollateral/contracts/TokenBridgeOft.sol
  • solidity/multicollateral/package.json
  • solidity/multicollateral/test/TokenBridgeOft.t.sol
  • typescript/sdk/src/token/EvmWarpRouteReader.ts
  • typescript/sdk/src/token/types.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • solidity/multicollateral/package.json
  • typescript/sdk/src/token/EvmWarpRouteReader.ts

Comment thread typescript/sdk/src/token/EvmWarpRouteReader.ts Outdated
Comment thread typescript/cli/src/config/warp.ts
- Replace plain mapping with EnumerableMap.UintToUintMap for domain mappings
- Add getDomainMappings() view to enumerate all configured domains on-chain
- Update reader to fetch domain mappings from chain instead of returning {}
- Filter collateralOft and collateralCctp from interactive CLI prompt
coderabbitai[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
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.

Review Summary

Well-structured adapter that bridges Hyperlane warp routes to LayerZero OFTs. Good test coverage with unit tests (mock OFT) and fork tests (USDT0, pyUSD on Arb/Eth). SDK integration is clean — reader fetches domain mappings from chain via EnumerableMap, deploy flow configures domains/options/refund.

CodeRabbit's earlier comments on ERC20 metadata, uint32 validation, and CLI TYPE_CHOICES have all been addressed. The refundAddress constructor issue (CodeRabbit's latest comment) is part of a larger design concern detailed below.

Main concerns

  1. Native value handling / refund flow — see inline comments on _transferRemote and refundAddress. Excess msg.value and LZ refunds accumulate in the contract with no recovery path. This is especially problematic when called from MCR's rebalance(), where MultiCollateral has no receive() to accept refunds.

  2. Fork tests not running in CI — see inline comment on test:ci. The existing solidity-fork-tests.yml workflow runs from ./solidity and picks up ForkTest contracts there, but doesn't run solidity/multicollateral fork tests. These are the only tests validating real OFT burn/mint/lock flows.

  3. removeDomain silently succeeds on non-existent domainsEnumerableMap.remove returns false if key doesn't exist, but the function ignores the return value and still emits DomainRemoved. Minor but misleading for indexers.

Nits

  • _handle reverts with a string; could use a custom error for gas consistency with LzEidNotConfigured

Comment thread solidity/multicollateral/contracts/TokenBridgeOft.sol Outdated
Comment thread solidity/multicollateral/contracts/TokenBridgeOft.sol Outdated
Comment thread solidity/multicollateral/contracts/TokenBridgeOft.sol
Comment thread solidity/multicollateral/package.json
Removed refundAddress storage/setter in favor of always refunding msg.sender.
Added excess value sweep with revert on failure. Made removeDomain revert
on non-existent domains. Removed refundAddress from SDK reader/deployer/types.
Added vm.createSelectFork to fork tests and CI workflow step for multicollateral.
Comment thread solidity/multicollateral/contracts/TokenBridgeOft.sol Outdated
Comment thread solidity/multicollateral/contracts/TokenBridgeOft.sol Outdated
Comment thread solidity/multicollateral/contracts/TokenBridgeOft.sol Outdated
Comment thread solidity/multicollateral/contracts/TokenBridgeOft.sol Outdated
Comment thread solidity/multicollateral/contracts/TokenBridgeOft.sol
Comment thread solidity/multicollateral/contracts/TokenBridgeOft.sol Outdated
Comment thread solidity/multicollateral/contracts/TokenBridgeOft.sol Outdated
Comment thread solidity/multicollateral/contracts/TokenBridgeOft.sol Outdated
Comment thread solidity/multicollateral/contracts/TokenBridgeOft.sol Outdated
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

Maintains Hyperlane warp route semantics where transferRemote(_amount)
means recipient gets _amount and fees are surfaced via quoteTransferRemote.
coderabbitai[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

OFTs with localDecimals > sharedDecimals (e.g. 18/6) truncate
sub-sharedDecimals precision via _removeDust(). Without rounding,
ceilDiv produces dusty gross amounts that cause SlippageExceeded
after the OFT's internal truncation. Now:
- grossAmount is rounded UP to the next dust-free boundary
- minAmountLD is rounded DOWN (OFT can't deliver sub-dust precision)
- decimalConversionRate stored as immutable for gas efficiency
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@nambrot nambrot enabled auto-merge March 12, 2026 21:08
@nambrot
Copy link
Copy Markdown
Contributor

nambrot commented Mar 12, 2026

@coderabbitai resume

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 12, 2026

✅ Actions performed

Reviews resumed.

devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

nambrot added 2 commits March 12, 2026 17:32
Use as unknown as HyperlaneContracts<Factories> with a CAST comment
explaining why instead of as any. Type the oftDeployedMap with the
proper Factories generic.
Fork tests now derive the total token charge (including OFT fees) from
quoteTransferRemote instead of using hardcoded AMOUNT for deals,
approvals, and balance assertions.
@nambrot
Copy link
Copy Markdown
Contributor

nambrot commented Mar 12, 2026

Re: CodeRabbit's comment on fork tests — fixed in 06d6fdf. All four test_transferRemote functions now derive totalTokenCharge from quotes[1].amount and use it for deals, approvals, and balance assertions.

devin-ai-integration[bot]

This comment was marked as resolved.

Leverage addDeployedContracts (accepts HyperlaneContracts<any>) and the
fact that super.deploy() returns this.deployedContracts which already
includes the OFT entries. No intermediate map or cast needed.
@nambrot nambrot added this pull request to the merge queue Mar 12, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Mar 12, 2026
@nambrot nambrot added this pull request to the merge queue Mar 12, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Mar 12, 2026
collateralOft requires a real OFT contract and cannot be tested with
randomly generated configs in the CLI e2e test harness.
@nambrot nambrot enabled auto-merge March 12, 2026 22:51
devin-ai-integration[bot]

This comment was marked as resolved.

OFT contracts were added to this.deployedContracts before super.deploy(),
causing the parent HyperlaneRouterDeployer to call Router-specific methods
(enrollRemoteRouters, configureClients) on OFT entries that lack the
Router interface. Store OFT contracts in a local map and merge after
super.deploy() returns.
@nambrot
Copy link
Copy Markdown
Contributor

nambrot commented Mar 12, 2026

Re: Devin's comment on OFT deploy ordering — fixed in e224644. OFT contracts are now stored in a local map and only merged into this.deployedContracts after super.deploy() returns, preventing enrollRemoteRouters/configureClients from seeing OFT entries.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

View 14 additional findings in Devin Review.

Open in Devin Review

Comment thread typescript/sdk/src/token/deploy.ts
Comment thread typescript/sdk/src/token/EvmWarpRouteReader.ts
Copy link
Copy Markdown
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

🧹 Nitpick comments (2)
typescript/sdk/src/token/deploy.ts (2)

454-462: Consider batch domain addition if supported.

These domain mappings are added one at a time, like peeling layers off an onion individually. If the contract ever gets a batch addDomains (similar to how CCTP does it at line 346), could speed things up. Not blocking, just something to keep in the swamp log.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@typescript/sdk/src/token/deploy.ts` around lines 454 - 462, The loop that
calls tokenBridge.addDomain for each entry in resolvedMappings should be updated
to use a batch call if the contract supports it: detect whether
tokenBridge.addDomains (or similar batch method) exists and, if so, construct
arrays of domain IDs and lzEids from resolvedMappings and call
tokenBridge.addDomains in a single multiProvider.handleTx(chain,
tokenBridge.addDomains(...)) call; if the batch method is not present, fall back
to the existing per-entry loop that uses multiProvider.handleTx(chain,
tokenBridge.addDomain(Number(domainId), lzEid)). Ensure you reference
resolvedMappings, tokenBridge.addDomain, tokenBridge.addDomains,
multiProvider.handleTx, and chain when implementing this change.

785-785: Type could be tightened.

This Record<string, Record<string, unknown>> is a bit loose. If you're fixing the merge into deployedContractsMap anyway, might as well give this proper typing from the start.

💡 Tighter typing suggestion
-    const oftContracts: Record<string, Record<string, unknown>> = {};
+    const oftContracts: HyperlaneContractsMap<Factories> = {};

Then the merge and return will have consistent types without needing casts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@typescript/sdk/src/token/deploy.ts` at line 785, Define and use a specific
type alias instead of the loose inline type: add e.g. type DeployedContractsMap
= Record<string, Record<string, unknown>> and replace the inline annotation on
oftContracts with const oftContracts: DeployedContractsMap = {}; then update any
references/merges with deployedContractsMap and the function return signature to
use DeployedContractsMap so the merge and return have consistent, tighter
typing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@typescript/sdk/src/token/deploy.ts`:
- Around line 800-814: The OFT entries merged into this.deployedContracts via
this.addDeployedContracts(...) are not being merged into the local
deployedContractsMap returned by super.deploy(...), causing
configureOftDomains(configMap, deployedContractsMap) and the function's return
to miss OFT deployments; after the loop that iterates oftContracts (where
this.addDeployedContracts is called), also merge the same OFT contract entries
into deployedContractsMap (preserving chain keys and contract lists) so that
configureOftDomains and the eventual return include the OFT deployments—modify
the block that handles oftContracts to update both this.deployedContracts and
deployedContractsMap consistently.

---

Nitpick comments:
In `@typescript/sdk/src/token/deploy.ts`:
- Around line 454-462: The loop that calls tokenBridge.addDomain for each entry
in resolvedMappings should be updated to use a batch call if the contract
supports it: detect whether tokenBridge.addDomains (or similar batch method)
exists and, if so, construct arrays of domain IDs and lzEids from
resolvedMappings and call tokenBridge.addDomains in a single
multiProvider.handleTx(chain, tokenBridge.addDomains(...)) call; if the batch
method is not present, fall back to the existing per-entry loop that uses
multiProvider.handleTx(chain, tokenBridge.addDomain(Number(domainId), lzEid)).
Ensure you reference resolvedMappings, tokenBridge.addDomain,
tokenBridge.addDomains, multiProvider.handleTx, and chain when implementing this
change.
- Line 785: Define and use a specific type alias instead of the loose inline
type: add e.g. type DeployedContractsMap = Record<string, Record<string,
unknown>> and replace the inline annotation on oftContracts with const
oftContracts: DeployedContractsMap = {}; then update any references/merges with
deployedContractsMap and the function return signature to use
DeployedContractsMap so the merge and return have consistent, tighter typing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c4b1835a-a897-4f7f-b08e-ccaf9c0d4ad4

📥 Commits

Reviewing files that changed from the base of the PR and between 69fede2 and e224644.

📒 Files selected for processing (1)
  • typescript/sdk/src/token/deploy.ts

Comment thread typescript/sdk/src/token/deploy.ts
@nambrot
Copy link
Copy Markdown
Contributor

nambrot commented Mar 12, 2026

Re: CodeRabbit's comment on OFT contracts missing from deployedContractsMap

This is a false positive. deployedContractsMap IS this.deployedContracts — same object reference (see HyperlaneDeployer.deploy() at line 205: return this.deployedContracts). When addDeployedContracts mutates this.deployedContracts[chain], deployedContractsMap[chain] sees the same data since they point to the same object.

@coderabbitai can you confirm this is not an issue?

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 12, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find HyperlaneDeployer.deploy() method to verify the object reference claim
ast-grep --pattern 'deploy($$$) {
  $$$
  return this.deployedContracts
  $$$
}'

# Also search with rg for the specific line
rg -n "return this\.deployedContracts" --type=ts -C5

Length of output: 802


@nambrot: You're right, this is a false positive. The code at line 205 of HyperlaneDeployer.ts confirms it:

return this.deployedContracts;

This is a direct reference return — no copy or spread — so deployedContractsMap and this.deployedContracts point to the same object. Any addDeployedContracts call that mutates this.deployedContracts[chain] is immediately visible via deployedContractsMap[chain]. My concern was swampy nonsense — sorry for the noise!


🧠 Learnings used
Learnt from: nambrot
Repo: hyperlane-xyz/hyperlane-monorepo PR: 8327
File: solidity/multicollateral/contracts/CrossCollateralRouter.sol:53-60
Timestamp: 2026-03-11T23:01:12.207Z
Learning: In `solidity/multicollateral/contracts/CrossCollateralRouter.sol` (PR `#8327`, hyperlane-xyz/hyperlane-monorepo), the MultiCollateral → CrossCollateral rename intentionally drops all legacy ABI selectors and events (e.g., `enrollRouters`, `unenrollRouters`, `getEnrolledRouters`, `getEnrolledDomains`, `quoteTransferRemoteTo`, `RouterEnrolled`, `RouterUnenrolled`). No backward-compatible wrapper aliases or dual-event emissions are added. Do not flag the absence of legacy ABI aliases as a backward-compatibility issue in this codebase/branch.

Learnt from: nambrot
Repo: hyperlane-xyz/hyperlane-monorepo PR: 8248
File: solidity/multicollateral/contracts/MultiCollateralRoutingFee.sol:33-35
Timestamp: 2026-03-02T21:12:14.049Z
Learning: In the hyperlane-xyz/hyperlane-monorepo repository, for MultiCollateralRoutingFee.sol and similar contracts, deploying with address(0) as owner is an intentional design choice to support explicit no-owner (immutable) configurations. Zero-owner deployments should not be flagged as errors.

Learnt from: nambrot-agent
Repo: hyperlane-xyz/hyperlane-monorepo PR: 8306
File: typescript/sdk/src/token/EvmWarpRouteReader.ts:857-884
Timestamp: 2026-03-08T13:54:29.577Z
Learning: In `typescript/sdk/src/token/EvmWarpRouteReader.ts`, `deriveHypCollateralOftTokenConfig` intentionally returns `domainMappings: {}`. The `hyperlaneDomainToLzEid` Solidity mapping on `TokenBridgeOft` has non-iterable keys, so domain mappings cannot be enumerated on-chain and are instead populated from the deploy config (not read back from chain state). Do not flag `domainMappings: {}` as missing data.

Learnt from: paulbalaji
Repo: hyperlane-xyz/hyperlane-monorepo PR: 8292
File: typescript/sdk/src/ism/HyperlaneIsmFactory.ts:717-733
Timestamp: 2026-03-05T11:39:36.798Z
Learning: In hyperlane-xyz/hyperlane-monorepo, `previewFactoryDeployAddress` in `typescript/sdk/src/ism/HyperlaneIsmFactory.ts` is a private method intentionally tested only through integration paths (e.g. `HyperlaneIsmFactory.hardhat-test.ts`). Unit tests for this method and its fallback logic (`getAddressResult` fallback, mismatch logging) are deferred to the viem migration PR where the factory code is being reworked. Do not flag missing unit tests for this private method as an issue.

Learnt from: nambrot-agent
Repo: hyperlane-xyz/hyperlane-monorepo PR: 8306
File: typescript/sdk/src/token/EvmWarpRouteReader.ts:857-884
Timestamp: 2026-03-08T13:54:29.577Z
Learning: In `typescript/sdk/src/token/EvmWarpRouteReader.ts`, `deriveHypCollateralOftTokenConfig` does not call `fetchScale` on the `TokenBridgeOft` contract. This is intentional: `TokenBridgeOft` always passes `(1, 1)` (identity scale) to `TokenRouter`, and OFT handles its own decimal normalization via `sharedDecimals`. `fetchScale` would return `undefined` for identity scale anyway, so omitting the call is correct.

Learnt from: CR
Repo: hyperlane-xyz/hyperlane-monorepo PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-04T16:26:08.116Z
Learning: Understand Warp Route concept: token bridge deployment across multiple chains for token transfers

Learnt from: nambrot
Repo: hyperlane-xyz/hyperlane-monorepo PR: 8327
File: typescript/cli/src/tests/ethereum/warp/warp-cross-collateral.e2e-test.ts:91-98
Timestamp: 2026-03-11T23:01:09.617Z
Learning: In the Hyperlane monorepo (PR `#8327`), legacy `multiCollateral` token type config input compatibility was intentionally removed as part of the MultiCollateral → CrossCollateral rename. There is no backward-compat canonicalization of `multiCollateral` → `crossCollateral` at the config-input level. Do not flag the absence of legacy-input regression tests in `typescript/cli/src/tests/ethereum/warp/warp-cross-collateral.e2e-test.ts` or related test files as a missing coverage gap.

Learnt from: nambrot
Repo: hyperlane-xyz/hyperlane-monorepo PR: 8306
File: solidity/multicollateral/contracts/TokenBridgeOft.sol:222-228
Timestamp: 2026-03-10T21:10:47.566Z
Learning: In `solidity/multicollateral/contracts/TokenBridgeOft.sol`, `addDomain` does NOT reject `_hyperlaneDomain == 0`. Domain 0 is a valid Hyperlane domain at the protocol level (unused today but not invalid). The "zero as sentinel" concern does not apply here because `hyperlaneDomainToLzEid` reverts with `LzEidNotConfigured` (via `_getLzEid()`) rather than returning 0. Do not flag the absence of a `_hyperlaneDomain != 0` guard in `addDomain`.

Learnt from: nambrot
Repo: hyperlane-xyz/hyperlane-monorepo PR: 8327
File: typescript/sdk/src/token/EvmWarpRouteReader.ts:0-0
Timestamp: 2026-03-11T23:01:14.339Z
Learning: In `typescript/sdk/src/token/EvmWarpRouteReader.ts`, the `getCrossCollateralDomainsCompat` and `getCrossCollateralRoutersCompat` compat helpers were intentionally removed. Legacy ABI support (for old `getEnrolledDomains`/`getEnrolledRouters` method names) was dropped as part of the MultiCollateral→CrossCollateral rename in PR `#8327`. The reader now calls only the renamed CrossCollateral ABI methods (`getCrossCollateralDomains`, `getCrossCollateralRouters`) directly.

Learnt from: nambrot
Repo: hyperlane-xyz/hyperlane-monorepo PR: 8327
File: typescript/cli/src/commands/warp.ts:195-196
Timestamp: 2026-03-11T23:01:15.601Z
Learning: In `typescript/sdk/src/token/types.ts`, the `CrossCollateralTokenConfigSchema` schema uses `crossCollateralRouters` (not `enrolledRouters`) as the field name, consistent with the CrossCollateral rename. The CLI help text in `typescript/cli/src/commands/warp.ts` also references `crossCollateralRouters`. Both schema and help text are now aligned.

Learnt from: CR
Repo: hyperlane-xyz/hyperlane-monorepo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T18:29:47.023Z
Learning: Applies to **/*.sol : Document rebasing token, fee-on-transfer, and ERC-777 support status

Learnt from: CR
Repo: hyperlane-xyz/hyperlane-monorepo PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-04T14:22:31.692Z
Learning: Applies to solidity/**/*.sol : Document rebasing token, fee-on-transfer, and ERC-777 support status in Solidity contracts

@nambrot nambrot added this pull request to the merge queue Mar 12, 2026
Merged via the queue into main with commit a4a74d8 Mar 13, 2026
108 checks passed
@nambrot nambrot deleted the nam/token-bridge-oft branch March 13, 2026 00:06
@github-project-automation github-project-automation Bot moved this from In Review to Done in Hyperlane Tasks Mar 13, 2026
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