Skip to content

feat: Index async vault cancellation lifecycle #257

@mustermeiszer

Description

@mustermeiszer

Summary

Add indexing support for the full async vault cancellation lifecycle. Currently the indexer tracks deposits and redeems through request → approve → issue → notify → claim, but completely ignores cancellation flows. Users who cancel requests have no visibility into cancellation state via the API.

Background

Cancellation Flow (Smart Contracts)

Cancellations follow two paths depending on timing:

Immediate cancellation — When the user's request was placed in the current epoch (no approval has happened yet), the hub can directly cancel and return assets/shares immediately via a FulfilledDepositRequest/FulfilledRedeemRequest callback with cancelledAmount > 0 and fulfilledAmount = 0.

Queued cancellation — When the user's request was placed in a prior epoch that has already been approved (but not yet claimed), the cancellation is queued (isCancelling = true on the hub). It executes later during notifyDeposit/notifyRedeem, producing a combined callback that may contain both fulfilledAmount > 0 AND cancelledAmount > 0 (partial fulfillment + partial cancellation).

Examples

Ex 1: No cancellation queued
State: 500 pending (200 effectively approved), 1000 queued, no cancel

  • Claim: 200 assets fulfilled, pending updated to 500 - 200 + 1000 = 1300
  • Hub sends FulfilledDepositRequest(fulfilled=200, cancelled=0)

Ex 2: Cancellation queued
State: 500 pending (200 effectively approved), 1000 queued, cancel queued

  • Claim: 200 assets fulfilled, cancelled = 500 - 200 + 1000 = 1300
  • Pending set to 0, total pending reduced by cancelled 300
  • Hub sends FulfilledDepositRequest(fulfilled=200, cancelled=1300)

State Machine (Deposit Cancel)

[No Request]
    │ requestDeposit()
    ▼
[Pending Deposit]
    │ cancelDepositRequest()
    ▼
[Cancel Pending]  ←── pendingCancelDepositRequest = true
    │
    ├── Hub: Immediate? ──► FulfilledDepositRequest(0, 0, cancelledAmount)
    │                            │
    └── Hub: Queued? ──► isCancelling=true, NO callback
                              │
                              ▼ (later) notifyDeposit()
                         FulfilledDepositRequest(fulfilled, shares, cancelledAmount)
    │
    ▼
[Cancel Claimable]  ←── claimableCancelDepositRequest += cancelledAssets
    │ claimCancelDepositRequest()
    ▼
[Claimed]  ←── assets returned to user

Redeem cancellation is analogous (shares instead of assets).

Current State in Indexer

What exists (unused)

  • InvestorTransactionType enum already includes DEPOSIT_REQUEST_CANCELLED and REDEEM_REQUEST_CANCELLED
  • InvestorTransactionService has cancelDepositRequest() and cancelRedeemRequest() methods — never called by any handler
  • CrosschainMessageService decodes CancelDepositRequest/CancelRedeemRequest message types
  • Vault ABI includes all 6 cancel events

What's missing

  • No event handlers for any of the 6 vault cancel events
  • isQueuedCancellation flag on UpdateDepositRequest/UpdateRedeemRequest is ignored by handlers
  • No schema fields for cancel state on VaultInvestOrder, VaultRedeemOrder, PendingInvestOrder, PendingRedeemOrder
  • No new InvestorTransactionType values for cancel-claimable and cancel-claimed phases

Implementation

1. Schema Changes (ponder.schema.ts)

New InvestorTransactionType values

Add 4 new enum values to distinguish cancel lifecycle phases:

CANCEL_DEPOSIT_CLAIMABLE   // cancelled deposit assets are ready to claim
CANCEL_DEPOSIT_CLAIMED     // user claimed cancelled deposit assets
CANCEL_REDEEM_CLAIMABLE    // cancelled redeem shares are ready to claim
CANCEL_REDEEM_CLAIMED      // user claimed cancelled redeem shares

The existing DEPOSIT_REQUEST_CANCELLED / REDEEM_REQUEST_CANCELLED continue to represent the initiation of a cancel request.

VaultInvestOrder — add fields

Field Type Description
pendingCancelDeposit boolean (default false) Whether a cancel deposit request is pending
claimableCancelDepositAmount bigint (default 0n) Assets available to claim from cancelled deposit

