Repository navigation
Sync with upstream repo - #149
github-actions[bot] wants to merge 738 commits into
Conversation
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.
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughLarge 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. ChangesCore Refactor and Features
Estimated code review effort Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
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 /> [](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 & 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 /> [](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 "fix: error should be thrown (<a href="https://redirect.github.com/dcodeIO/protobuf.js/issues/1817">#1817</a>)" (<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>
Minor robustness improvements.
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>
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>
Merge latest changes from upstream repo
Summary by CodeRabbit
New Features
Infrastructure & Documentation
Removals