Skip to content

feat(BundleDataClient): refund deposits to abandoned destinations (UMIP-179)#1477

Draft
droplet-rl wants to merge 1 commit into
masterfrom
droplet/abandoned-destination-deposit-refunds
Draft

feat(BundleDataClient): refund deposits to abandoned destinations (UMIP-179)#1477
droplet-rl wants to merge 1 commit into
masterfrom
droplet/abandoned-destination-deposit-refunds

Conversation

@droplet-rl

Copy link
Copy Markdown
Contributor

Draft — companion implementation to UMAprotocol/UMIPs#631. Compiles clean (cjs/esm/types); needs dedicated test coverage + review before merge (this is consensus/settlement code). Inert until the ConfigStore VERSION gate is set, so it does not change current bundle output.

Problem

A Deposit whose destinationChainId is not an active protocol chain — never onboarded (typo / unsupported id) or currently in DISABLED_CHAINS (e.g. Scroll) — is accepted on-chain (SpokePool._depositV3 does not validate destinationChainId) but then silently dropped by the dataworker: loadData only iterates allChainIds (active chains), so these deposits never enter the origin/destination loops, and expired-deposit refunds are defined against a destination clock that doesn't exist for them. The input funds sit on the origin SpokePool indefinitely. Hit in production (Base→Scroll via a third-party integrator; funds stranded, manual admin recovery required).

Change

After the existing deposit/fill/expiry processing in loadData, add an origin-clock refund pass for abandoned-destination deposits (gated on ABANDONED_DESTINATION_REFUND_CONFIG_STORE_VERSION):

  • For each origin chain, scan deposits whose destinationChainId ∉ allChainIds.
  • Apply the same forward-only gate as expired deposits, but on the origin bundle window: refund only if fillDeadline falls within [originStart, originEnd]. Older deadlines are assumed resolved in a prior bundle → no retroactive sweep.
  • Classify using on-chain event timestamps, never quoteTimestamp (so it's independent of depositQuoteTimeBuffer, which may be raised to ~1 week):
    • orphan — destination ∉ CHAIN_ID_INDICES at the fillDeadline HubPool block. CHAIN_ID_INDICES is append-only ⇒ absent at the deadline ⇒ absent across the whole window ⇒ no SpokePool ever existed ⇒ no fill.
    • disabled — destination ∈ DISABLED_CHAINS at both the deposit block.timestamp and the fillDeadline HubPool blocks. Combined with the re-enablement bundle-range rule (UMIP PR), disabled-interval fills are never bundled, so the depositor refund can't be double-paid.
  • Refund via the existing updateExpiredDepositsV3 → flows into the origin relayer-refund root. No new leaf type, no contract change.

Safety

No destination FillStatus check (the chain is unresolvable) — safety is structural: an orphan destination never had a SpokePool, and a disabled-throughout destination's fills are never in any bundle range. Membership resolves from deposit/fillDeadline block timestamps via HubPoolClient.getBlockNumber(); near a DISABLED_CHAINS boundary the origin→mainnet resolution should be taken conservatively (design note in the UMIP).

Test plan (TODO before un-drafting)

  • Extend the BundleDataClient loadData suite (MockConfigStoreClient version ≥ gate) to assert:
    • Deposit to a never-onboarded destination → refunded to depositor on origin when fillDeadline in window; not before.
    • Deposit to a DISABLED_CHAINS destination disabled at both deposit & deadline → refunded; disabled only after deposit (active at deposit) → not refunded (double-pay guard).
    • Deposit whose fillDeadline predates the origin window → not refunded (forward-only).
    • Version below gate → no behavior change.
  • Confirm ABANDONED_DESTINATION_REFUND_CONFIG_STORE_VERSION is set to the ratified activation VERSION.

🤖 Generated with Claude Code

…IP-179)

A deposit whose destinationChainId is not an active protocol chain — never
onboarded, or in DISABLED_CHAINS across its fillable window — is currently
filtered out of bundle consideration (it never enters the origin/destination
loops, which iterate only `allChainIds`) and stranded on the origin SpokePool
indefinitely, because expired-deposit refunds are defined against the
destination clock which doesn't exist for these deposits.

This adds an origin-clock refund for such "abandoned destination" deposits,
per UMIP-179 "Finding Abandoned-Destination Deposits":

- Iterate each origin chain's deposits whose destination is not in the active
  set, gated by the same forward-only rule as expired deposits (fillDeadline
  within the origin bundle block range).
- Classify using on-chain event timestamps, never quoteTimestamp, so the rule
  is independent of depositQuoteTimeBuffer:
  * orphan: destination absent from CHAIN_ID_INDICES at the fillDeadline
    HubPool block (append-only => absent for the whole window => no fill).
  * disabled: destination in DISABLED_CHAINS at BOTH the deposit block
    timestamp and the fillDeadline (disabled across the window; combined with
    the re-enablement bundle-range rule, no repayable fill can be double-paid).
- Refund matching deposits to the depositor on origin via updateExpiredDepositsV3.

Gated behind ABANDONED_DESTINATION_REFUND_CONFIG_STORE_VERSION so proposers
switch deterministically; the placeholder version keeps it inert until the
UMIP-179 clause is ratified.

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

1 participant