Skip to content

feat(rebalancer-sim): Add MockActionTracker for ProductionRebalancer inflight tracking#8033

Merged
nambrot merged 15 commits into
mainfrom
nam/mock-inflight-context-adapter
Feb 6, 2026
Merged

feat(rebalancer-sim): Add MockActionTracker for ProductionRebalancer inflight tracking#8033
nambrot merged 15 commits into
mainfrom
nam/mock-inflight-context-adapter

Conversation

@nambrot
Copy link
Copy Markdown
Contributor

@nambrot nambrot commented Feb 4, 2026

Summary

  • Adds MockActionTracker implementing IActionTracker interface for simulation testing
  • Adds optional actionTracker config to RebalancerService for test injection
  • Exports IActionTracker and related types from rebalancer package
  • MockInfrastructureController wires Dispatch events to MockActionTracker for inflight tracking
  • MockValueTransferBridge now extends Router so bridge transfers dispatch through Mailbox

Built on top of #8060 which unified BridgeMockController + MessageTracker into a single MockInfrastructureController using HyperlaneCore.

What This Enables

inflight-guard test: ProductionRebalancer now correctly uses fewer rebalances compared to SimpleRebalancer because it tracks pending transfers and doesn't over-correct. Typically achieves >50% reduction in unnecessary rebalances.

blocked-user-transfer test: ProductionRebalancer achieves 100% completion (vs 0% for SimpleRebalancer) by proactively adding collateral when it sees pending user transfers that would otherwise be blocked.

Key Implementation Details

  • MockActionTracker tracks transfers and rebalance intents/actions in memory
  • MockInfrastructureController auto-classifies Dispatch events (warp vs bridge) and updates the tracker
  • MockValueTransferBridge extends Router so _Router_dispatch() emits real Mailbox Dispatch events, enabling black-box observation by the controller
  • RebalancerService accepts an optional actionTracker to skip ExplorerClient-based tracking

Test plan

  • pnpm -C typescript/rebalancer-sim test --grep "inflight-guard" passes
  • pnpm -C typescript/rebalancer-sim test --grep "blocked-user-transfer" passes
  • Full simulation test suite passes
  • pnpm -C typescript/cli build passes (e2e tests updated for new MockValueTransferBridge constructor)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Unified mock infrastructure for simulated cross-chain deliveries with KPI-driven tracking and optional inflight action inspection.
    • In-memory mock action tracker for deterministic, test-friendly inspection of inflight intents/actions.
    • Rebalancer accepts an external action-tracking hook for observability and control.
  • Removed

    • Legacy message- and bridge-tracking modules replaced by the new unified controller.
    • Harness reset API removed; public exports updated accordingly.
  • Tests

    • Tests updated for dynamic local RPC ports, KPI-driven assertions, adjusted timings, and updated bridge init flow.
  • Contracts

    • Test bridge now requires mailbox/router initialization and supports remote router enrollment.

@github-project-automation github-project-automation Bot moved this to In Review in Hyperlane Tasks Feb 4, 2026
@paulbalaji paulbalaji changed the title feat(rebalancer-sim): Add mock inflight context adapter for ProductionRebalancer feat(rebalancer-sim): add mock inflight context adapter for ProductionRebalancer Feb 4, 2026
@paulbalaji paulbalaji changed the title feat(rebalancer-sim): add mock inflight context adapter for ProductionRebalancer feat(rebalancer-sim): add MockInflightContextAdapter for ProductionRebalancer Feb 4, 2026
Copy link
Copy Markdown
Collaborator

@paulbalaji paulbalaji left a comment

Choose a reason for hiding this comment

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

Correctness concern: Pending item removal by origin+destination only

The MockInflightContextAdapter removes pending items by matching origin + destination only:

removePendingTransfer(origin: string, destination: string): boolean {
  const idx = this.pendingRebalances.findIndex(
    (r) => r.origin === origin && r.destination === destination,
  );
  // removes first match...
}

Problem: If there are multiple concurrent transfers on the same route (e.g., two user transfers chain1→chain2), removing one will remove the wrong item (first match, not the specific transfer that completed). This corrupts inflight totals → wrong reserve math → incorrect strategy decisions.

Suggestion: Add transfer ID to callbacks and match on ID when removing:

// types.ts
interface InflightContextCallbacks {
  onTransferInitiated: (id: string, origin: string, destination: string, amount: bigint) => void;
  onTransferDelivered: (id: string) => void;  // just need id to remove
  // ...
}

// MockInflightContextAdapter.ts
interface PendingItem extends Route {
  id: string;
}

removePendingTransfer(id: string): boolean {
  const idx = this.pendingTransfers.findIndex((r) => r.id === id);
  // ...
}

The SimulationEngine already has transfer IDs available (transfer.id, message.transferId) - just need to wire them through.


Lower priority: The hard equality assertions (expect(...).to.equal(0), expect(...).to.equal(1.0)) could be flaky with timing variance. Consider tolerances like lessThanOrEqual(0.05) for completion rate checks. But this is less critical than the ID issue.

Base automatically changed from nam/blocked-user-transfer-test to main February 4, 2026 11:01
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

Replaces legacy bridge/message trackers with a unified MockInfrastructureController and MockActionTracker; SimulationEngine adds private buildHyperlaneCore(); RebalancerService accepts an optional IActionTracker; tests and deployments updated for mailbox-aware bridges and dynamic Anvil RPC ports. (≤50 words)

Changes

