-
Notifications
You must be signed in to change notification settings - Fork 2
Description
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)
InvestorTransactionTypeenum already includesDEPOSIT_REQUEST_CANCELLEDandREDEEM_REQUEST_CANCELLEDInvestorTransactionServicehascancelDepositRequest()andcancelRedeemRequest()methods — never called by any handlerCrosschainMessageServicedecodesCancelDepositRequest/CancelRedeemRequestmessage types- Vault ABI includes all 6 cancel events
What's missing
- No event handlers for any of the 6 vault cancel events
isQueuedCancellationflag onUpdateDepositRequest/UpdateRedeemRequestis ignored by handlers- No schema fields for cancel state on
VaultInvestOrder,VaultRedeemOrder,PendingInvestOrder,PendingRedeemOrder - No new
InvestorTransactionTypevalues 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:
- Create
InvestorTransactionwith typeDEPOSIT_REQUEST_CANCELLED(method already exists) - Update
VaultInvestOrder.pendingCancelDeposit = true
Do NOT reduce
requestedAssetsAmounthere — 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:
- Create
InvestorTransactionwith typeCANCEL_DEPOSIT_CLAIMABLE(new method needed) - Update
VaultInvestOrder:pendingCancelDeposit = falseclaimableCancelDepositAmount += assetsrequestedAssetsAmount -= 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:
- Create
InvestorTransactionwith typeCANCEL_DEPOSIT_CLAIMED(new method needed) - Update
VaultInvestOrder:claimableCancelDepositAmount -= assets
saveOrClear()— delete if all amounts are zero
vault:CancelRedeemRequest
Event: CancelRedeemRequest(controller indexed, requestId indexed, sender)
When: User initiates redeem cancellation
Actions:
- Create
InvestorTransactionwith typeREDEEM_REQUEST_CANCELLED(method already exists) - Update
VaultRedeemOrder.pendingCancelRedeem = true
vault:CancelRedeemClaimable
Event: CancelRedeemClaimable(controller indexed, requestId indexed, shares)
When: Hub processed the cancellation, shares are now claimable
Actions:
- Create
InvestorTransactionwith typeCANCEL_REDEEM_CLAIMABLE(new method needed) - Update
VaultRedeemOrder:pendingCancelRedeem = falseclaimableCancelRedeemAmount += sharesrequestedSharesAmount -= shares
vault:CancelRedeemClaim
Event: CancelRedeemClaim(controller indexed, receiver indexed, requestId indexed, sender, shares)
When: User claims refunded shares
Actions:
- Create
InvestorTransactionwith typeCANCEL_REDEEM_CLAIMED(new method needed) - Update
VaultRedeemOrder:claimableCancelRedeemAmount -= shares
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:
- Update
PendingInvestOrder.isQueuedCancellation = true - Update
EpochOutstandingInvestto 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()— setspendingCancelDeposit = truecancelDepositClaimable(assets)— setspendingCancelDeposit = false, adds toclaimableCancelDepositAmount, reducesrequestedAssetsAmountcancelDepositClaim(assets)— reducesclaimableCancelDepositAmount- Update
saveOrClear()to also checkclaimableCancelDepositAmount === 0nandpendingCancelDeposit === false
VaultRedeemOrderService — add methods
cancelRedeemRequest()— setspendingCancelRedeem = truecancelRedeemClaimable(shares)— setspendingCancelRedeem = false, adds toclaimableCancelRedeemAmount, reducesrequestedSharesAmountcancelRedeemClaim(shares)— reducesclaimableCancelRedeemAmount- Update
saveOrClear()similarly
InvestorTransactionService — add methods
cancelDepositClaimable()— typeCANCEL_DEPOSIT_CLAIMABLEcancelDepositClaimed()— typeCANCEL_DEPOSIT_CLAIMEDcancelRedeemClaimable()— typeCANCEL_REDEEM_CLAIMABLEcancelRedeemClaimed()— typeCANCEL_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:
- Normal pending request (waiting for approval)
- Pending request with queued cancellation (will be cancelled when next
notifyDepositruns)
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
CancelDepositRequestevent on the vault may NOT fire in this flow (since the user didn't initiate it), so thependingCancelDepositflag may transition directly fromfalseto receiving aCancelDepositClaimableevent - The
VaultInvestOrderhandlers should handle this gracefully —cancelDepositClaimable()should work regardless of whetherpendingCancelDepositwas 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
- Deploy to a testnet fork and trigger a deposit cancellation in both immediate and queued scenarios
- Query the GraphQL API to verify:
InvestorTransactionrecords are created for each cancel phaseVaultInvestOrder/VaultRedeemOrderreflect cancel state correctlyPendingInvestOrder.isQueuedCancellationis set when cancel is queued on hub
- Verify combined fulfillment + cancellation callbacks produce correct state (both fulfilled shares claimable AND cancelled assets claimable)
- Verify
saveOrClearcorrectly deletes orders when all amounts (including cancel amounts) reach zero
References
- AsyncVault.sol —
cancelDepositRequest,claimCancelDepositRequest,CancelDepositClaimable,CancelDepositClaimevents - BaseVaults.sol —
cancelRedeemRequest,claimCancelRedeemRequest, redeem cancel events - AsyncRequestManager.sol — Cancel state machine,
fulfillDepositRequestwithcancelledAssetsparam - BatchRequestManager.sol — Hub-side cancel logic, queued cancellation,
_postClaimUpdateQueued - RequestCallbackMessageLib.sol —
FulfilledDepositRequeststruct withcancelledAssetAmountfield