VaultRedeemOrder — add fields

Field Type Description
pendingCancelRedeem boolean (default false) Whether a cancel redeem request is pending
claimableCancelRedeemAmount bigint (default 0n) Shares available to claim from cancelled redeem

PendingInvestOrder — add field

Field Type Description
isQueuedCancellation boolean (default false) Whether this pending order has a queued cancellation on the hub

PendingRedeemOrder — add field

Field Type Description
isQueuedCancellation boolean (default false) Whether this pending order has a queued cancellation on the hub

2. New Event Handlers (vaultHandlers.ts)

vault:CancelDepositRequest

Event: CancelDepositRequest(controller indexed, requestId indexed, sender)
When: User initiates deposit cancellation on vault

Actions:

  1. Create InvestorTransaction with type DEPOSIT_REQUEST_CANCELLED (method already exists)
  2. Update VaultInvestOrder.pendingCancelDeposit = true

Do NOT reduce requestedAssetsAmount here — the actual cancelled amount is unknown until the hub processes it and sends back the callback.

vault:CancelDepositClaimable

Event: CancelDepositClaimable(controller indexed, requestId indexed, assets)
When: Hub processed the cancellation, assets are now claimable

Actions:

  1. Create InvestorTransaction with type CANCEL_DEPOSIT_CLAIMABLE (new method needed)
  2. Update VaultInvestOrder:
    • pendingCancelDeposit = false
    • claimableCancelDepositAmount += assets
    • requestedAssetsAmount -= assets (the cancelled portion is no longer "requested")

vault:CancelDepositClaim

Event: CancelDepositClaim(controller indexed, receiver indexed, requestId indexed, sender, assets)
When: User claims the refunded assets

Actions:

  1. Create InvestorTransaction with type CANCEL_DEPOSIT_CLAIMED (new method needed)
  2. Update VaultInvestOrder:
    • claimableCancelDepositAmount -= assets
  3. saveOrClear() — delete if all amounts are zero

vault:CancelRedeemRequest

Event: CancelRedeemRequest(controller indexed, requestId indexed, sender)
When: User initiates redeem cancellation

Actions:

  1. Create InvestorTransaction with type REDEEM_REQUEST_CANCELLED (method already exists)
  2. Update VaultRedeemOrder.pendingCancelRedeem = true

vault:CancelRedeemClaimable

Event: CancelRedeemClaimable(controller indexed, requestId indexed, shares)
When: Hub processed the cancellation, shares are now claimable

Actions:

  1. Create InvestorTransaction with type CANCEL_REDEEM_CLAIMABLE (new method needed)
  2. Update VaultRedeemOrder:
    • pendingCancelRedeem = false
    • claimableCancelRedeemAmount += shares
    • requestedSharesAmount -= shares

vault:CancelRedeemClaim

Event: CancelRedeemClaim(controller indexed, receiver indexed, requestId indexed, sender, shares)
When: User claims refunded shares

Actions:

  1. Create InvestorTransaction with type CANCEL_REDEEM_CLAIMED (new method needed)
  2. Update VaultRedeemOrder:
    • claimableCancelRedeemAmount -= shares
  3. saveOrClear()

3. Hub-side Handler Updates (batchRequestManagerHandlers.ts)

UpdateDepositRequest handler (line 26)

The isQueuedCancellation (pendingCancellation in V3) field is currently destructured but ignored. When this flag is true:

  1. Update PendingInvestOrder.isQueuedCancellation = true
  2. Update EpochOutstandingInvest to reflect the queued cancellation state

When a subsequent UpdateDepositRequest arrives with isQueuedCancellation = false and the pending order previously had isQueuedCancellation = true, clear the flag.

UpdateRedeemRequest handler

Same pattern — propagate isQueuedCancellation to PendingRedeemOrder.

4. Service Changes

VaultInvestOrderService — add methods

  • cancelDepositRequest() — sets pendingCancelDeposit = true
  • cancelDepositClaimable(assets) — sets pendingCancelDeposit = false, adds to claimableCancelDepositAmount, reduces requestedAssetsAmount
  • cancelDepositClaim(assets) — reduces claimableCancelDepositAmount
  • Update saveOrClear() to also check claimableCancelDepositAmount === 0n and pendingCancelDeposit === false