Cohort / File(s) Summary
Simulation core
typescript/rebalancer-sim/src/SimulationEngine.ts, typescript/rebalancer-sim/src/SimulationDeployment.ts, typescript/rebalancer-sim/src/index.ts
Added private buildHyperlaneCore(); switched to MockInfrastructureController; deployment now supplies mailbox to bridges, calls initialize and enrollRemoteRouters; updated exports (removed old controllers, exported new controller).
Mock infra & trackers
typescript/rebalancer-sim/src/MockInfrastructureController.ts, typescript/rebalancer-sim/src/runners/MockActionTracker.ts
Added MockInfrastructureController (dispatch listeners, delivery queue, retries, KPI & action-tracker hooks). Added in-memory MockActionTracker implementing IActionTracker (transfers/intents/actions lifecycle and helpers).
Removed modules
typescript/rebalancer-sim/src/BridgeMockController.ts, typescript/rebalancer-sim/src/MessageTracker.ts
Deleted legacy BridgeMockController and MessageTracker and their event-driven delivery/message-tracking logic.
Rebalancer integration
typescript/rebalancer/src/core/RebalancerService.ts, typescript/rebalancer/src/index.ts
RebalancerServiceConfig gains optional actionTracker?: IActionTracker; service can use provided tracker (creates InflightContextAdapter). Added tracking-related type exports.
Runner & types
typescript/rebalancer-sim/src/runners/ProductionRebalancerRunner.ts, typescript/rebalancer-sim/src/types.ts
Production runner now holds/exposes MockActionTracker via getActionTracker() and resets it across lifecycle. IRebalancerRunner adds optional `getActionTracker?(): MockActionTracker
Tests & harness
typescript/rebalancer-sim/test/integration/full-simulation.test.ts, typescript/rebalancer-sim/test/integration/harness-setup.test.ts, typescript/rebalancer-sim/test/utils/anvil.ts
Switched to dynamic Anvil port mapping with getAnvilRpcUrl(); provider created in beforeEach; integration tests converted to KPI-driven assertions and timing adjusted for slower bridge.
CLI tests / solidity changes
typescript/cli/src/tests/ethereum/warp/warp-rebalancer.e2e-test.ts, solidity/contracts/mock/MockValueTransferBridge.sol
Bridge contract now inherits Router, requires mailbox in constructor and adds initialize; tests updated to deploy with mailbox, call initialize, and enroll remote routers post-deploy.
Harness API change
typescript/rebalancer-sim/src/RebalancerSimulationHarness.ts
Removed public reset() method from the harness API.
Scenario config
typescript/rebalancer-sim/scenarios/inflight-guard.json
Adjusted scenario timings (longer duration/deliveryDelay, removed some keepalive transfers) to match slower bridge behavior.

Sequence Diagram(s)

sequenceDiagram
    participant Engine as SimulationEngine
    participant Core as HyperlaneCore
    participant Controller as MockInfrastructureController
    participant Runner as ProductionRebalancerRunner
    participant Tracker as MockActionTracker
    participant Rebalancer as RebalancerService

    Note over Engine,Core: Initialization
    Engine->>Core: buildHyperlaneCore()
    Engine->>Controller: new(core, domains, config, userDelay, kpiCollector, actionTracker?)
    Engine->>Controller: start()
    Engine->>Runner: start()
    Runner->>Tracker: initialize() (if provided)
    Runner->>Rebalancer: start(config with actionTracker?)

    Note over Controller,Engine: Incoming Dispatch
    Engine->>Controller: mailbox Dispatch event
    Controller->>Controller: classify message (user/bridge), compute messageId, decode amount
    Controller->>Controller: enqueue delivery with delay

    Note over Controller,Tracker: Delivery processing
    Controller->>Controller: process ready message (callStatic then process tx)
    Controller->>Tracker: record KPI / create/complete actions/intents (if tracker present)
    Controller->>Rebalancer: (KPI updates / delivery notifications)
    Tracker-->>Rebalancer: intent/action records reflected back
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

In the swamp of code we shuffled things around,
Old bridges gone, a single controller found.
Mailboxes hum, trackers keep the score,
Tests spin up ports and dance across the moor.
Nice and tidy now — don’t wake the ogre’s snore.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding MockActionTracker for ProductionRebalancer inflight tracking in the rebalancer-sim package.
Description check ✅ Passed The description covers summary, drive-by changes, and includes testing notes, but lacks explicit sections for related issues and backward compatibility assessment as per template.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch nam/mock-inflight-context-adapter

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

Caution

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

⚠️ Outside diff range comments (1)
typescript/rebalancer-sim/src/SimulationEngine.ts (1)

391-415: ⚠️ Potential issue | 🟡 Minor

Clear inflight pending transfers on timeout.
When the wait times out, MessageTracker is cleared but inflight context still thinks transfers are pending. That can skew rebalancer decisions in the tail end of the run.

Suggested fix
         for (const msg of pending) {
           logger.warn(
             {
               messageId: msg.id,
               origin: msg.origin,
               destination: msg.destination,
               status: msg.status,
               attempts: msg.attempts,
               error: msg.lastError || 'timeout',
             },
             'Marking pending message as failed',
           );
           // Record as failed in KPI collector
           this.kpiCollector?.recordTransferFailed(msg.transferId);
+          // Also clear inflight pending state
+          this.inflightCallbacks?.onTransferDelivered(msg.transferId);
         }
🤖 Fix all issues with AI agents
In `@typescript/rebalancer-sim/src/runners/ProductionRebalancerRunner.ts`:
- Around line 308-343: The inflight callbacks use non-null assertions on
this.mockAdapter and can throw if stop() clears the adapter; update
getInflightCallbacks so each callback first checks that the adapter still exists
(e.g., read a local const adapter = this.mockAdapter and return early/no-op if
undefined) instead of calling this.mockAdapter! directly; apply this change for
onTransferInitiated, onTransferDelivered, onRebalanceInitiated and
onRebalanceDelivered so they safely no-op when the adapter has been cleared.

In `@typescript/rebalancer-sim/test/integration/inflight-guard.test.ts`:
- Around line 102-116: The test currently uses conditional guards
(productionResult and simpleResult) which can allow missing rebalancer results
to silently pass; update the test to explicitly fail when results are absent by
asserting presence of productionResult and simpleResult before using their
properties (e.g., assert/expect that productionResult exists and, when
simpleResult is required for comparison, that simpleResult exists) so the test
fails loudly if a rebalancer is missing; locate the checks around
productionResult.kpis and simpleResult.kpis in inflight-guard.test.ts and add
presence assertions for productionResult and simpleResult immediately before the
existing kpi assertions.
🧹 Nitpick comments (3)
typescript/rebalancer-sim/src/SimulationEngine.ts (1)

97-149: Wrap inflight callback calls with error isolation.
These are externalized callbacks; a throw here could derail the sim loop. A tiny helper keeps it safe and consistent.

Suggested refactor
+  private notifyInflight<K extends keyof InflightContextCallbacks>(
+    key: K,
+    ...args: Parameters<InflightContextCallbacks[K]>
+  ): void {
+    if (!this.inflightCallbacks) return;
+    try {
+      this.inflightCallbacks[key](...args);
+    } catch (error: unknown) {
+      logger.error({ error, key }, 'Inflight callback failed');
+      throw error;
+    }
+  }
-        this.inflightCallbacks?.onTransferDelivered(message.transferId);
+        this.notifyInflight('onTransferDelivered', message.transferId);
...
-        this.inflightCallbacks?.onRebalanceInitiated(
+        this.notifyInflight(
+          'onRebalanceInitiated',
           event.transfer.id,
           event.transfer.origin,
           event.transfer.destination,
           event.transfer.amount,
         );
...
-        this.inflightCallbacks?.onRebalanceDelivered(event.transfer.id);
+        this.notifyInflight('onRebalanceDelivered', event.transfer.id);
...
-        this.inflightCallbacks?.onTransferInitiated(
+        this.notifyInflight(
+          'onTransferInitiated',
           transfer.id,
           transfer.origin,
           transfer.destination,
           transfer.amount,
         );

As per coding guidelines: “Use try/catch for external system calls; log and rethrow; don’t swallow errors.”

Also applies to: 162-166, 322-328

typescript/rebalancer/src/core/RebalancerService.ts (2)

443-446: Use assert for the rebalancer precondition.
The warn+return can hide misconfiguration. Better to fail fast.

Suggested fix
-    if (!this.rebalancer) {
-      this.logger.warn('Rebalancer not available, skipping');
-      return;
-    }
+    assert(this.rebalancer, 'Rebalancer not available');

As per coding guidelines: “Use assert() for preconditions/invariants; fail fast with clear messages.”


617-620: Don’t swallow rebalance errors in the fallback path.
catch (error: any) both violates the typing rule and hides failures. Prefer unknown, log, and rethrow so callers can decide.

Suggested fix
-    } catch (error: any) {
+    } catch (error: unknown) {
       this.metrics?.recordRebalancerFailure();
       this.logger.error({ error }, 'Error while rebalancing');
+      throw error;
     }

As per coding guidelines: “Prefer catch (unknown) and narrow via type guards; avoid catch (any) in new code” and “Use try/catch for external system calls; log and rethrow; don’t swallow errors.”

Comment thread typescript/rebalancer-sim/src/runners/ProductionRebalancerRunner.ts Outdated
Comment thread typescript/rebalancer-sim/test/integration/inflight-guard.test.ts Outdated
@nambrot nambrot enabled auto-merge February 4, 2026 19:07
nambrot and others added 2 commits February 4, 2026 14:08
…nRebalancer

Adds IInflightContextAdapter interface to rebalancer package and MockInflightContextAdapter
to rebalancer-sim, enabling simulation of inflight context tracking without requiring a real
ActionTracker/ExplorerClient.

Changes:
- Add IInflightContextAdapter interface to tracking module
- Add optional inflightContextAdapter to RebalancerServiceConfig
- Add executeWithoutTracking() fallback when ActionTracker unavailable
- Create MockInflightContextAdapter maintaining pending rebalances/transfers
- Wire SimulationEngine events to mock adapter callbacks
- Update tests with assertions for inflight-guard and blocked-user-transfer scenarios

Test results:
- inflight-guard: ProductionRebalancer uses 1 rebalance vs SimpleRebalancer's 6 (83% reduction)
- blocked-user-transfer: ProductionRebalancer achieves 100% completion vs SimpleRebalancer's 0%

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…f origin/destination

Addresses PR review feedback - matching pending items by origin+destination
could remove the wrong item when multiple concurrent transfers exist on the
same route. Now uses unique transfer IDs for correct removal.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@nambrot nambrot force-pushed the nam/mock-inflight-context-adapter branch from f3f71b4 to 96c8128 Compare February 4, 2026 19:13
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

🤖 Fix all issues with AI agents
In `@typescript/rebalancer/src/core/RebalancerService.ts`:
- Around line 561-620: In executeWithoutTracking, don't swallow errors from the
external call to this.rebalancer.rebalance: update the catch from catch (error:
any) to catch (error: unknown), keep the metrics?.recordRebalancerFailure() and
logger.error(...) but then rethrow the caught error (throw error) so callers can
handle it; this change should be applied inside the executeWithoutTracking
method that wraps the call to this.rebalancer.rebalance.
🧹 Nitpick comments (1)
typescript/rebalancer-sim/test/integration/full-simulation.test.ts (1)

255-270: Tighten the block comment. It’s a bit long; a short summary will do.

Possible trim
-  /**
-   * Blocked User Transfer Test
-   *
-   * Tests that the ProductionRebalancer proactively adds collateral when
-   * user transfers are pending but blocked due to insufficient collateral.
-   *
-   * Scenario: 130 total tokens split 90/40 between chain1/chain2.
-   * User initiates 50 token transfer from chain1 → chain2.
-   * chain2 only has 40 tokens but needs 50 to pay out.
-   *
-   * SimpleRebalancer: Only sees on-chain balances, doesn't know about pending
-   * transfer, weights appear within tolerance → no action → transfer stuck
-   *
-   * ProductionRebalancer: Mock adapter tracks pending transfer, strategy
-   * reserves collateral for it, detects deficit → rebalances → transfer succeeds
-   */
+  /**
+   * Blocked transfer: pending user transfer needs collateral.
+   * Scenario: 90/40 split, 50 transfer chain1→chain2; Simple=stuck, Production=rebalance.
+   */
As per coding guidelines: “Be extremely concise in code comments and documentation.”

Comment thread typescript/rebalancer/src/core/RebalancerService.ts Outdated
@nambrot nambrot disabled auto-merge February 4, 2026 19:42
@nambrot
Copy link
Copy Markdown
Contributor Author

nambrot commented Feb 4, 2026

Going to change the mocking abstraction, dont love it

@nambrot nambrot closed this Feb 4, 2026
@github-project-automation github-project-automation Bot moved this from In Review to Done in Hyperlane Tasks Feb 4, 2026
@nambrot nambrot reopened this Feb 4, 2026
@github-project-automation github-project-automation Bot moved this from Done to Sprint in Hyperlane Tasks Feb 4, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 4, 2026

Codecov Report

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

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #8033   +/-   ##
=======================================
  Coverage   77.02%   77.02%           
=======================================
  Files         117      117           
  Lines        2651     2651           
  Branches      244      244           
=======================================
  Hits         2042     2042           
  Misses        593      593           
  Partials       16       16           
Flag Coverage Δ
solidity 77.02% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
core 87.80% <ø> (ø)
hooks 71.86% <ø> (ø)
isms 81.10% <ø> (ø)
token 86.67% <ø> (ø)
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.

…htContextAdapter

The simulation now mocks at the ActionTracker level rather than the
InflightContextAdapter level. This provides cleaner integration with
RebalancerService and properly demonstrates inflight tracking benefits.

Key changes:
- Add MockActionTracker implementing IActionTracker interface
- Remove MockInflightContextAdapter (replaced by MockActionTracker)
- Update RebalancerService to accept optional actionTracker config
- Handle MockValueTransferBridge not emitting Dispatch events:
  - failRebalanceIntent keeps intent as in_progress (bridge succeeded)
  - createActionForPendingIntent creates actions when bridge events fire
  - completeRebalanceByRoute marks actions complete on delivery

Results demonstrate inflight tracking benefits:
- inflight-guard: ProductionRebalancer 5 rebalances vs SimpleRebalancer 18
- blocked-user-transfer: ProductionRebalancer 100% completion vs 0%

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@nambrot nambrot changed the title feat(rebalancer-sim): add MockInflightContextAdapter for ProductionRebalancer feat(rebalancer-sim): Add MockActionTracker for ProductionRebalancer inflight tracking Feb 4, 2026
- inflight-guard: Assert ProductionRebalancer uses >50% fewer rebalances
  than SimpleRebalancer (currently achieves ~75% reduction)
- Update test description to reflect working inflight tracking
- Update blocked-user-transfer comment to use correct terminology

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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

🤖 Fix all issues with AI agents
In `@typescript/rebalancer/src/core/RebalancerService.ts`:
- Around line 189-205: The injected ActionTracker passed in via
this.config.actionTracker may not be initialized; ensure you call and await its
initialize() before using it (or check/assert its initialized state) in the
branch that sets this.actionTracker and this.inflightContextAdapter;
specifically, after assigning this.actionTracker in RebalancerService (the
branch that uses this.config.actionTracker) call await
this.actionTracker.initialize() (or verify an isInitialized method) and only
then proceed to create InflightContextAdapter or log 'Using externally provided
ActionTracker'.

Comment thread typescript/rebalancer/src/core/RebalancerService.ts
…er (#8060)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Feb 6, 2026

Claude finished @nambrot's task in 5m 19s —— View job


Trail of Bits Security Review

  • Invoke Trail of Bits security review skill
  • Analyze Solidity contract changes
  • Review TypeScript changes for security concerns
  • Post findings

Scope

  • Solidity: MockValueTransferBridge.sol (mock contract, now extends Router)
  • TypeScript: MockInfrastructureController.ts, MockActionTracker.ts, RebalancerService.ts

Overall Assessment: LOW risk

All changes are in mock/simulation code. The Solidity contract is test-only and properly guarded by Router's onlyMailbox + router enrollment checks. No production contract attack surface is introduced.


Findings

ID Severity Finding File Line
ToB-1 LOW collateral should be immutable MockValueTransferBridge.sol 12
ToB-2 INFO block.chainid truncated to uint32 in event MockValueTransferBridge.sol 62
ToB-3 INFO _handle mints via unprotected ERC20Test.mintTo MockValueTransferBridge.sol 87
ToB-4 LOW Hardcoded body offset 77 is protocol-coupled MockInfrastructureController.ts 152
ToB-5 LOW Exact amount matching in MockActionTracker is brittle MockActionTracker.ts 276+
ToB-6 LOW clear() resets idCounter — potential ID collision MockActionTracker.ts 260
ToB-7 INFO Externally injected actionTracker now properly initialized RebalancerService.ts 197

ToB-1 | LOW — collateral should be immutable

File: solidity/contracts/mock/MockValueTransferBridge.sol:12

collateral is only set in the constructor and never mutated. Per Solcurity guidelines, constructor-set values should use immutable to save a storage slot read (~2100 gas) on every access and prevent accidental mutation.

-    address public collateral;
+    address public immutable collateral;

Since this contract extends Router (which uses OwnableUpgradeable and initializer), confirm immutable doesn't conflict with proxy patterns if this mock is ever deployed behind a proxy. In the current direct-deployment test usage, immutable is safe.


ToB-2 | INFO — block.chainid truncated to uint32

File: solidity/contracts/mock/MockValueTransferBridge.sol:62

block.chainid is uint256; casting to uint32 silently truncates upper bits. For Anvil/Hardhat chains this is fine, but block.chainid also doesn't equal the Hyperlane domain ID in production. Since this is a mock contract, just noting for awareness — if anyone reuses this pattern for real bridges, the event would log a wrong origin.


ToB-3 | INFO — _handle mints via unprotected ERC20Test.mintTo

File: solidity/contracts/mock/MockValueTransferBridge.sol:87

ERC20Test.mintTo() is completely unprotected (public with no modifiers). The _handle call chain is properly guarded by Router.handle()onlyMailbox modifier + router enrollment check, so this is safe within the simulation context.

However, ERC20Test(collateral).mintTo() is callable by anyone directly — any external account can mint unlimited tokens. This is expected for test contracts but would be critical in production. The mock should never be deployed outside test environments.

Also note: there's no ReceivedTransferRemote event emitted here (unlike TokenRouter._handle), which means the MockInfrastructureController relies solely on the Dispatch event for tracking rather than a delivery event.


ToB-4 | LOW — Hardcoded body offset 77 is protocol-coupled

File: typescript/rebalancer-sim/src/MockInfrastructureController.ts:152

The magic number 77 represents the Hyperlane message header size before the TokenRouter body. If the message envelope format changes, this decode will silently produce wrong amounts. The BigInt(1e18) scale factor (line 163) is similarly coupled to warp token decimal configuration.

Suggestion — extract as named constants:

const HYPERLANE_MESSAGE_BODY_OFFSET = 77;
const WARP_TOKEN_SCALE = BigInt(1e18);

When ABI decode fails, amount stays at 0n. The downstream msg.amount > 0n guard (line 266) means zero-amount entries are silently skipped by the action tracker, which could mask decode bugs. Consider logging at error level for bridge-transfer decode failures.


ToB-5 | LOW — Exact amount matching in MockActionTracker is brittle

File: typescript/rebalancer-sim/src/runners/MockActionTracker.ts:276-417

createActionForPendingIntent (line 289), completeRebalanceByRoute (line 358), and failRebalanceByRoute (line 417) all use strict equality i.amount === amount. If bridge transfers ever have amounts that differ due to fees, slippage, or rounding, these lookups will silently fail.

In the current simulation where amounts are deterministic this works. Consider documenting the exact-match assumption.


ToB-6 | LOW — clear() resets idCounter — potential ID collision

File: typescript/rebalancer-sim/src/runners/MockActionTracker.ts:260

Resetting idCounter to 0 means newly generated IDs after clear() could collide with IDs from a previous run if any async callbacks still hold references. The current usage (called between test runs after controller.stop()) appears safe.


ToB-7 | INFO — Externally injected actionTracker properly initialized

File: typescript/rebalancer/src/core/RebalancerService.ts:197

The await this.actionTracker.initialize() call correctly ensures consistent initialization regardless of tracker source. For MockActionTracker this is a no-op, but a real IActionTracker implementation (e.g., DB-backed) needs this to sync state. Correctly fixed.


Invariant Suggestions for Property-Based Testing

If fuzz testing is added for the simulation:

  1. Collateral accounting: After any transferRemote + _handle round-trip, the total collateral across all domains should remain constant
  2. Action tracker consistency: intent.fulfilledAmount should never exceed intent.amount. After completeRebalanceByRoute, the sum of all action amounts for an intent should equal intent.fulfilledAmount
  3. Pending message monotonicity: pendingMessages.length should decrease (or stay zero) after each processLoop iteration unless new Dispatch events arrive concurrently
  4. ID uniqueness: No two intents or actions should share the same ID within a single simulation run

Observations Outside This PR

  • ERC20Test.sol (mintTo, mint, burnFrom) are all completely unprotected. Expected for test contracts but should never be deployed to production
  • Router.handle() (Router.sol:101) validates _router == _sender, which is correct. However, routers() returns bytes32(0) for unenrolled domains rather than reverting — the _mustHaveRemoteRouter path correctly reverts, but direct routers() callers should be aware of the zero-return behavior

@nambrot nambrot requested a review from paulbalaji February 6, 2026 14:54
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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: 6

🤖 Fix all issues with AI agents
In `@solidity/contracts/mock/MockValueTransferBridge.sol`:
- Around line 25-31: The MockValueTransferBridge instances are left
uninitialized in some tests; after deploying each MockValueTransferBridge call
its initialize(...) method (which delegates to _MailboxClient_initialize) with
the correct hook, ism and owner addresses used elsewhere in tests so the mutable
state is configured; update the warp-rebalancer.e2e-test deployment logic to
invoke bridge.initialize(hookAddress, ismAddress, ownerAddress) for every
deployed MockValueTransferBridge (same pattern as SimulationDeployment.ts where
Router requires initialization).

In `@typescript/rebalancer-sim/src/MockInfrastructureController.ts`:
- Around line 325-339: The timeout handling in the pendingMessages loop
currently calls this.kpiCollector.recordRebalanceFailed(msg.messageId) but then
marks the action as completed by calling
this.actionTracker.completeRebalanceByRoute(...), which incorrectly increments
fulfillment; change this to mark the action as failed instead: add/raise use of
a fail path on MockActionTracker (e.g., implement a failRebalanceByRoute in
MockActionTracker or call an existing failRebalanceAction action), and replace
the completeRebalanceByRoute(...) call in the branch handling msg.type ===
'bridge-transfer' with the new fail method so the KPI and action tracker
semantics remain consistent (refer to pendingMessages loop,
recordRebalanceFailed, completeRebalanceByRoute, MockActionTracker,
failRebalanceByRoute / failRebalanceAction).
- Around line 268-277: The conditional in MockInfrastructureController that
guards calling actionTracker.completeRebalanceByRoute incorrectly treats a
bigint 0n as falsy; change the if-check around msg.amount in the bridge-transfer
branch to an explicit existence check (e.g., msg.amount !== undefined &&
msg.amount !== null or typeof msg.amount === 'bigint') so zero-amount rebalances
are still forwarded to actionTracker.completeRebalanceByRoute (referencing the
bridge-transfer branch, this.actionTracker, and completeRebalanceByRoute).
- Around line 246-252: The catch block around mailbox.callStatic.process in
MockInfrastructureController silently swallows errors; update it to catch the
error object (e.g., catch (err)) and log the failure before continuing — include
the error and relevant message context (msg.id or msg.message and
attempts/deliveryTime) using the controller's logger (this.logger.error) or
console.error if no logger exists, then keep the existing retry behavior
(msg.attempts++, msg.deliveryTime = now + 200, continue).

In `@typescript/rebalancer-sim/src/runners/MockActionTracker.ts`:
- Around line 260-275: completeRebalanceAction currently increments
intent.fulfilledAmount but never marks the parent intent complete; update
completeRebalanceAction (in MockActionTracker) to, after increasing
intent.fulfilledAmount and setting intent.updatedAt, check if
intent.fulfilledAmount >= intent.amount and if so set intent.status = 'complete'
and update intent.updatedAt (and optionally log the transition) so behavior
matches completeRebalanceByRoute's completion logic.

In `@typescript/rebalancer-sim/test/integration/full-simulation.test.ts`:
- Around line 275-282: Guard against division-by-zero when computing
reductionRatio: check simpleResult.kpis.totalRebalances (and optionally
productionResult.kpis.totalRebalances) before dividing; if
simpleResult.kpis.totalRebalances === 0, fail the test with a clear
assertion/message (e.g., assert.fail or expect(...).to.equal(...) with a
descriptive message) explaining that SimpleRebalancer produced zero rebalances,
otherwise compute reductionRatio as productionResult.kpis.totalRebalances /
simpleResult.kpis.totalRebalances and run the existing
expect(reductionRatio).to.be.lessThan(0.5) assertion. Ensure you reference the
same symbols used in the diff: simpleResult.kpis.totalRebalances,
productionResult.kpis.totalRebalances, and reductionRatio.
🧹 Nitpick comments (11)
solidity/contracts/mock/MockValueTransferBridge.sol (1)

21-23: collateral can be immutable since it's only assigned in the constructor.

It's set once and never touched again — like an ogre's favorite swamp, it ain't movin'. Marking it immutable saves a storage slot read on every access.

🧅 Proposed fix
-    address public collateral;
+    address public immutable collateral;
typescript/rebalancer-sim/src/types.ts (1)

10-11: Concrete MockActionTracker return type on a general interface — worth a ponder.

The IRebalancerRunner interface lives in the shared types file but now returns a concrete simulation class. This means any non-simulation implementer of IRebalancerRunner has to know about MockActionTracker. Since IActionTracker is already exported from the package, returning that interface (or a broader simulation-specific interface covering the extra helpers) would keep this swamp tidier.

That said, I see you've already noted you plan to rework the mocking abstraction, so this might sort itself out like onion layers. Just flagging it so it doesn't get lost in the shuffle.

Also applies to: 418-424

typescript/rebalancer-sim/src/runners/ProductionRebalancerRunner.ts (1)

296-302: Return type is narrower than the interface contract — fine, but worth noting.

getActionTracker() here always returns MockActionTracker (non-optional), while IRebalancerRunner declares the return as MockActionTracker | undefined. TypeScript allows this narrowing. Just make sure callers obtained through the interface still handle undefined.

typescript/rebalancer-sim/test/integration/full-simulation.test.ts (1)

253-264: Soft guards (if (productionResult)) mean the test passes vacuously when a rebalancer is filtered out.

This is intentional for REBALANCERS env-var flexibility, but it's worth knowing that running with a single rebalancer type silently skips half the assertions. No action needed unless you want a stricter stance.

typescript/rebalancer-sim/src/SimulationEngine.ts (3)

137-149: Bare catch {} blocks silently swallow errors during cleanup.

The coding guidelines are pretty clear about never swallowing errors silently. Even in a finally block, a quick debug log helps when something's rotting in the swamp and you're trying to figure out where the smell's coming from.

Suggested fix
     try {
       await rebalancer.stop();
-    } catch {
-      // Ignore stop errors
+    } catch (error: unknown) {
+      logger.debug({ error }, 'Rebalancer stop failed during cleanup');
     }

     if (controller) {
       try {
         await controller.stop();
-      } catch {
-        // Ignore stop errors
+      } catch (error: unknown) {
+        logger.debug({ error }, 'Controller stop failed during cleanup');
       }
     }

As per coding guidelines: "Never swallow errors silently; always log or re-throw caught exceptions" and "When catching errors, prefer catch (error: unknown) with type guards over catch (e: any) in new code."


230-245: catch (error) should be catch (error: unknown) per guidelines.

Small thing, but the coding guidelines prefer the explicit unknown type annotation to encourage proper narrowing. You're already doing the narrowing correctly on line 234, so it's just the declaration.

Suggested fix
-    } catch (error) {
+    } catch (error: unknown) {

As per coding guidelines: "When catching errors, prefer catch (error: unknown) with type guards over catch (e: any) in new code."


278-309: buildHyperlaneCore() — clean helper, duplicates chain metadata construction seen in ProductionRebalancerRunner.

This method and the one in ProductionRebalancerRunner.ts (lines 144-165) both construct very similar chain metadata objects with chainId: 31337, protocol: ProtocolType.Ethereum, same RPC URL, same native token config. Not a blocker — they serve slightly different purposes (different signers, different polling needs) — but if the metadata shape drifts between the two, debugging gets murky. Something to keep an eye on for the mocking abstraction rework.

typescript/rebalancer-sim/src/MockInfrastructureController.ts (3)

42-58: Consider guarding signer with assert() instead of !.

The signer!: ethers.Signer non-null assertion on line 44 means any accidental access before start() silently produces undefined at runtime rather than a clear error. Since the coding guidelines prefer assert() for preconditions, you could initialize it as private signer: ethers.Signer | undefined and assert at usage sites, or assert at the top of doProcessReadyMessages. Not a swamp-drainer, but it keeps things honest.

As per coding guidelines, "Use assert() for validating preconditions and invariants instead of throwing errors directly".


146-167: Hardcoded body offset (77) and scale factor (1e18) are brittle magic numbers.

These values are tightly coupled to the current TokenRouter message format and warp token decimal configuration. If the message envelope or token decimals change, the decode silently produces wrong amounts. This is tolerable for a controlled simulation, but extracting these as named constants (or deriving from config) would make the coupling explicit and easier to update.

Also, the amount silently stays 0n on decode failure (line 152). Worth a log-level bump from warn to error if the message must carry an amount for correct KPI tracking — a zero-amount rebalance could quietly skew metrics.

Extract constants
+const MESSAGE_BODY_OFFSET = 77;
+const WARP_TOKEN_SCALE = BigInt(1e18);
+
 // inside onDispatch:
-    const bodyOffset = 77;
-    const body = '0x' + message.slice(2 + bodyOffset * 2);
+    const body = '0x' + message.slice(2 + MESSAGE_BODY_OFFSET * 2);
     ...
-        type === 'user-transfer' ? scaledAmount / BigInt(1e18) : scaledAmount;
+        type === 'user-transfer' ? scaledAmount / WARP_TOKEN_SCALE : scaledAmount;

294-305: removeAllListeners() nukes all listeners, not just ours.

mailbox.removeAllListeners() at line 303 removes every listener on the contract, including any that other components may have registered. In a simulation this is probably fine since the controller owns the mailbox lifecycle, but if the scope ever widens, you'd want to track and remove only your own listeners. Just somethin' to keep in the back of your mind, like an ogre keeps... layers.

typescript/rebalancer-sim/src/runners/MockActionTracker.ts (1)

14-58: Consider exporting and importing the tracking types from @hyperlane-xyz/rebalancer instead of redefining them locally.

The Transfer, RebalanceIntent, and RebalanceAction interfaces already exist in typescript/rebalancer/src/tracking/types.ts but aren't currently part of the package's public API exports. Right now you're rolling your own versions here, which creates a maintenance burden if the source types ever shift. If they change upstream, these local copies won't know about it.

Best move would be to export these types from the rebalancer package's index, then pull 'em in here. Keeps things in one place rather than scattered around like layers of an onion. If exporting them breaks something, you could always import directly from the internal path (@hyperlane-xyz/rebalancer/dist/tracking/types), though that's a bit less tidy.

Comment thread solidity/contracts/mock/MockValueTransferBridge.sol
Comment thread typescript/rebalancer-sim/src/MockInfrastructureController.ts Outdated
Comment thread typescript/rebalancer-sim/src/MockInfrastructureController.ts Outdated
Comment thread typescript/rebalancer-sim/src/MockInfrastructureController.ts
Comment thread typescript/rebalancer-sim/src/runners/MockActionTracker.ts
Comment thread typescript/rebalancer-sim/test/integration/full-simulation.test.ts
- Log static-call pre-check failures instead of silently swallowing
- Use explicit `> 0n` check instead of falsy check for bigint amount
- Add failRebalanceByRoute to MockActionTracker for timeout path
  (timed-out transfers should not be marked as complete)
- Auto-complete parent intent in completeRebalanceAction
- Add division-by-zero guard in inflight-guard test
- Call initialize() on MockValueTransferBridge in e2e tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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: 3

🤖 Fix all issues with AI agents
In `@typescript/rebalancer-sim/src/runners/MockActionTracker.ts`:
- Around line 14-58: Replace the local interface declarations Transfer,
RebalanceIntent, and RebalanceAction in MockActionTracker.ts with imports from
the canonical package: add an import type statement that brings in Transfer,
RebalanceIntent, and RebalanceAction from '@hyperlane-xyz/rebalancer' and then
delete the three local interface blocks; ensure any code in MockActionTracker.ts
that referenced the local types uses the imported names unchanged so types line
up with the source of truth.

In `@typescript/rebalancer-sim/test/integration/full-simulation.test.ts`:
- Around line 333-355: The test currently guards assertions behind "if
(simpleResult)" and "if (productionResult)", which lets the test silently pass
when results are missing; remove those conditional guards and instead assert
presence of results first (e.g., use expect(simpleResult, 'missing
SimpleRebalancer result').to.exist and expect(productionResult, 'missing
ProductionRebalancer result').to.exist) and then run the existing KPI assertions
against simpleResult.kpis and productionResult.kpis so failures are explicit;
update references to simpleResult and productionResult accordingly and keep the
existing KPI checks unchanged.
- Around line 244-264: The test currently uses conditional checks (if
(productionResult) / if (simpleResult)) which let the test pass silently when
results.find(...) returns undefined; change these to explicit existence
assertions before checking KPIs — e.g., assert that productionResult and
simpleResult are defined (using expect(productionResult).to.exist /
expect(simpleResult).to.exist or expect(...).to.not.be.undefined) and then
perform the completionRate assertions on productionResult.kpis and
simpleResult.kpis; apply the same fix to the blocked-user-transfer test by
replacing its conditional existence checks with explicit expects so the test
fails fast when the expected result objects are missing.
🧹 Nitpick comments (6)
typescript/rebalancer-sim/src/runners/MockActionTracker.ts (3)

191-216: Silent no-ops when entities aren't found — that'll come back like an unwelcome visitor.

Methods like completeRebalanceIntent, cancelRebalanceIntent, failRebalanceIntent, and failRebalanceAction silently do nothing when the ID doesn't exist. In a simulation, calling these with a stale or wrong ID is likely a bug worth knowing about. At minimum, a logger.warn would save someone a debugging headache.

Example for one method — same pattern for others
   async completeRebalanceIntent(id: string): Promise<void> {
     const intent = this.intents.get(id);
-    if (intent) {
-      intent.status = 'complete';
-      intent.updatedAt = Date.now();
-      logger.debug({ id }, 'Rebalance intent completed');
+    if (!intent) {
+      logger.warn({ id }, 'completeRebalanceIntent: intent not found');
+      return;
     }
+    intent.status = 'complete';
+    intent.updatedAt = Date.now();
+    logger.debug({ id }, 'Rebalance intent completed');
   }

As per coding guidelines, "Never swallow errors silently; always log or re-throw caught exceptions."


306-312: clear() resets idCounter — be mindful of ID collisions if interleaved with lookups.

Resetting idCounter to 0 after clearing is fine for full test isolation, but if something still holds references to old IDs (e.g., async callbacks in flight), newly generated IDs could collide. Since this is test-only and meant for clean resets between runs, it's probably fine — just noting it.


322-385: createActionForPendingIntent matches by exact amount — might miss partial fills or rounding.

The filter uses strict equality i.amount === amount (line 335). If bridge transfers ever have amounts that differ slightly (fees, rounding), this won't match. In the current sim where amounts are controlled, this is okay, but it's worth a comment noting the assumption.

Also, this method is synchronous while createRebalanceAction is async — minor inconsistency in the API surface but not a functional concern.

typescript/rebalancer-sim/src/MockInfrastructureController.ts (3)

91-101: Fire-and-forget onDispatch — unhandled rejections could vanish into the swamp.

void this.onDispatch(...) suppresses the floating-promise lint warning but if onDispatch throws, it becomes an unhandled promise rejection. In simulation this might just cause a test to hang mysteriously rather than fail clearly.

Catch and log the rejection
       mailbox.on(
         mailbox.filters.Dispatch(),
         (
           sender: string,
           destination: number,
           _recipient: string,
           message: string,
         ) => {
-          void this.onDispatch(chainName, sender, destination, message);
+          this.onDispatch(chainName, sender, destination, message).catch(
+            (error: unknown) => {
+              logger.error(
+                { chainName, error },
+                'Unhandled error in onDispatch',
+              );
+            },
+          );
         },
       );

As per coding guidelines, "Always log or re-throw errors; never swallow errors silently."


146-167: Hardcoded offset 77 and scale 1e18 — a couple o' magic numbers that deserve their own names.

The body offset (77 bytes) is the Hyperlane message header size, and 1e18 is the warp token decimal scaling. Both are brittle if the message format or token configuration changes. Extracting these as named constants would make intent clearer and updates easier.

Also, if decoding fails, amount stays at 0n, which means downstream tracking (KPI, actionTracker) silently records a zero-amount transfer. The logger.warn is good, but this defensive fallback could mask real issues — a failed decode for a bridge transfer means completeRebalanceByRoute won't find a matching action later (amount mismatch).

Extract constants and consider failing loudly
+const HYPERLANE_MESSAGE_BODY_OFFSET = 77;
+const WARP_TOKEN_SCALE = BigInt(1e18);
+
   // Decode amount from body
-  const bodyOffset = 77;
+  const bodyOffset = HYPERLANE_MESSAGE_BODY_OFFSET;
   const body = '0x' + message.slice(2 + bodyOffset * 2);
   let amount = 0n;
   try {
     const decoded = ethers.utils.defaultAbiCoder.decode(
       ['bytes32', 'uint256'],
       body,
     );
     const scaledAmount = decoded[1].toBigInt();
-    amount = type === 'user-transfer' ? scaledAmount / BigInt(1e18) : scaledAmount;
+    amount = type === 'user-transfer' ? scaledAmount / WARP_TOKEN_SCALE : scaledAmount;
   } catch (error) {

42-49: signer! definite assignment assertion — lifecycle coupling not enforced.

this.signer is declared with ! (line 44) meaning TS trusts it's assigned before use. If processReadyMessages somehow fires before start() completes (e.g., during tests), you'd get a runtime error with no helpful message. An assert at the top of doProcessReadyMessages would be a safer contract.

That said, the nonceInitialized guard at line 237 provides some protection, though it re-fetches the nonce rather than guarding against a missing signer.

Comment thread typescript/rebalancer-sim/src/runners/MockActionTracker.ts Outdated
Comment thread typescript/rebalancer-sim/test/integration/full-simulation.test.ts
Comment thread typescript/rebalancer-sim/test/integration/full-simulation.test.ts
…c loop

Txs are sequential (await tx.wait()), so ethers handles nonces naturally.
Replace setInterval + re-entry guard with simple while loop + sleep.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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

🤖 Fix all issues with AI agents
In `@typescript/rebalancer-sim/src/MockInfrastructureController.ts`:
- Around line 74-96: The start method in MockInfrastructureController currently
calls the async onDispatch fire-and-forget (via void) and allows unhandled
rejections; change the mailbox handler in start() to capture the returned
promise and handle rejections (e.g., call onDispatch(...).catch(...) or wrap in
try/catch via an async wrapper) and log the error; additionally, modify
onDispatch to defensively check this.domains[originChain] (or
this.domains[chainName]) at the top and bail with a clear logged error if the
domain is missing to avoid a null/undefined access and surface the problem.
🧹 Nitpick comments (4)
typescript/rebalancer-sim/src/MockInfrastructureController.ts (4)

17-32: Well-structured interface — nice and clean.

One small thing: the type field uses string literals where the coding guidelines prefer enum values for discriminant fields. For an internal simulation type with just two values, this is fine to defer. As per coding guidelines, "Use enum values for discriminant fields in interfaces instead of string literals."


134-155: Magic number 77 for body offset — give it a name so it doesn't just lurk in the code like a fairy-tale creature.

This protocol-level constant deserves to be a named constant for clarity and to avoid accidental modification. Also, BigInt(1e18) on line 149 could be extracted alongside it.

Suggested extraction
+/** Byte offset into a Hyperlane message where the TokenRouter body starts */
+const TOKEN_MESSAGE_BODY_OFFSET = 77;
+const WARP_TOKEN_SCALE = BigInt(1e18);
+
 ...
-    const bodyOffset = 77;
-    const body = '0x' + message.slice(2 + bodyOffset * 2);
+    const body = '0x' + message.slice(2 + TOKEN_MESSAGE_BODY_OFFSET * 2);
 ...
-        type === 'user-transfer' ? scaledAmount / BigInt(1e18) : scaledAmount;
+        type === 'user-transfer' ? scaledAmount / WARP_TOKEN_SCALE : scaledAmount;

150-150: Prefer explicit catch (error: unknown) per coding guidelines.

All three catch blocks use catch (error) without the explicit type annotation. The guidelines ask for catch (error: unknown) in new code.

Quick diff
-    } catch (error) {
+    } catch (error: unknown) {

(Apply at lines 150, 223, and 260.)

As per coding guidelines, "When catching errors, prefer catch (error: unknown) with type guards over catch (e: any) in new code."

Also applies to: 223-223, 260-260


286-288: removeAllListeners() is a broad stroke — clears every listener on the mailbox, not just ours.

In the simulation context this is likely fine since the controller owns these contract instances. But if other code ever shares these mailbox references, listeners would be silently lost. Consider storing the listener references and using mailbox.off(filter, listener) for surgical cleanup.

Comment thread typescript/rebalancer-sim/src/MockInfrastructureController.ts
nambrot and others added 2 commits February 6, 2026 11:27
- Import Transfer/RebalanceIntent/RebalanceAction from @hyperlane-xyz/rebalancer
  instead of redefining locally (export added to rebalancer index)
- Catch unhandled rejections from async onDispatch in event listener
- Guard domains[originChain] access in onDispatch

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@nambrot nambrot enabled auto-merge February 6, 2026 16:37
nambrot and others added 2 commits February 6, 2026 12:14
Bridge extends Router, so _Router_dispatch requires a remote router
enrolled for the destination domain. Without it, transferRemote reverts
and the SentTransferRemote event listener hangs forever.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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: 0

Caution

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

⚠️ Outside diff range comments (1)
solidity/contracts/mock/MockValueTransferBridge.sol (1)

61-66: ⚠️ Potential issue | 🟡 Minor

block.chainid truncated to uint32 — fine for known test chains, but worth a wee note.

block.chainid is uint256; casting to uint32 silently drops the upper bits. For Anvil/Hardhat test chains this won't bite, but if this mock were ever reused on a chain with a large chain ID, the event would log a wrong origin. Also, block.chainid doesn't necessarily equal the Hyperlane domain ID — in production those are separate concepts.

Since it's a mock, this is just something to keep in mind rather than a swamp-level emergency.

🧹 Nitpick comments (1)
solidity/contracts/mock/MockValueTransferBridge.sol (1)

12-12: collateral could be immutable since it's only set in the constructor.

It's set once and never mutated again — marking it immutable would be the proper way to go here. It's a mock, so it's not like the gas savings matter much in the grand swamp of things, but it keeps the pattern tidy.

Suggested diff
-    address public collateral;
+    address public immutable collateral;

As per coding guidelines: "In Solidity, use constant for compile-time values and immutable for constructor-set values."

Copy link
Copy Markdown
Collaborator

@paulbalaji paulbalaji left a comment

Choose a reason for hiding this comment

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

LGTM — core refactor is clean and the inflight tracking benefits are well-demonstrated.

5 minor items as inline comments below. None are blockers.

Already Addressed from Prior Reviews

  • ✅ Transfer ID-based removal (vs origin+destination matching)
  • onDispatch rejection handling (.catch() wrapper)
  • ✅ Domain guard in onDispatch
  • failRebalanceByRoute for timeout path
  • completeRebalanceAction auto-completes parent intent
  • ✅ Division-by-zero guard in inflight-guard test
  • ✅ Types imported from @hyperlane-xyz/rebalancer
  • initialize() called on externally provided ActionTracker
  • ✅ Dynamic anvil ports
  • ✅ Bridge initialize() + enrollRemoteRouter in e2e tests

Comment thread typescript/rebalancer-sim/src/SimulationEngine.ts Outdated
Comment thread typescript/rebalancer-sim/src/SimulationEngine.ts
Comment thread typescript/rebalancer-sim/src/MockInfrastructureController.ts
Comment thread typescript/rebalancer-sim/test/integration/full-simulation.test.ts
Comment thread solidity/contracts/mock/MockValueTransferBridge.sol Outdated
nambrot and others added 2 commits February 6, 2026 15:54
- Make collateral immutable in MockValueTransferBridge
- Replace Record<string, any> with Record<string, ChainMetadata>
- Log errors in catch blocks instead of swallowing silently
- Extract magic numbers into named constants (MESSAGE_BODY_OFFSET, WARP_TOKEN_SCALE)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…text-adapter

# Conflicts:
#	typescript/rebalancer-sim/test/integration/full-simulation.test.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

🤖 Fix all issues with AI agents
In `@typescript/rebalancer-sim/src/MockInfrastructureController.ts`:
- Around line 311-345: When timing out in waitForAllDeliveries, avoid replacing
this.pendingMessages because processLoop may be mid-delivery; instead mark each
pending message as cancelled (e.g., add msg.timedOut = true) and call the
failure KPI/actionTracker logic, then remove or keep them as needed. Also update
the delivery path in processLoop (where
recordRebalanceComplete/recordTransferComplete are called) to check msg.timedOut
and skip completion KPIs for timed-out messages. Reference waitForAllDeliveries,
this.pendingMessages, processLoop, recordRebalanceFailed,
recordRebalanceComplete, recordTransferFailed and recordTransferComplete when
making the changes.
🧹 Nitpick comments (7)
solidity/contracts/mock/MockValueTransferBridge.sol (2)

33-47: Hardcoded collateral quote of 1 — worth a quick note.

Right, so quotes[0] = Quote(collateral, 1) always reports a collateral fee of 1 wei regardless of _amountOut, while transferRemote actually pulls the full _amountOut via safeTransferFrom. If Quote represents additional fees on top of the transfer amount, this is fine — it's a mock charging a trivial fee. But if any caller uses this quote to determine how much collateral to approve, they'll approve way too little and the transfer will revert.

For a mock that only lives in test harnesses, this probably won't bite anyone, but it might cause head-scratching later if someone writes a test that actually checks approval amounts against the quote. A small comment clarifying the intent (e.g., // Mock: trivial fee, caller must separately approve _amountOut) would save some time spelunking through the layers.

📝 Optional: add a clarifying comment
         Quote[] memory quotes = new Quote[](2);
-        quotes[0] = Quote(collateral, 1);
+        // Mock fee: callers must separately approve _amountOut for the actual transfer
+        quotes[0] = Quote(collateral, 1);
         quotes[1] = Quote(address(0), dispatchFee);

76-88: _handle minting logic — watch the bytes32→address truncation.

The conversion address(uint160(uint256(recipientBytes32))) takes the lower 20 bytes. This is the standard Hyperlane convention (TypeCasts.bytes32ToAddress), so it's consistent with how addresses are encoded elsewhere in the codebase. The minting via ERC20Test.mintTo is clean for a mock — no need for actual cross-chain token mechanics.

One thing worth considering: there's no guard against recipient == address(0). If a garbled message arrives (or a test bug encodes a zero recipient), this mints tokens to address(0), which is effectively a burn. For a mock contract in a controlled test environment, this is unlikely to matter, but a quick require(recipient != address(0)) would make debugging easier if it ever does happen.

🛡️ Optional: guard against zero-address recipient
         address recipient = address(uint160(uint256(recipientBytes32)));
+        require(recipient != address(0), "MockBridge: zero recipient");
         // Mint collateral tokens to recipient (destination warp token)
         ERC20Test(collateral).mintTo(recipient, amount);
typescript/rebalancer-sim/src/MockInfrastructureController.ts (3)

19-20: Prefer 10n ** 18n for the scale constant.

BigInt(1e18) works here because 1e18 is exactly representable in IEEE 754, but using 10n ** 18n is the idiomatic BigInt pattern and sidesteps any reader's doubt about float-to-bigint precision.

🔧 Optional tweak
-const WARP_TOKEN_SCALE = BigInt(1e18);
+const WARP_TOKEN_SCALE = 10n ** 18n;

153-171: Body decoding looks correct, but narrow the catch type.

The hex slicing and ABI decode logic are sound. One small thing though — the catch at line 166 should type the error as unknown per the project's guidelines. It's not going to break anything, but somebody's going to bring it up eventually.

🔧 Minor fix
-    } catch (error) {
+    } catch (error: unknown) {

As per coding guidelines, "Catch errors as unknown type and narrow with type guards; avoid catch(e: any) in new code."


254-283: Process + wait pattern is fine for simulation; type the catch.

The splice-from-pending-during-iteration is safe since ready is a separate filtered array. The retry-on-tx-failure path also works because the static pre-check on the next pass will catch already-delivered messages.

Same catch (error: unknown) nit as above on line 276.

🔧 Minor fix
-        } catch (error) {
+        } catch (error: unknown) {

As per coding guidelines, "Catch errors as unknown type and narrow with type guards; avoid catch(e: any) in new code."

typescript/rebalancer-sim/src/SimulationEngine.ts (2)

229-246: Fallback KPI recording on transfer failure is the right call.

Since no Dispatch event fires when the on-chain tx fails, manually recording start+fail keeps KPI counts honest. Same catch (error: unknown) nit on line 231 though.

🔧 Minor fix
-    } catch (error) {
+    } catch (error: unknown) {

As per coding guidelines, "Catch errors as unknown type and narrow with type guards; avoid catch(e: any) in new code."


279-310: buildHyperlaneCore is solid; a couple of small things.

  1. Record<string, ChainMetadata> on line 280 could be ChainMap<ChainMetadata> — it's the same type, just the project's preferred alias for per-chain maps.

  2. The as cast on line 305 can be avoided with instanceof:

🔧 Optional cleanup
+import type { ChainMap } from '@hyperlane-xyz/sdk';
 // ...
-    const chainMetadata: Record<string, ChainMetadata> = {};
+    const chainMetadata: ChainMap<ChainMetadata> = {};
 // ...
       if (p && 'pollingInterval' in p) {
-        (p as ethers.providers.JsonRpcProvider).pollingInterval = 100;
+        if (p instanceof ethers.providers.JsonRpcProvider) {
+          p.pollingInterval = 100;
+        }
       }

As per coding guidelines, "Use ChainMap for per-chain configurations" and "avoid unnecessary type casts (as assertions)."

Comment thread typescript/rebalancer-sim/src/MockInfrastructureController.ts
@hyper-gonk
Copy link
Copy Markdown
Contributor

hyper-gonk Bot commented Feb 6, 2026

⚙️ Node Service Docker Images Built Successfully

Service Tag
🔑 key-funder dc66ae8-20260206-205813
🔍 offchain-lookup-server dc66ae8-20260206-205813
♻️ rebalancer dc66ae8-20260206-205813
🚀 ts-relayer dc66ae8-20260206-205813
🕵️ warp-monitor dc66ae8-20260206-205813
Full image paths
gcr.io/abacus-labs-dev/hyperlane-key-funder:dc66ae8-20260206-205813
gcr.io/abacus-labs-dev/hyperlane-offchain-lookup-server:dc66ae8-20260206-205813
gcr.io/abacus-labs-dev/hyperlane-rebalancer:dc66ae8-20260206-205813
gcr.io/abacus-labs-dev/hyperlane-ts-relayer:dc66ae8-20260206-205813
gcr.io/abacus-labs-dev/hyperlane-warp-monitor:dc66ae8-20260206-205813

@nambrot nambrot added this pull request to the merge queue Feb 6, 2026
Merged via the queue into main with commit a1a6d88 Feb 6, 2026
152 checks passed
@nambrot nambrot deleted the nam/mock-inflight-context-adapter branch February 6, 2026 21:20
@github-project-automation github-project-automation Bot moved this from Sprint to Done in Hyperlane Tasks Feb 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

2 participants