Skip to content

Sync with upstream repo - #149

Open
github-actions[bot] wants to merge 738 commits into
LiskHQ:masterfrom
across-protocol:master
Open

github-actions[bot] wants to merge 738 commits into
LiskHQ:masterfrom
across-protocol:master

Conversation

@github-actions

@github-actions github-actions Bot commented Nov 24, 2025 •

Copy link
Copy Markdown

Merge latest changes from upstream repo

Summary by CodeRabbit

  • New Features

    • New CLI tooling (Binance swap/sweep/inspect/refill, simulate/refill/withdraw utilities), BridgeApi and TokenSplitter bridge options, Solana CCTP v2 mint/receive support, optional Redis caching, multi-gateway Arweave support.
  • Infrastructure & Documentation

    • Node 22.18, CI reworked for shared executors and workspace caching, improved Docker build, updated lint/format configs, and extensive new operator docs (bundle construction, inventory, rebalancer, CCTP finalizer, scripts).
  • Removals

    • Scroll bridge adapter and related Scroll ABIs/artifacts removed.

Review Change Stack

pxrl and others added 12 commits May 7, 2026 10:30
For improved SpokePoolClient duplicate event detection & tiny reduction
in strictNullChecks violations.
## What changed
- removed the custom Binance server-time synchronizer from
`getBinanceApiClient()`
- introduced a simple endpoint-level `recvWindow` policy in
`src/utils/BinanceUtils.ts`
- route signed read/setup calls through shared helpers with `recvWindow
= 60000` where the Binance endpoint accepts it
- route market orders through a shared helper with `recvWindow = 60000`
- route withdrawals through a shared helper with `recvWindow = 5000`
- route the current Binance withdrawal-quota caller through
`getBinanceWithdrawalLimits()` while keeping that custom private-request
payload empty
- updated Binance call sites in the rebalancer, finalizer, Binance
client, and bridge adapters to use those helpers
- added focused tests that pin the read/order/withdrawal `recvWindow`
behavior

## Why
The original goal is narrow: avoid Binance `-1021 INVALID_TIMESTAMP` /
`recvWindow` errors without adding unnecessary complexity.

After reviewing the signed Binance calls in this repo, the cleanest
policy is:
- signed reads can tolerate a much larger validity window, because a
delayed accepted request still returns current server data at processing
time
- market orders need the same wider window to tolerate the clock skew
and request delay that motivated this change
- withdrawals should stay tight, because we do not want delayed accepted
requests to submit funds transfers much later than intended

That means we do not need a custom client-side time synchronizer here.

## Policy
- signed read/setup endpoints: `recvWindow = 60000`
  - `tradeFee()`
  - `depositAddress()`
  - `allOrders()`
  - `accountCoins()` / `capital/config/getall`
  - `depositHistory()`
  - `withdrawHistory()`
- market `order()`: `recvWindow = 60000`
- `withdraw()`: `recvWindow = 5000`
- withdrawal quota helper: empty custom private-request payload

## Notes
This is still not a substitute for keeping the host clock reasonably
synced. A larger `recvWindow` helps with stale timestamps, but it does
not fix the case where the local clock is ahead of Binance.

## Validation
- `yarn test test/BinanceUtils.ts`
- `yarn eslint src/utils/BinanceUtils.ts src/clients/BinanceClient.ts
src/rebalancer/adapters/binance.ts
src/adapter/bridges/BinanceCEXBridge.ts
src/adapter/bridges/BinanceCEXNativeBridge.ts
src/adapter/l2Bridges/BinanceCEXBridge.ts
src/adapter/l2Bridges/BinanceCEXNativeBridge.ts
src/finalizer/utils/binance.ts test/BinanceUtils.ts`
- `yarn build`
None of this is actually used.
The Lens USDC bridge does not support `finalizeDeposit` so we need to
still use `finalizeWithdrawal` there.

---------

Signed-off-by: bennett <bennett@umaproject.org>
Co-authored-by: Paul <108695806+pxrl@users.noreply.github.com>
This is the final piece of the puzzle, enable strictNullChecks!
Enable the following:
- await-thenable (don't await on non-promises)
- require-array-sort-compare (avoid sorting by string)
- no-unnecessary-type-assertion (drop a bunch of redunant type fudging)
remove extra the in src/relayer/README.md

Signed-off-by: criciss <cricis@msn.com>
This uses go binaries for speed. Add a couple of :fast options to yarn
script targets to access it. Obviously not to be enabled in production
builds any time soon.
@coderabbitai

coderabbitai Bot commented May 8, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Large coordinated overhaul: bridges/adapters and l2Bridges use contract-entry helpers and pending-bridge hooks; clients (Transaction, MultiCaller, Inventory, Profit) rewritten; CCTP finalizer added with EVM/SVM V2 support; many operator scripts added/updated; constants, contract-addresses, ABIs, CI, Docker, and docs updated.

Changes

Core Refactor and Features

Layer / File(s) Summary
Adapters and L2 bridges refactor
src/adapter/**, src/adapter/bridges/**, src/adapter/l2Bridges/**, src/common/ContractAddresses.ts, src/common/Constants.ts
Bridges now resolve contracts via getContractEntry/getContractAbi, add pending-bridge Redis integration, introduce TokenSplitter and BridgeApi adapters, update many bridge implementations (OFT, CCTP, OpStack, ZKStack, Binance, Blast, etc.), and remove Scroll-related adapters/ABIs.
Client engines update
src/clients/**, src/cache/Redis.ts
TransactionClient: signer rotation, per-signer nonces, ensureConfirmation and TVM path; MultiCaller: grouping changes, gas multiplier tuning; InventoryClient/ProfitClient: major refactors and new APIs; new BinanceClient; new Redis cache module replaces old caching module.
CCTP finalizer service
src/cctp-finalizer/**
Adds finalizer app, typed CCTP error hierarchy, services, EVM/SVM mint utilities including Solana V2 receive logic, message parsing helpers, types, and README.
Operator scripts
scripts/**
Adds/updates many scripts: swapOnBinance, sweepBinanceBalance, simulateFill, refillUsdcToSpecificChain, withdrawTokenFromL2, and others; rewires scripts to use getContractEntry and SDK/typechain factories.
Contracts / ABIs
contracts/*, src/common/abi/*, deployments/*
Adds AtomicWethDepositor constructor, removes MockScrollBridge, updates mock event signatures, and adds multiple ABI JSON files (CounterfactualDepositFactory, EIP3009, Hyperliquid, Permit2, SP1Helios) while removing some Scroll ABIs.
Infrastructure / config
.circleci/config.yml, Dockerfile, .env.example, .nvmrc, .eslintrc.js, package.json
CI: shared executor/workspaces, added typecheck job; Dockerfile two-phase Yarn build and ARGs; .env.example supports RELAYER_TEST and ARWEAVE_GATEWAYS; Node version pinned; ESLint/Prettier and package scripts/tooling updated.
Constants & Contract addresses
src/common/Constants.ts, src/common/ContractAddresses.ts
Expanded CONTRACT_ADDRESSES and helper getters (getContractEntry, getContractAbi, getContractAddress); new chain entries (MEGAETH, TEMPO, TRON), token-splitter registries, LZ fee tokens and generated SWAP_ROUTES.
Docs and guidance
docs/**, AGENTS.md, src/clients/README.md
Large set of new/updated docs: architecture, bundle construction, inventory model, rebalancer architecture, repayment selection/eligibility, slow-fill lifecycle, CCTP finalizer README, and multiple script READMEs.

Estimated code review effort
🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

A rabbit taps keys with a careful hop,
Bridges rerouted, no bytes left to drop.
CCTP sings, Solana joins the tune,
Binance swaps whirl, then settle soon.
Clients aligned, docs shining bright—ship on, goodnight. 🐇✨

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

pxrl and others added 17 commits May 8, 2026 19:19
Redis can be used independently for non-cache services. This gives a
better structure and cleans up some tech debt.
#3380)

Bumps
[actions/dependency-review-action](https://github.com/actions/dependency-review-action)
from 4.9.0 to 5.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/dependency-review-action/releases">actions/dependency-review-action's
releases</a>.</em></p>
<blockquote>
<h2>5.0.0</h2>
<p>This is a new major version of the Dependency Review Action which
updates the runtime to node24. This requires a minimum Actions Runner
version <a
href="https://github.com/actions/runner/releases/tag/v2.327.1">v2.327.1</a>
to run.</p>
<h2>What's Changed</h2>
<ul>
<li>Add .github/copilot-instructions.md for Copilot coding agent by <a
href="https://github.com/ahpook"><code>@​ahpook</code></a> in <a
href="https://redirect.github.com/actions/dependency-review-action/pull/1067">actions/dependency-review-action#1067</a></li>
<li>Update Node.js runtime from 20 to 24 by <a
href="https://github.com/scottschreckengaust"><code>@​scottschreckengaust</code></a>
in <a
href="https://redirect.github.com/actions/dependency-review-action/pull/1084">actions/dependency-review-action#1084</a></li>
<li>Bump spdx-license-ids from 3.0.20 to 3.0.23 by <a
href="https://github.com/mongolyy"><code>@​mongolyy</code></a> in <a
href="https://redirect.github.com/actions/dependency-review-action/pull/1091">actions/dependency-review-action#1091</a></li>
<li>docs: bump actions/checkout from v4 to v6 in workflow examples by <a
href="https://github.com/Marukome0743"><code>@​Marukome0743</code></a>
in <a
href="https://redirect.github.com/actions/dependency-review-action/pull/1077">actions/dependency-review-action#1077</a></li>
<li>fix: patched version display for advisories with non-strict semver
ranges (e.g. Maven beta versions) by <a
href="https://github.com/tspascoal"><code>@​tspascoal</code></a> in <a
href="https://redirect.github.com/actions/dependency-review-action/pull/1076">actions/dependency-review-action#1076</a></li>
<li>Resolve security findings by <a
href="https://github.com/AshelyTC"><code>@​AshelyTC</code></a> in <a
href="https://redirect.github.com/actions/dependency-review-action/pull/1094">actions/dependency-review-action#1094</a></li>
<li>v5.0.0 release branch by <a
href="https://github.com/ahpook"><code>@​ahpook</code></a> in <a
href="https://redirect.github.com/actions/dependency-review-action/pull/1098">actions/dependency-review-action#1098</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/scottschreckengaust"><code>@​scottschreckengaust</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/dependency-review-action/pull/1084">actions/dependency-review-action#1084</a></li>
<li><a href="https://github.com/mongolyy"><code>@​mongolyy</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/dependency-review-action/pull/1091">actions/dependency-review-action#1091</a></li>
<li><a
href="https://github.com/Marukome0743"><code>@​Marukome0743</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/dependency-review-action/pull/1077">actions/dependency-review-action#1077</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/dependency-review-action/compare/v4.9.0...v5.0.0">https://github.com/actions/dependency-review-action/compare/v4.9.0...v5.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/dependency-review-action/commit/a1d282b36b6f3519aa1f3fc636f609c47dddb294"><code>a1d282b</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/dependency-review-action/issues/1098">#1098</a>
from actions/ahpook/v5-release</li>
<li><a
href="https://github.com/actions/dependency-review-action/commit/eb6c199c5a85c7387f1f0b02b3ba5c6364740695"><code>eb6c199</code></a>
update examples to show <a
href="https://github.com/v5"><code>@​v5</code></a></li>
<li><a
href="https://github.com/actions/dependency-review-action/commit/3943c2c5beaaaf1806eb3758273c203dabcbf89c"><code>3943c2c</code></a>
v5.0.0 release branch</li>
<li><a
href="https://github.com/actions/dependency-review-action/commit/454943c880b147adbfe7de0cdd3ece1c00882033"><code>454943c</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/dependency-review-action/issues/1094">#1094</a>
from actions/ashelytc/security-findings</li>
<li><a
href="https://github.com/actions/dependency-review-action/commit/6d92a1228e9e9db334f02c09f84fe9217d2b4463"><code>6d92a12</code></a>
revert <code>@​typescript-eslint/parser</code> update</li>
<li><a
href="https://github.com/actions/dependency-review-action/commit/a8e5a7e93695b41abf6d1083cd220bee39a720f0"><code>a8e5a7e</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/dependency-review-action/issues/1076">#1076</a>
from tspascoal/fix-version-matching-for-non-string-s...</li>
<li><a
href="https://github.com/actions/dependency-review-action/commit/b6b7079031ef4ed61656c221988f1f3bcbf35101"><code>b6b7079</code></a>
update <code>@​typescript-eslint/parser</code> to 8.40.0</li>
<li><a
href="https://github.com/actions/dependency-review-action/commit/821a21dd691f162c4c5c2e9754a344accde9a208"><code>821a21d</code></a>
update more dependencies</li>
<li><a
href="https://github.com/actions/dependency-review-action/commit/05aaaae45cf4c420de012addf2a72e3435ddaa63"><code>05aaaae</code></a>
run npm audit fix</li>
<li><a
href="https://github.com/actions/dependency-review-action/commit/55d3e754501fc13c84b95637ce51f135012d41ea"><code>55d3e75</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/dependency-review-action/issues/1077">#1077</a>
from Marukome0743/docs/checkout</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/dependency-review-action/compare/v4.9.0...v5.0.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/dependency-review-action&package-manager=github_actions&previous-version=4.9.0&new-version=5.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fix some comments to improve readability

Signed-off-by: box4wangjing <box4wangjing@outlook.com>
Bumps
[fast-xml-builder](https://github.com/NaturalIntelligence/fast-xml-builder)
from 1.1.5 to 1.2.0.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/NaturalIntelligence/fast-xml-builder/blob/main/CHANGELOG.md">fast-xml-builder's
changelog</a>.</em></p>
<blockquote>
<p><strong>1.2.0</strong> (2026-05-08)</p>
<ul>
<li>Add support for <code>sanitizeName</code> option</li>
<li>Support xml-naming for validating and sanitizing tag and attribute
names</li>
</ul>
<p><strong>1.1.9</strong> (2026-05-06)</p>
<ul>
<li>fix: format output for preserve order when indent by is set to empty
string</li>
</ul>
<p><strong>1.1.8</strong> (2026-05-05)</p>
<ul>
<li>fix: skip text property for PI tags</li>
<li>improve typings</li>
</ul>
<p><strong>1.1.7</strong> (2026--05-04)</p>
<ul>
<li>fix security issues when attribute value contains quotes</li>
</ul>
<p><strong>1.1.6</strong> (2026--05-04)</p>
<ul>
<li>fix security issues related to comment</li>
<li>skip comment with null value</li>
</ul>
<p><strong>1.1.5</strong> (2026-04-17)</p>
<ul>
<li>fix security issues related to comment and cdata</li>
</ul>
<p><strong>1.1.4</strong> (2026-03-16)</p>
<ul>
<li>support maxNestedTags option</li>
</ul>
<p><strong>1.1.3</strong> (2026-03-13)</p>
<ul>
<li>declare Matcher &amp; Expression as unknown so user is not forced to
install path-expression-matcher</li>
</ul>
<p><strong>1.1.2</strong> (2026-03-11)</p>
<ul>
<li>fix typings</li>
</ul>
<p><strong>1.1.1</strong> (2026-03-11)</p>
<ul>
<li>upgrade path-expression-matcher to 1.1.3</li>
</ul>
<p><strong>1.1.0</strong> (2026-03-10)</p>
<ul>
<li>Integrate <a
href="https://github.com/NaturalIntelligence/path-expression-matcher">path-expression-matcher</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/NaturalIntelligence/fast-xml-builder/commit/a9a905b316176ef9a97bdf5450e60efbf0341f25"><code>a9a905b</code></a>
for release</li>
<li><a
href="https://github.com/NaturalIntelligence/fast-xml-builder/commit/42680e8d730c48082268823fd285e10127ddba21"><code>42680e8</code></a>
support name sanitization</li>
<li><a
href="https://github.com/NaturalIntelligence/fast-xml-builder/commit/8b00185bf6be67981ffc40e06c18acbbbe908779"><code>8b00185</code></a>
release info</li>
<li><a
href="https://github.com/NaturalIntelligence/fast-xml-builder/commit/8a08f173d7b9c9a82599fe7de279ca7e12c3ad6b"><code>8a08f17</code></a>
allow indentation to be empty string</li>
<li><a
href="https://github.com/NaturalIntelligence/fast-xml-builder/commit/7fc5decb9613afbd5d03747b1a0f11e0916e34ef"><code>7fc5dec</code></a>
update docs</li>
<li><a
href="https://github.com/NaturalIntelligence/fast-xml-builder/commit/c241b6a8ed1863e5f518490ec1fcc38b13f2c370"><code>c241b6a</code></a>
improve documentation</li>
<li><a
href="https://github.com/NaturalIntelligence/fast-xml-builder/commit/15d5668b53777400c8d80b6e21029c1a70888c78"><code>15d5668</code></a>
update for release</li>
<li><a
href="https://github.com/NaturalIntelligence/fast-xml-builder/commit/98774853a696a1aee4dca830dd3eee2759676bd2"><code>9877485</code></a>
fix: skip text property for PI tags</li>
<li><a
href="https://github.com/NaturalIntelligence/fast-xml-builder/commit/311a2213a817cf31558bea7c0e0807b0d4441814"><code>311a221</code></a>
fix <a
href="https://redirect.github.com/NaturalIntelligence/fast-xml-builder/issues/5">#5</a>
typing import issues</li>
<li><a
href="https://github.com/NaturalIntelligence/fast-xml-builder/commit/e8fc5b15d9d54b559781961f066de82a55aabcdd"><code>e8fc5b1</code></a>
update for releast</li>
<li>Additional commits viewable in <a
href="https://github.com/NaturalIntelligence/fast-xml-builder/compare/v1.1.5...v1.2.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=fast-xml-builder&package-manager=npm_and_yarn&previous-version=1.1.5&new-version=1.2.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/across-protocol/relayer/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Paul <108695806+pxrl@users.noreply.github.com>
- v4 is now in maintenance; v5 is the future.
- Reconnect cause now logged per attempt.
- SET options use typed { expiration }, not deprecated { EX }.
- Enables future RESP3 opt-in (client-side caching).
…3372)

Permits a deposit or fill to be resolved via a transaction hash alone,
without the corresponding chain ID.
)

Partition indexer messages by classification: 
- `correct_transfer` keeps the existing forward path
- `mis_route / intent_refund` are routed to a new `initiateWithdraw`
stub only when `WITHDRAW_ENABLED=true` (strict equality with "true"),
and dropped otherwise.

Co-authored-by: Alexandru Matei <alexandrumatei3693@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HyperEVM often throws up "duplicate" events because different RPCs
disagree about transactionIndex due to the inclusion (or not) of system
transactions.

Consolidate on the minimal set of unique constraints that are required
to detect uniqueness. This should reduce the noise from duplicate
events.

While here, drop the hashing of the eventKey - it's redundant.
## Summary
- Add Tron as a direct Binance USDT deposit/withdrawal network for swap
rebalancer route generation.
- Convert Tron addresses between native and ethers-compatible formats
for Binance deposits, withdrawals, and withdrawal-history matching.
- Exclude Tron from Hyperliquid/OFT route families where direct Binance
routing is intended.
- Fix cumulative balance loading so non-EVM remote token addresses such
as Tron USDT do not trip the EVM-only assertion.

## Review Notes
- Ran a review pass on the branch diff against `origin/master` and fixed
the runtime assertion hit by configured Tron USDT cumulative balances.
- Added regression coverage for the cumulative balance loader and Tron
Binance route/address handling.

## Tests
- `git diff --check cc2c253`
- `yarn test test/Rebalancer.loadCumulativeModeBalances.ts
test/RebalancerClient.buildRebalanceRoutes.ts
test/BinanceAdapter.helpers.ts`
- `yarn typecheck`
This PR is meant to enable sponsored flows where USDH is the base token.

---------

Signed-off-by: bennett <bennett@umaproject.org>
1. Wires the bot's `mis_route` and `intent_refund` classifications to
the new quote-api `/swap/counterfactual/sign-withdraw` endpoint. Refunds
the full ERC-20 transfer amount to the leaf-pinned `refundAddress`;
⚠️ gas-reserve / fee deduction is deferred to a follow-up, but should be
added to this PR

- Extend DepositAddressMessage with adminWithdrawManagerContractAddress
and counterfactualMaterials.withdrawLeaf
- Add _post to BaseAcrossApiClient and signedWithdraw to
AcrossSwapApiClient.
- Replace initiateWithdraw stub: balance guard, in-flight + Redis dedup
(mirrors initiateDeposit), and raw-tx broadcast of the quote-api
response.
Bumps [@protobufjs/utf8](https://github.com/dcodeIO/protobuf.js) from
1.1.0 to 1.1.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/dcodeIO/protobuf.js/releases">@​protobufjs/utf8's
releases</a>.</em></p>
<blockquote>
<h2>protobufjs-cli: v1.1.1</h2>
<h2><a
href="https://github.com/protobufjs/protobuf.js/compare/protobufjs-cli-v1.1.0...protobufjs-cli-v1.1.1">1.1.1</a>
(2023-02-02)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>cli:</strong> fix relative path to Google pb files (<a
href="https://redirect.github.com/protobufjs/protobuf.js/issues/1859">#1859</a>)
(<a
href="https://github.com/protobufjs/protobuf.js/commit/e42eea4868b11f4a07934804a56683321ed191e2">e42eea4</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/protobufjs/protobuf.js/commit/644d588c0495da6a570344248e1b5af901bc3b0c"><code>644d588</code></a>
chore: release master (<a
href="https://redirect.github.com/dcodeIO/protobuf.js/issues/1865">#1865</a>)</li>
<li><a
href="https://github.com/protobufjs/protobuf.js/commit/e42eea4868b11f4a07934804a56683321ed191e2"><code>e42eea4</code></a>
fix(cli): fix relative path to Google pb files (<a
href="https://redirect.github.com/dcodeIO/protobuf.js/issues/1859">#1859</a>)</li>
<li><a
href="https://github.com/protobufjs/protobuf.js/commit/dce9a2ef92d363752e40b295b0da9bd178f82e83"><code>dce9a2e</code></a>
fix: use bundled filename to fix common pb includes (<a
href="https://redirect.github.com/dcodeIO/protobuf.js/issues/1860">#1860</a>)</li>
<li><a
href="https://github.com/protobufjs/protobuf.js/commit/64e8936ad9f73c68b3fa1e57857dd38323b5a745"><code>64e8936</code></a>
fix: use ES5 style function syntax (<a
href="https://redirect.github.com/dcodeIO/protobuf.js/issues/1830">#1830</a>)</li>
<li><a
href="https://github.com/protobufjs/protobuf.js/commit/4489fa771464bcb49b57149760e9cc4131e8077e"><code>4489fa7</code></a>
Revert &quot;fix: error should be thrown (<a
href="https://redirect.github.com/dcodeIO/protobuf.js/issues/1817">#1817</a>)&quot;
(<a
href="https://redirect.github.com/dcodeIO/protobuf.js/issues/1864">#1864</a>)</li>
<li>See full diff in <a
href="https://github.com/dcodeIO/protobuf.js/compare/protobufjs-cli-v1.1.0...protobufjs-cli-v1.1.1">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
As was done with the refiller config, compress the monitor config to
make it more manageable.
remove extra word in comment

Signed-off-by: cuoguojida <cuoguojida@outlook.com>
droplet-rl and others added 30 commits August 11, 2026 13:06
No longer necessary to track this.

Co-authored-by: droplet-rl <284132418+droplet-rl@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Paul <108695806+pxrl@users.noreply.github.com>
## Summary

Narrow split of #3680: two `RedisCache` multi-exec primitives, used by
`BaseAdapter` so rebalancer order-state writes cannot be torn by a crash
between two Redis calls.

- `moveSetMember(source, destination, value)`: `_redisUpdateOrderStatus`
previously ran `sRem` + `sAdd` as two independent calls, so a crash in
between could drop an order from both status sets (orphaning it) or
leave it in both
- `setAndAddToSet(key, value, setKey, setValue, ttl)`:
`_redisCreateOrder` previously wrote the order-details key and the
status-set membership as two independent calls; now both land in one
transaction (also honoring an infinite TTL, previously unreachable
through the `Promise.all` path)

No behavior change beyond atomicity.

Independent of and mergeable in any order with #3690 / #3691 / #3692.

## Validation

- `yarn typecheck`, `yarn lint`
- `test/Redis.ts` (new): 2 passing; rebalancer suites
(`BinanceAdapter.helpers`, `BinanceAdapter.conversions`,
`RebalancerAdapter.routeSupport`,
`RebalancerClient.cumulativeRebalancing`, `PendingBridgeAdapters`): 53
passing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01Af752JVaaja93dDBR3Hmav

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Af752JVaaja93dDBR3Hmav)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
…age (#3694)

## Summary

Narrow split of #3680: two InventoryClient shortfall-sizing fixes.

- `_getPossibleShortfallRebalances` now counts Redis-tracked pending
same-asset rebalances (from `RebalancerClient.getPendingRebalances`)
toward the outstanding cross-chain transfer amount for the canonical L2
token. These transfers never appear as on-chain outstanding transfers,
so without this every run re-initiated coverage for a shortfall that was
already covered in flight.
- Same-route shortfall rebalances are combined into a single bridge
transfer instead of one transfer per unfilled deposit, saving
per-transfer bridge fees and pending-order slots on capacity-limited
bridges.

Independent of and mergeable in any order with #3690 / #3691 / #3692 /
#3693.

## Validation

- `yarn typecheck`, `yarn lint`
- two new tests in `test/InventoryClient.InventoryRebalance.ts`
(pending-rebalance coverage, shortfall aggregation); the suite needs
compiled contract fixtures, so it relies on CI in this environment

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01Af752JVaaja93dDBR3Hmav

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Af752JVaaja93dDBR3Hmav)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
…SSION (#3691)

## Summary

Narrow split of #3680: make direct Binance swap deposits crash-safe by
flipping the write order around the deposit transaction.

- new `PENDING_DEPOSIT_SUBMISSION` order state: the order and a recovery
marker are persisted in Redis **before** the deposit transaction is
submitted; the order is promoted to `PENDING_DEPOSIT` (and the marker
cleared) after a clean submission
- on the next lifecycle pass, `_reconcileDepositRecovery` resolves
marked orders from the on-chain deposit receipt: promote on success
(re-tagging the deposit), purge on revert, wait while unconfirmed
- fail-closed defaults: a marked order whose transaction hash was never
persisted, or a markerless `PENDING_DEPOSIT_SUBMISSION` order, is left
for the existing `REBALANCER_PENDING_ORDER_TTL` prune, which now also
covers the new status (and clears the marker)
- if the recovery-marker write itself fails, nothing was submitted, so
the pre-submission order is purged instead of left to the prune

This is a substantially simplified version of the same protection in
#3680/#3689: it deliberately does **not** add broadcast-boundary
callbacks, `DefinitiveTransactionFailure` classification, or new Redis
primitives. The trade-off is that a submission failure that provably
moved no funds (e.g. deposit-address lookup failure) waits out its order
TTL instead of being purged immediately — one pending-order slot held
for at most the TTL, in exchange for ~600 fewer lines.

Independent of and mergeable in any order with #3690 (both touch the
direct-deposit initiation region of `binance.ts`, so whichever lands
second may need a trivial rebase).

## Validation

- `yarn typecheck`, `yarn lint`
- `test/BinanceAdapter.depositSubmission.ts`: 4 passing (write ordering,
failed-submission survival, receipt reconciliation for
success/revert/missing-hash, markerless fail-closed)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01Af752JVaaja93dDBR3Hmav

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Af752JVaaja93dDBR3Hmav)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
…lock (#3692)

## Summary

Narrow split of #3680: protect rebalance initiation from colliding runs
— reworked from a per-route initiation guard to a **run-level mutex**
after review discussion.

- `withRebalancerInitiationLock` wraps each sending entrypoint's whole
plan-and-initiate phase — balance snapshot through initiation — in a
per-account Redis `SET NX` lock (existing `acquireLock`/`releaseLock`
primitives, 30-minute TTL). An overlapping run is **skipped** rather
than allowed to initiate from a snapshot that predates the lock holder's
orders; its next scheduled run replans from fresh state.
- Applied at the three initiating entrypoints: the relayer's
`--rebalancer` run (`runRebalancer`), and the cumulative and same-asset
swap rebalancers. Read-only runs (`SEND_REBALANCES` unset / sending
disabled) do not take the lock, and deployments without a
status-tracking Redis proceed unserialized.
- The lock is released before Redis clients disconnect; a crashed
holder's lock expires via TTL.

Why run-level rather than per-route at initiation time: deficits are
computed upstream of the adapter (InventoryClient / cumulative-client
balance snapshots), so a guard inside `initializeRebalance` cannot cover
the stale-planning window — a run that planned before another run's
order existed could still initiate a duplicate after the guard freed.
Serializing the whole plan-and-initiate phase closes that window by
construction, while multiple *sequential* same-route orders remain
legitimate (`maxAmountsToTransfer`-capped chunks, one per run, bounded
by `maxPendingOrders`), because each serialized run replans with the
prior orders visible. `src/rebalancer/adapters/binance.ts` is untouched
by this PR.

This replaces #3680's signer-scoped reservation-set system
(`reservePendingOrderSlot`/`releasePendingOrderSlot` with account locks,
ownership tokens, and a reservation registry) with one lock around the
run.

Independent of and mergeable in any order with #3690 and #3691.

## Validation

- `yarn typecheck`, `yarn lint`
- `test/Rebalancer.initiationLock.ts`: overlapping run skipped without
planning, release after completion and on failure, per-account scoping,
unserialized fallback without Redis

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01Af752JVaaja93dDBR3Hmav

---------

Co-authored-by: Claude <noreply@anthropic.com>
…account lock (#3692)" (#3707)

Was seen to cause issues in production.

Co-authored-by: droplet-rl <284132418+droplet-rl@users.noreply.github.com>
Co-authored-by: droplet-rl <284132418+droplet-rl@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Paul <108695806+pxrl@users.noreply.github.com>
Enables the serverless-gcp env configuration.

Co-authored-by: droplet-rl <284132418+droplet-rl@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
First small step towards handling deposits via multicall.

Co-authored-by: dijanin-brat <106677800+dijanin-brat@users.noreply.github.com>
… log (#3723)

The deposit-address handler and gasless relayer entrypoints logged the
full `config` object at startup (`startupLogLevel`, info by default),
serializing the mandatory `SWAP_API_KEY` — a live `Authorization:
Bearer` credential used by the swap-API client for
deposit-addresses/execute and sign-withdraw — verbatim into every
configured log transport. Anyone with read access to the log aggregator
or an export could recover it.

This excludes `swapApiKey` from the logged config in both entrypoints,
mirroring how the dataworker already destructures out `addressFilter`
before logging.

Operational note: any key already present in existing logs should be
rotated independently of this change. Build (`tsc --build`), prettier
and eslint pass.

Co-authored-by: droplet-rl <284132418+droplet-rl@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary

- add shared `getAttributedBinanceDeposits` ownership attribution:
- ERC20 deposits use recipient-filtered `Transfer` logs when cheaper
than receipt lookup
  - failed/missing log results fall back to bounded receipt lookups
- native deposits use receipts; resolved null receipts retry once, while
RPC errors propagate
- use the shared utility in the Binance finalizer and Binance L2 bridge
adapters
- interpret `FINALIZER_WITHDRAWAL_TO_ADDRESSES` as `EOA -> authorized
token symbols`
- attribute only configured, confirmed Ethereum/BSC deposits; an empty
token configuration returns before Binance or RPC work
- finalize same-coin deposits to their depositing EOA while serially
consuming the shared Binance balance and withdrawal fees
- fail closed when a relevant confirmed deposit cannot be attributed
- reserve credited, unauthorized, and unsupported-network balances from
orphan sweeping
- send genuine orphan balances to the first configured EOA authorizing
the symbol
- persist orphan-sweep identity in Binance `withdrawOrderId` and exclude
those withdrawals from reconciliation
- incorporate PR #3686's four-hour inventory lookback and terminal
failed-deposit exclusion

No legacy aggregate same-symbol routing is retained. This supersedes
#3686's adapter-specific attribution implementations with the shared
utility.

## Simplification

The final tree is 51 changed lines smaller and 71 net lines smaller than
the previous PR head. The reduction removes one-use helpers/tests, uses
native `Object.groupBy`, compresses repeated fixtures and finalizer
setup, and narrows attribution to the configured safety boundary.

## Validation

- `yarn lint`
- `yarn typecheck`
- `RELAYER_TEST=true hardhat test test/BinanceFinalizer.ts
test/BinanceUtils.ts test/InventoryClient.RefundChain.ts` (80 passing)
- Ponytail review of the complete final diff: no actionable findings
- independent review of the complete final diff: no actionable findings
- read-only 20-day utility comparison using the credential/egress
pairing documented for this account:
- fetched 592 Binance deposits; compared all 511 Ethereum/BSC deposits
in that snapshot
- shared utility and all-receipt attribution resolved 511/511 with zero
sender mismatches
- shared utility used 324 RPC calls versus 511 receipt calls and
completed in 1.38s versus 4.59s
- two consecutive warm production-shaped attribution runs resolved
512/512 supported deposits with zero unattributed deposits
- exact final tree exits cleanly before Binance/API/RPC work when no
symbols are configured

Live validation used no authorized withdrawal symbols. No Binance or
blockchain transaction was submitted.

## Deployment

Deploy this relayer version before enabling multiple EOAs in
Configurama. The corresponding Configurama PR must remain blocked until
the exact relayer image is live.
## Problem

An expired Across intent refunds on-chain to the **deposit address
itself** — the PDA is the `depositor` of record — so the funds need a
second hop out to the committed `refundAddress`. That second hop is the
sweeper's job.

The indexer marks a v3 `intent_refund` bot-eligible (refund address
present and non-zero) and serves it to the bot, but `processExecution`'s
v3 branch only implemented `correct_transfer` and `mis_route`.
`intent_refund` hit the fall-through and was dropped at `debug` level —
silent in prod. The withdraw row stays `auto_pending` forever and
nothing alerts.

Two transfers have hit this so far, both still holding funds (recovering
those is out of scope here — they need a manual sign-withdraw, since the
indexer's 15-minute redelivery window has long closed).

## Change

- Route v3 `intent_refund` to `initiateWithdrawV3`, alongside
`mis_route`. That path was already classification-agnostic (it works off
`refundAddress` + the withdraw leaf), so nothing else in it changes.
- Same routing in the Pub/Sub service's `parseTransfer`, which mirrors
the polling dispatch.
- **Gas:** `deductGasFromRefund: false` for an `intent_refund`. Nobody
filling the intent is our failure, not a user mistake, so the user is
refunded in full and we eat the gas. Every other refund reason still
deducts.

Note the gas change means the terminal `422 GAS_EXCEEDS_REFUND` can no
longer fire for intent refunds, so dust refunds will be attempted even
when gas exceeds the amount.

## Testing

`processExecution` routing and the sign-withdraw request-mapping tests
updated (including one asserting `deductGasFromRefund: false`); service
routing test flipped from drop to withdraw. Typecheck, lint, and the
deposit-address suites pass.

Requires `ENABLE_V3_WITHDRAWALS=true` in prod for the path to fire.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This PR changes the way we calculate `limitOrderOut` before sending it
into `finalizeSwapFlows` on the HyperCore swapper contract.

Roughly, it does:
- if final funds target is SPOT, old logic applies
- if final funds target is default PERP (dex index 0), we apply logic so
that the amount we send ends up being representable in PERP decimals.
This is easier for some cases than others, see
`defaultPerpLimitOrderOut` fn (Please read comments in fn body for
explanations)
- if final funds target is anything else, we log error and skip
processing for now, since we don't know how to handle dust for that dex
destination yet, it requires manual intervention

Signed-off-by: Ihor Farion <ihor@umaproject.org>
…nly on deep backlog (#3651)

Aggressively replacing transactions can be harmful in terms of gas expenditure
and eventual transaction confirmation. Allow a short delta for the time being.

Co-authored-by: droplet-rl <284132418+droplet-rl@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
More agent hinting.

---------

Co-authored-by: droplet-rl <284132418+droplet-rl@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Paul <108695806+pxrl@users.noreply.github.com>
…paging (#3740)

Signed-off-by: Ihor Farion <ihor@umaproject.org>
…alance read (#3737)

## Problem

Scam / non-conforming ERC-20s get sent to v3 persistent deposit
addresses. The indexer sees the `Transfer` log, classifies it
`mis_route` (no `supportedInputs` lane), writes a
`deposit_address_transfer_withdraw` row at `auto_pending` and marks it
bot-eligible. The bot then dies on its defensive balance read, because
the contract does not implement `balanceOf`:

```
[warn] DepositAddressHandler#initiateWithdrawV3 → Skipping withdraw: failed to fetch deposit address balance
err: missing revert data in call exception; Transaction reverted without a reason string (data="0x", CALL_EXCEPTION)
```

Any contract can emit a `Transfer`-shaped log, so "a PDA received token
X" can be a fiction. `DepositAddressHandler` had **no failure-publish
path at all** — only `withdraw_executed` and `deposit_executed`; every
skip path logged a warn and returned. Prod confirms the aggregate
effect: **649 `executed`, 0 `failed`, 6152 `auto_pending`**. The bot
only reported good news, so every non-happy path stranded a row and the
user saw `auto-refund-pending` forever. There is no second chance: the
indexer serves each transfer once (`consumed = true`) plus a ~15-minute
replay window, so the failure window is the only opportunity to report
anything.

## What changed

Producer-side only. The indexer's `withdraw_failed` → `failed` → API
`refund-failed` path already exists with integration coverage.

- **`withdrawPayload.ts`** — `WithdrawFailedPayload` +
`buildWithdrawFailedPayload(depositMessage, reason)`. No receipt, no log
matching, cannot return `undefined`: the `erc20Transfer` lookup key
comes straight off the indexer message. Normalizes `chainId` to a number
and lowercases `txHash`, as the existing builders do.
- **`isTerminalBalanceReadError(err)`** — exported pure function:
`typeguards.isEthersError(err) && err.code ===
ethers.errors.CALL_EXCEPTION`. Gates on the ethers `code`, never the
message string.
- **`_publishWithdrawFailed`** — mirrors `_publishWithdrawExecuted`:
same gate (`ENABLE_DEPOSIT_ADDRESS_WITHDRAW_PUBLISHER`), same topic,
`across-bot-error` on publish failure, never throws.
- **Wired into the `initiateWithdrawV3` balance-read catch** — terminal
→ publish, then persist the terminal skip, then `return` as today.
Transient → unchanged warn-and-return.

## The load-bearing detail

We do **not** publish whenever the balance read throws — that would turn
every RPC hiccup into a permanent user-facing `refund-failed`:

| Signal | Meaning | Disposition |
| -- | -- | -- |
| ethers `code === CALL_EXCEPTION` on the static `balanceOf` | contract
does not implement ERC-20 `balanceOf` (or no contract at all — a
codeless address surfaces as the same code) | **terminal** → publish
`withdraw_failed` |
| `TIMEOUT` / `SERVER_ERROR` / `NETWORK_ERROR` / rate limit, or any
non-ethers throw (missing provider, base58 conversion) | our node, not
the token | **transient** → today's warn + return, retried next poll |

Ordering: publish **before** persisting the terminal skip, because the
Redis write can throw and a lost failure event is exactly what this
fixes. The persisted skip (`terminallySkippedWithdrawKeys`, same set the
422 path uses) then caps this at one publish per transfer — the replay
window will not re-read the balance.

The reason string lands verbatim in `metadata.failureReason`: `balance
read reverted: token 0x… is not a conforming ERC-20 on chain 8453`.

## Decisions worth a reviewer's attention

- **`eth_getCode` probe dropped.** The ticket listed it as a second
terminal signal, but `balanceOf` on a codeless address already raises
`CALL_EXCEPTION`, and a contract that exists but reverts is terminal too
— so `getCode` cannot flip a verdict. Skipped: one RPC per failure, and
the discriminator stays a pure function. Add it if a provider turns up
that reports reverts as `SERVER_ERROR`.
- **No new config key.** Reuses the existing withdraw gate and topic,
which are already on in prod, so the fix is live when the image ships.
- **v3 only.** The v1 `initiateWithdraw` catch is byte-identical but
deliberately untouched.
- **Terminal 422 in `_getSignedWithdrawV3` still unpublished.** It
strands rows the same way and is one more call site on the same
publisher — decided separately.
- The 6152 pre-existing stranded rows are past the replay window; this
only helps future transfers.

## Wire contract

Shape is locked by the consumer's `isDepositAddressExecutionMessage`
(`indexer/packages/indexer/src/pubsub/DepositAddressExecutionConsumer.ts`).
A payload that fails that guard is ack'd and dropped with a warn, so a
shape mistake is **silent** — verified field-by-field against the guard
(numeric `chainId`/`blockNumber`/`logIndex`, string `txHash`/`reason`,
and no execution-tx coordinates: there is no settlement tx).

```jsonc
{
  "type": "withdraw_failed",
  "data": {
    "erc20Transfer": { "chainId": 8453, "blockNumber": 123, "txHash": "0x…", "logIndex": 4 },
    "reason": "…"
  }
}
```

## Tests

`83 passing`; `tsc --noEmit`, eslint and prettier clean.

- `buildWithdrawFailedPayload`: exact wire shape, plus string→number
`chainId` and tx-hash lowercasing.
- `isTerminalBalanceReadError`: synthetic `CALL_EXCEPTION` → terminal;
`TIMEOUT` / `SERVER_ERROR` / `NETWORK_ERROR` / `UNPREDICTABLE_GAS_LIMIT`
and non-ethers throws → transient.
- `initiateWithdrawV3 balance check`: a reverting read publishes exactly
one `withdraw_failed` and persists the skip key; a transient one
publishes nothing and persists nothing.

Docs updated in the same change — `src/deposit-address/README.md` and
`docs/deposit-address-withdraw-pubsub.md` both claimed failure events
were not produced, and both named the stale
`DepositAddressWithdrawConsumer` instead of the live
`DepositAddressExecutionConsumer`.

## Verification after deploy

```sql
SELECT w.status, count(*) AS rows,
       count(*) FILTER (WHERE w.metadata->>'failureReason' IS NOT NULL) AS with_failure_reason,
       max(w."updatedAt") AS last_write
FROM evm.deposit_address_transfer_withdraw w
GROUP BY w.status ORDER BY rows DESC;
```

Wired correctly ⇒ a `failed` bucket appears with `with_failure_reason`
equal to its row count, and those rows carry a **null**
`transactionHash` (the consumer nulls the tx columns on failure).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ithdraw_failed on refund 422 (#3744)

## Summary

A 1 USDC deposit to a PDA on Ethereum couldn't be swept — Circle's live
CCTP fee exceeded the on-chain fee cap at that amount — and the bot
retried the execute endpoint indefinitely instead of refunding, leaving
the funds parked until a manual refund. This PR routes such deposits to
the refund path immediately, and when the refund itself is impossible
(e.g. 1 USDC can't cover mainnet gas), reports a terminal failure so the
user sees `refund-failed` instead of a permanently pending state.

## Technical details

- `_getExecuteTx` now classifies on a `TERMINAL_EXECUTE_ERROR_CODES` set
— `AMOUNT_BELOW_MINIMUM` plus `AMOUNT_TEMPORARILY_UNSWEEPABLE` — reusing
the existing refund-only routing (same-tick refund, Redis-persisted
marker, handover-safe). Treating the unsweepable 400 as terminal
deliberately deviates from the API's "400 = retry" contract; the
trade-off is documented at the constant.
- `_getSignedWithdrawV3`'s terminal 422 branch now publishes
`withdraw_failed` before persisting the skip, with the API's error code
as the reason (`GAS_EXCEEDS_REFUND` / `UNPRICEABLE_REFUND_TOKEN` / …,
fallback `SIGN_WITHDRAW_REJECTED`). `signWithdrawDepositAddressV3`
switched to `_postOrThrowWithErrorCode` so the code survives to the
handler. No indexer change needed.
- Tests for both paths; README + Pub/Sub contract doc updated.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…in blocks (#3750)

On chains with fixed slot times, a block delivered late within its own
slot was published after that slot's attestation deadline and is
materially more likely to be replaced at the same height by the next
proposer. The existing deposit confirmation gate compares block heights,
so a same-height replacement is invisible to it.

A safe default threshold is implemented for mainnet but can be overriden
by config.

---------

Co-authored-by: droplet-rl <284132418+droplet-rl@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Allows the Disputer config to be aligned with existing balance monitoring
& alerting.

---------

Co-authored-by: droplet-rl <284132418+droplet-rl@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
…#3756)

Circle offers Fast Transfer only from source chains where it beats that
chain's standard attestation time. From any other source the burn is
accepted on-chain and then silently attested as standard, so we spend
two API calls (`fastBurnAllowance` + `minTransferFees`) to arrive at the
standard transfer we'd have sent anyway — and log "using fast mode" for
a transfer that wasn't.

---------

Co-authored-by: droplet-rl <284132418+droplet-rl@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Paul <108695806+pxrl@users.noreply.github.com>
`rpcRateLimited` computed its exponential backoff in milliseconds, but
`delay()` takes seconds — so the requested sleep was 1000x the intended
value (attempt 0 asked for ~1000-2000s rather than 1000-2000ms).

Thanks to Saurav Krish for the initial identification of the ms/s
mismatch.

That sleep never actually elapsed, which is why this didn't show up as a
hang: ethers races the entire fetch — throttle callback included —
against the connection timeout of 60s. So any 429 rejected with
`TIMEOUT` after `NODE_TIMEOUT` (60s default) and the retry never
fired, leaving the backoff path effectively dead. Each 429 also
leaked an uncleared multi-minute timer.

Fixed by computing the backoff in seconds where the value is produced,
rather than converting at the call site — no ms/s round-trip left to get
wrong.

---------

Co-authored-by: droplet-rl <284132418+droplet-rl@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
…hreshold (#3766)

The nonce-backlog threshold reads a deep pending queue as evidence that
the transaction at the confirmed nonce is stuck, and replaces it. That
holds for a single-process bot, but not for a service that fans one
signer out over concurrent requests — there a deep queue is ordinary
load, so unrelated submissions bid to evict each other at the same nonce
and none of them confirms. The depth also grows while that is happening,
so it is self-sustaining.

Raises the threshold to 10 on mainnet via a chain-keyed default (same
idiom as `CONFIRMATION_TIMEOUTS_MS`), leaving 4 elsewhere. Env overrides
keep precedence.

---------

Co-authored-by: droplet-rl <284132418+droplet-rl@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
…3678)

Part of #3663 — PR 3 of nine. **Stacked on #3670**, which is stacked on
#3668. Bases retarget automatically as each merges.

## What

The two Redis keys the design turns on, plus the receipt classifier.
`src/deposit-address-service/transferState.ts`.

```
deposit-address:lock:<transferId>    expiring, SET NX, uuid token generated inside the store
deposit-address:state:<transferId>   broadcast_pending (never expires) | 3 terminal statuses (90d)
```

They are **separate because their lifetimes are opposites**: the lock
must expire so a dead consumer does not block a transfer forever; the
state must not, because a `broadcast_pending` record has to outlive
anything that could still land on-chain. Merging them reintroduces the
problem the design exists to remove.

Built entirely on `acquireLock` / `releaseLock` / `get` / `set` / `del`,
all already on `RedisCacheInterface` — **no new Redis primitive**, which
is what #3648 was criticised for.

## Guards worth reviewing

- **`clearRevertedBroadcast(id, expectedTxHash)`** deletes only if the
record is *still* that transaction. A stale reconciliation of `h1` must
not delete a newer pending `h2` or a terminal outcome — that would
unblock a transfer mid-sweep, which is the exact failure this project
exists to prevent. Read-then-check is sufficient because callers hold
the lock; it would need to be atomic only if that stopped being true.
- **`recordBroadcast` refuses to overwrite a terminal outcome** — the
one transition that loses funds-safety information.
- **Every write throws unless Redis replies `"OK"`.** `set` can answer
`undefined`, and awaiting a confirmation while believing a broadcast
hash is durable is the unrecoverable direction.
- **A present-but-unparseable record throws** rather than reading as
absent: it may describe a transfer already swept, so the transfer stays
blocked. `CorruptTransferStateError` is retriable but distinct from a
Redis outage — it clears only when an operator repairs or deletes the
key.
- **The lock token never crosses the boundary.** `acquireLock(id)`
generates a uuid and returns a `TransferLock` handle, so no caller can
pass a `messageId` or reuse a token. A message id could not serve
anyway: Pub/Sub redelivers the same message.

## `classifyReceipt` returns facts, not actions

`"confirmed" | "reverted" | "unresolved"`. Whether each means ACK, NACK,
clear or persist is retry policy and belongs with the orchestration that
also holds the lock — so this file has no opinion on it.

A receipt with **no explicit `status === 0` reads as confirmed**. ethers
leaves `status` undefined on some chains; guessing confirmed can strand
a deposit for manual recovery, whereas guessing reverted would
re-broadcast one that already landed. Only the first is reversible.

## Deliberately skipped

- **`schemaVersion`.** Superstruct `type()` already tolerates unknown
keys, so today's records stay readable. Add one when there is a second
version — a version number does not help with the case it would not
cover anyway (a field whose *meaning* changes).
- **A full transition table.** Only the pending-over-terminal row is
enforced. Reaching the other six requires a caller to bypass the state
read every path begins with.
- **Wiring.** Same reason as PR 2: nothing to lock or record until the
execution paths exist, and a handler that ACKed without executing would
discard work.

## Testing

`RELAYER_TEST=true yarn hardhat test "test/DepositAddressService*.ts"` →
**79 passing** (20 new).

Covers all four `classifyReceipt` branches, `SET NX` exclusivity,
token-checked release, internally-generated and differing tokens, the
lock TTL, round-trip of every status with its TTL, idempotent terminal
rewrite, refusal to downgrade a terminal, unacknowledged writes, corrupt
and out-of-range records, and that a stale clear cannot delete either a
newer pending hash or a terminal outcome.

`yarn build` reports only the three pre-existing
`poolRebalanceLeafCount` errors. `yarn lint` clean.

Note this touches `errors.ts` (three new classes) and the module README,
both introduced in #3668.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Part of #3663. **Stacked on #3678** — review that first; this diff is
against its branch.

The first PR here that moves funds: guards, the execute request,
broadcast, resolving the
outcome against the chain, terminal state, lock release.

*Review focus: guard-for-guard parity with `initiateDepositV3`.*
`DepositAddressHandler.ts` is
untouched and `EXECUTION_ENABLED` defaults `false`, so nothing here can
move funds until PR 7.

**Two optional fields on `AugmentedTransaction`**, both defaulting to
today's behaviour so every
existing caller is byte-identical:
- `onBroadcast` — records `broadcast_pending` the moment a hash exists,
*before* the confirmation
wait, and again on every hash change. `sendAndConfirmTransaction` can't
do this: it submits and
  confirms in one call and returns `undefined` with no hash on failure.
- `maxTries` — bounds that wait. The hard-coded default of 10 is
`M(M+1)/2` = 55 waits, ~22 min on
  mainnet, outliving both the deadline and the lock. This uses 4.

**The outcome comes from the chain, not the exception.** `submit()`
flattens revert, exhausted
retries and RPC failure into one untyped `Error`, so
`resolvePendingTransaction` reads the receipt
instead. A revert is the only case that clears the record; every "no
receipt" reason is retained.

**Lock TTL 600s → 900s**, asserted at startup as `lockTtl >= deadline +
confirmBudget` — the
deadline check bounds when a broadcast *begins*, not when its
confirmation ends.

**Deliberately not here:** the `MetadataEmitted` pre-submission scan,
nonce bookkeeping (that's
`TransactionClient`'s), the send-adjacent lock check, and address/hash
validation. Each is reasoned
out in #3663's Scope, with the residuals stated.

## Rebased onto #3678, and one behavioural change

Master's #3736 landed while this was open and changed the service's own
message contract:
`intent_refund` now routes to withdraw instead of being dropped.
Resolving textually would have
reinstated the drop and regressed a production fix, so master's
behaviour wins and this branch's
shape wins — the classification passes through untranslated and the
handler diverts on
`!== "correct_transfer"`. `assertActionableClassification` is deleted:
with all three
classifications actionable it had no work left.

Note #3651 also landed, which appends behind in-flight nonces instead of
colliding on `"latest"`.
That materially narrows the nonce race described in #3663's Scope 5 —
I'll correct that entry.

## Verification

`yarn tsc --build --force`, `yarn lint`, **164 passing** across the
service and `TransactionClient`
suites. `DepositAddressService.deposit.ts` drives the real Express
boundary, faking only the chain,
quote-api and submission client — and its fake invokes `onBroadcast`
where the real client does.
Several tests were verified to fail without their fix rather than merely
pass with it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: droplet-rl <284132418+droplet-rl@users.noreply.github.com>
Part of #3663 — PR 5 of the standalone deposit-address service. Stacked
on #3701.

Executes the v3 refund withdrawal: the path for a `mis_route`, and for a
`correct_transfer` the execute endpoint rejected as
`AMOUNT_BELOW_MINIMUM`. Replaces PR 4's two placeholder throws at the
lines they sat on, so the deposit lock is held across both actions via
`processUnderLock`'s existing `try/finally`.
`WithdrawRouteNotImplementedError` disappears with them, taking the last
identifier carrying the removed `route` vocabulary.

**Review focus: one lock held across both actions; terminal 422
handling.**

## What is genuinely new

- `executeWithdraw` in `depositHandler.ts`, ported guard-for-guard from
`initiateWithdrawV3` per the issue's parity matrix: the
`ENABLE_V3_WITHDRAWALS` gate (same env var the polling bot reads, NACK
while off), **EVM-only** namespaces (stricter than the deposit path —
`assertSupportedNamespace` allows `tron` on TVM chains, so it could not
be reused), the withdraw leaf materials check, and a smaller response
assertion (`assertValidWithdrawResponse`: signed `chainId` against the
**refund** chain `erc20Transfer.chainId`, and the now+60s signature
deadline — `assertValidExecuteResponse` deliberately does not carry
over).
- Terminal classification of a sign-withdraw failure **on the HTTP
status alone**, exactly as production's `_getSignedWithdrawV3` does
(`isHttpError(err) && err.status === 422`): persist `withdraw_failed`,
ACK. Everything else NACKs. No client change; `_postOrThrow` discards
the API's error code, so `withdraw_failed.code` becomes `optional(...)`
and is unset — every existing PR 3 state test passes unchanged.
- `deductGasFromRefund: true`, deliberately unlike v1's full-amount
refund — not to be unified in the v1 PR.

## Reused unchanged

The lock, both state reads, `assertSupportedOriginChain` (already runs
on `erc20Transfer.chainId` before routing), canonicality-then-balance in
that order and for the same reason, `onBroadcast` + `maxTries`, the
pending-write retry, and `resolvePendingTransaction` (whose `operation:
"withdraw"` mapping already existed). `broadcast()` is parameterised
over `{operation, to, data, value, message, mrkdwn}` rather than
duplicated. One adjustment there: a confirmed **withdraw** is not
expected to carry the `MetadataEmitted` provenance event, so the
missing-metadata warning is now gated to deposits.

## Not in this PR

`withdraw_executed` is recorded but **not published** — lifecycle
publishing and its recovery are PR 6, which lands before PR 7 enables
execution anywhere.

## Verification

- `yarn tsc --build --force` clean; `yarn lint` clean.
- 190 passing across the service suites + `TransactionClient` (163 on
the base branch; +27 here).
- Every new guard and branch was red-checked by reintroducing the bug:
the metadata gate, `deductGasFromRefund`, the signed-chainId check, the
`operation` label, 422-only terminal classification, the code-gated
below-minimum fallthrough, the lock held across both actions (the fake
API records the lock token it observed inside each call), the withdraw
gate, the leaf `kind` filter, EVM-only strictness, and
canonicality-before-balance ordering — each failed exactly the expected
tests, then passed on revert.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…3772)

`matchL2EthDepositAndWrapEvents` consumed one wrap event per L2 ETH
deposit. Wrapping sweeps the relayer's whole ETH balance down to a gas
reserve, so one wrap can convert several deposits — e.g. when a
rebalance bridges in more than one leg.

Co-authored-by: droplet-rl <284132418+droplet-rl@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
## Problem

A gasless API message carries the deposit input amount **twice**:

- `witness.data.inputAmount` — the top-level field in the periphery's
`DepositData` struct
- `witness.data.baseDepositData.inputAmount` — a mirror of the same
quote value

Only the top-level one is part of the struct.
`SpokePoolPeripheryInterface.BaseDepositData` has no `inputAmount` —
it's hoisted out because that struct is shared with the swap flow, where
the amount isn't known until the swap executes:

> `// Params we'll need caller to pass in to specify an Across Deposit.
The input token will be swapped into first before submitting a bridge
deposit, which is why we don't include the input token amount as it is
not known until after the swap.`

So only the top-level value is covered by the depositor's EIP-712
signature (the API generates the typed data straight off the ABI), and
only it reaches the chain. The mirror is an unsigned API-side copy.

The two code paths read **different fields**:

| path | reads | location |
|---|---|---|
| origin deposit tx | top-level (signed) | `GaslessUtils.ts`
`toContractDepositData` |
| immediate fill relay data | mirror (unsigned) | `GaslessUtils.ts`
`buildSyntheticDeposit` |

When they disagree we submit the deposit with one amount and the fill
with the other. The fill then hashes to a relay with no deposit behind
it, so:

1. the fill can never be repaid — the dataworker has no matching deposit
to refund against, and
2. the depositor's real deposit is left unfilled until it expires at
`fillDeadline`.

Not hypothetical — this happened on 2026-09-10 (Arbitrum → Optimism,
`depositId 570997…70228`). The deposit landed with `inputAmount = 1`,
matching both the signed value in the calldata and the on-chain ERC-3009
transfer, while the fill went out with `inputAmount = 2`. Deposit
`internalHash 0xe624d7…` has no fill; fill `internalHash 0xb9ee3a…` has
no deposit.

The non-immediate path is unaffected: it builds relay data from the
actual on-chain `FundsDeposited` event, so it can't diverge.

## Change

Discard such messages in `_queryGaslessApi` with a `warn`, alongside the
existing integrator and address filtering, so they never enter the state
machine — no deposit submitted, no fill attempted.

- Runs **after** the integrator and address filters, so we don't warn
about messages this instance wouldn't have handled anyway.
- Compares with `BigNumber.from(...).eq(...)` rather than string
equality, so formatting differences aren't false positives; anything
unparseable counts as a mismatch.
- An **absent** mirror is also treated as a mismatch. It can
legitimately be missing (the API's submit schema never declares the
field), and today that would reach `BigNumber.from(undefined)` inside
`buildSyntheticDeposit` and throw mid-batch, taking out the whole poll
tick. Happy to relax this to "only reject on an actual disagreement" if
preferred.
- `swapAndBridge` messages are **exempt** — their input amount is only
known once the swap executes, so there is no signed top-level amount to
compare against.

## Testing

`yarn typecheck` clean; `eslint` + `prettier` clean on changed files.

`test/GaslessRelayer.ts` — 56 passing, including 5 new cases under
`input amount mismatch filter`: agreeing amounts kept; disagreeing
amounts discarded with a `warn`; absent mirror discarded; numeric rather
than lexical comparison; `swapAndBridge` exempt. `test/GaslessUtils.ts`
+ `test/GaslessRelayerConfig.ts` — 41 passing.

Worth knowing: the existing fixtures set `inputAmount:
baseDepositData.inputAmount`, so nothing in the suite could produce this
divergence before. The new tests override the top-level value
explicitly.

## Follow-ups (deliberately not in this PR)

This stops us acting on a bad message, but doesn't stop the bad message
existing. Worth considering separately in `quote-api`:

- reject on submit unless `baseDepositData.inputAmount` is absent or
equals the top-level value
- the witness schemas use superstruct `type()` (non-strict) checked via
`assert()`, which neither rejects nor strips undeclared keys — so any
unsigned field can ride along into Redis and out to relayers
- ideally stop emitting the mirror at all; it has no contract
counterpart

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: droplet-rl <284132418+droplet-rl@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.