VaultRedeemOrderService — add methods

  • cancelRedeemRequest() — sets pendingCancelRedeem = true
  • cancelRedeemClaimable(shares) — sets pendingCancelRedeem = false, adds to claimableCancelRedeemAmount, reduces requestedSharesAmount
  • cancelRedeemClaim(shares) — reduces claimableCancelRedeemAmount
  • Update saveOrClear() similarly

InvestorTransactionService — add methods

  • cancelDepositClaimable() — type CANCEL_DEPOSIT_CLAIMABLE
  • cancelDepositClaimed() — type CANCEL_DEPOSIT_CLAIMED
  • cancelRedeemClaimable() — type CANCEL_REDEEM_CLAIMABLE
  • cancelRedeemClaimed() — type CANCEL_REDEEM_CLAIMED

PendingInvestOrderService / PendingRedeemOrderService

  • Add updateQueuedCancellation(isQueued: boolean) method

5. Deprecation: OutstandingInvest / OutstandingRedeem

These entities are already marked DEPRECATED. Cancel events should not be added to them. Maintain existing behavior only (no new cancel fields).

Key Design Decisions

Why not reduce requestedAssetsAmount on CancelDepositRequest?

The actual cancelled amount is unknown at the time of the request. If the cancel gets queued, the user may receive partial fulfillment + partial cancellation. The definitive amounts only arrive via CancelDepositClaimable.

Why track isQueuedCancellation on PendingInvestOrder?

This allows the frontend to distinguish between:

  1. Normal pending request (waiting for approval)
  2. Pending request with queued cancellation (will be cancelled when next notifyDeposit runs)

Without this flag, the frontend can only see pendingCancelDeposit = true on the spoke side, which doesn't indicate whether the hub has processed it yet.

Combined fulfillment + cancellation callbacks

The DepositClaimable and CancelDepositClaimable events fire from the same transaction when a combined callback arrives. The handlers must work correctly when both fire in sequence — the requestedAssetsAmount must be reduced by both the fulfilled amount (in DepositClaimable handler → claimableDeposit()) and the cancelled amount (in CancelDepositClaimable handler → cancelDepositClaimable()).

Future: Hub-Side Cancellations (PR #158)

A forthcoming change (protocol-internal#158) will allow the BatchRequestManager to initiate cancellations directly from the hub side (not requiring user initiation via cancelDepositRequest).

Design considerations:

  • The spoke-side events (CancelDepositClaimable, CancelDepositClaim) will still fire when the user claims, so the spoke-side handlers above remain valid
  • The CancelDepositRequest event on the vault may NOT fire in this flow (since the user didn't initiate it), so the pendingCancelDeposit flag may transition directly from false to receiving a CancelDepositClaimable event
  • The VaultInvestOrder handlers should handle this gracefully — cancelDepositClaimable() should work regardless of whether pendingCancelDeposit was set
  • New hub-side events (if any) will need additional handlers

Files to Modify

File Changes
ponder.schema.ts Add 4 enum values, add fields to VaultInvestOrder, VaultRedeemOrder, PendingInvestOrder, PendingRedeemOrder
src/handlers/vaultHandlers.ts Add 6 new cancel event handlers
src/handlers/batchRequestManagerHandlers.ts Handle isQueuedCancellation flag in updateDepositRequest and updateRedeemRequest
src/services/VaultInvestOrderService.ts Add cancel methods, update saveOrClear
src/services/VaultRedeemOrderService.ts Add cancel methods, update saveOrClear
src/services/InvestorTransactionService.ts Add 4 new cancel transaction type methods
src/services/PendingInvestOrderService.ts Add updateQueuedCancellation method
src/services/PendingRedeemOrderService.ts Add updateQueuedCancellation method

Verification

  1. Deploy to a testnet fork and trigger a deposit cancellation in both immediate and queued scenarios
  2. Query the GraphQL API to verify:
    • InvestorTransaction records are created for each cancel phase
    • VaultInvestOrder / VaultRedeemOrder reflect cancel state correctly
    • PendingInvestOrder.isQueuedCancellation is set when cancel is queued on hub
  3. Verify combined fulfillment + cancellation callbacks produce correct state (both fulfilled shares claimable AND cancelled assets claimable)
  4. Verify saveOrClear correctly deletes orders when all amounts (including cancel amounts) reach zero

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions