cancelOrder on IntentGatewayV2 (evm/src/apps/IntentGatewayV2.sol) emits nothing. The only event in the whole cancellation lifecycle is EscrowRefunded, which fires when the refund finally lands on the source chain — in the same transaction for a same-chain cancel, after a Hyperbridge round trip for a cross-chain one. The gateway should emit a cancel event carrying the order commitment at the moment a cancellation is initiated, on whichever chain it is initiated.
Today
cancelOrder routes to one of three paths, none of which emits a gateway event:
_cancelSameChain (evm/src/apps/intentsv2/IntrinsicIntents.sol): refunds via _withdraw, so EscrowRefunded(commitment, tokens) appears in the same tx. The event says "refunded", not "cancelled", and is the same event a cross-chain refund arrives with.
_cancelFromSource (evm/src/apps/intentsv2/ExtrinsicIntents.sol): dispatches a GET request to the destination and returns. The only trace is the host's GetRequestEvent, which does not reference the order.
_cancelFromDest: writes _filled[commitment] = order.user — the order is unfillable from this point — and dispatches a RefundEscrow POST. Again nothing from the gateway; the only trace is the host's PostRequestEvent. Every later fill attempt reverts with Filled() (testFillOrderAfterDestinationChainCancellationFails), which from the outside is indistinguishable from a real fill, except that no OrderFilled was ever emitted. The Cancelled() error declared in IntentsBase.sol is never used.
What that costs each consumer:
- Indexer (
sdk/packages/indexer): OrderStatus is PLACED | FILLED | REDEEMED | REFUNDED, and a cancel only becomes visible when EscrowRefunded lands on the source chain. A cross-chain order cancelled from the destination stays PLACED for the whole round trip, and one whose refund message is stuck (no relayer, fee too low) stays PLACED forever — indistinguishable from an open order. Mainnet right now has 9 PLACED orders, 8 of them past their deadline; the indexer cannot say whether any of them has a cancel in flight. (142 orders have been refunded so far: 103 same-chain, 39 cross-chain.)
- SDK (
sdk/packages/sdk/src/protocols/intents/OrderCanceller.ts): to track a cross-chain cancel it parses GetRequestEvent / PostRequestEvent out of the cancel receipt with the EvmHost ABI, because the gateway gives it nothing keyed by commitment. orderStatusStream shows a user who just cancelled PLACED until the refund arrives. Anyone not going through the SDK — explorers, the order page, ops scripts — has no signal at all.
- Simplex (
sdk/packages/simplex/src/core/event-monitor.ts, filler.ts): the filler retracts its bid when it sees OrderFilled, but a destination-side cancel kills the order silently, so the bid stays live on Hyperbridge with its deposit locked until the hourly retractStaleBids sweep, and the order sits in the queue until a fill attempt reverts.
Proposal
Add to IntentsBase:
/**
* @dev Emitted when an order's cancellation is initiated, on the chain it is initiated from.
* For same-chain orders the refund is processed in the same transaction and `EscrowRefunded`
* follows; for cross-chain orders `EscrowRefunded` follows on the source chain once the
* cancellation has travelled through Hyperbridge.
* @param commitment The order commitment hash.
* @param canceller The account that initiated the cancellation.
*/
event OrderCancelled(bytes32 indexed commitment, address canceller);
and emit it from all three paths:
_cancelSameChain: alongside the _withdraw refund.
_cancelFromSource: after the GET dispatch.
_cancelFromDest: right after _filled[commitment] = order.user, i.e. at the moment the order stops being fillable.
commitment is indexed like every other order event, so it is a cheap topic filter. canceller is msg.sender: the destination path is permissionless after expiry, so whether the user or a relayer triggered it is information that is otherwise lost. EscrowRefunded stays the terminal event; the completion paths (onGetResponse, onAccept with RefundEscrow) are unchanged.
The gateway upgrades through cross-chain governance (RequestKind.UpgradeContract, #981), so this is an implementation upgrade and an additive ABI change — no redeploy.
Optional, same area: fillOrder could revert with the existing Cancelled() when _filled[commitment] == address(uint160(uint256(order.user))) so a solver's failed fill says why, with the caveat that a user filling their own order would also match.
Consumers
- Indexer: handler for
OrderCancelled in src/handlers/events/intentGatewayV3/, topic in scripts/templates/evm-chain.yaml.hbs, IntentGatewayV3.abi.json. New OrderStatus.CANCELLED written as an IOrderV3StatusMetadata row on the initiating chain (store the canceller — the row's filler column is the natural slot, possibly renamed to actor). CANCELLED is non-terminal for cross-chain orders: REFUNDED still closes them. A cancel arriving before the source node has indexed OrderPlaced goes through PendingStatusMetadata like any other out-of-order status.
- SDK:
OrderStatus.CANCELLED in sdk/packages/sdk/src/types, the ABI in src/abis/IntentGatewayV2.ts, and OrderCanceller confirming CANCEL_STARTED from the gateway's own event. The terminal set of orderStatusStream is unchanged.
- Simplex: add
OrderCancelled to the shared scanner's event set next to OrderFilled / PartialFill and route it through the same path as orderFilledOnChain — retract the bid and drop the order immediately instead of waiting for the sweep.
- Docs:
docs/content/developers/evm/intent-gateway/overview.mdx and cancelling-orders.mdx. Both still document EscrowRefunded(bytes32 indexed commitment) without the tokens field, and the IIntentGatewayV2 interface in sdk/packages/core/contracts/apps/IntentGatewayV2.sol declares the pre-tokens signatures for OrderFilled, EscrowReleased and EscrowRefunded too — worth bringing in line while adding the new event.
- Tests:
vm.expectEmit for each path in evm/tests/foundry/IntentGatewayV2Test.sol (testCancelOrder*) and IntentGatewayV2SameChainTest.sol (testSameChainCancel_*).
cancelOrderon IntentGatewayV2 (evm/src/apps/IntentGatewayV2.sol) emits nothing. The only event in the whole cancellation lifecycle isEscrowRefunded, which fires when the refund finally lands on the source chain — in the same transaction for a same-chain cancel, after a Hyperbridge round trip for a cross-chain one. The gateway should emit a cancel event carrying the order commitment at the moment a cancellation is initiated, on whichever chain it is initiated.Today
cancelOrderroutes to one of three paths, none of which emits a gateway event:_cancelSameChain(evm/src/apps/intentsv2/IntrinsicIntents.sol): refunds via_withdraw, soEscrowRefunded(commitment, tokens)appears in the same tx. The event says "refunded", not "cancelled", and is the same event a cross-chain refund arrives with._cancelFromSource(evm/src/apps/intentsv2/ExtrinsicIntents.sol): dispatches a GET request to the destination and returns. The only trace is the host'sGetRequestEvent, which does not reference the order._cancelFromDest: writes_filled[commitment] = order.user— the order is unfillable from this point — and dispatches aRefundEscrowPOST. Again nothing from the gateway; the only trace is the host'sPostRequestEvent. Every later fill attempt reverts withFilled()(testFillOrderAfterDestinationChainCancellationFails), which from the outside is indistinguishable from a real fill, except that noOrderFilledwas ever emitted. TheCancelled()error declared inIntentsBase.solis never used.What that costs each consumer:
sdk/packages/indexer):OrderStatusisPLACED | FILLED | REDEEMED | REFUNDED, and a cancel only becomes visible whenEscrowRefundedlands on the source chain. A cross-chain order cancelled from the destination staysPLACEDfor the whole round trip, and one whose refund message is stuck (no relayer, fee too low) staysPLACEDforever — indistinguishable from an open order. Mainnet right now has 9PLACEDorders, 8 of them past their deadline; the indexer cannot say whether any of them has a cancel in flight. (142 orders have been refunded so far: 103 same-chain, 39 cross-chain.)sdk/packages/sdk/src/protocols/intents/OrderCanceller.ts): to track a cross-chain cancel it parsesGetRequestEvent/PostRequestEventout of the cancel receipt with the EvmHost ABI, because the gateway gives it nothing keyed by commitment.orderStatusStreamshows a user who just cancelledPLACEDuntil the refund arrives. Anyone not going through the SDK — explorers, the order page, ops scripts — has no signal at all.sdk/packages/simplex/src/core/event-monitor.ts,filler.ts): the filler retracts its bid when it seesOrderFilled, but a destination-side cancel kills the order silently, so the bid stays live on Hyperbridge with its deposit locked until the hourlyretractStaleBidssweep, and the order sits in the queue until a fill attempt reverts.Proposal
Add to
IntentsBase:and emit it from all three paths:
_cancelSameChain: alongside the_withdrawrefund._cancelFromSource: after the GET dispatch._cancelFromDest: right after_filled[commitment] = order.user, i.e. at the moment the order stops being fillable.commitmentis indexed like every other order event, so it is a cheap topic filter.cancellerismsg.sender: the destination path is permissionless after expiry, so whether the user or a relayer triggered it is information that is otherwise lost.EscrowRefundedstays the terminal event; the completion paths (onGetResponse,onAcceptwithRefundEscrow) are unchanged.The gateway upgrades through cross-chain governance (
RequestKind.UpgradeContract, #981), so this is an implementation upgrade and an additive ABI change — no redeploy.Optional, same area:
fillOrdercould revert with the existingCancelled()when_filled[commitment] == address(uint160(uint256(order.user)))so a solver's failed fill says why, with the caveat that a user filling their own order would also match.Consumers
OrderCancelledinsrc/handlers/events/intentGatewayV3/, topic inscripts/templates/evm-chain.yaml.hbs,IntentGatewayV3.abi.json. NewOrderStatus.CANCELLEDwritten as anIOrderV3StatusMetadatarow on the initiating chain (store the canceller — the row'sfillercolumn is the natural slot, possibly renamed toactor).CANCELLEDis non-terminal for cross-chain orders:REFUNDEDstill closes them. A cancel arriving before the source node has indexedOrderPlacedgoes throughPendingStatusMetadatalike any other out-of-order status.OrderStatus.CANCELLEDinsdk/packages/sdk/src/types, the ABI insrc/abis/IntentGatewayV2.ts, andOrderCancellerconfirmingCANCEL_STARTEDfrom the gateway's own event. The terminal set oforderStatusStreamis unchanged.OrderCancelledto the shared scanner's event set next toOrderFilled/PartialFilland route it through the same path asorderFilledOnChain— retract the bid and drop the order immediately instead of waiting for the sweep.docs/content/developers/evm/intent-gateway/overview.mdxandcancelling-orders.mdx. Both still documentEscrowRefunded(bytes32 indexed commitment)without thetokensfield, and theIIntentGatewayV2interface insdk/packages/core/contracts/apps/IntentGatewayV2.soldeclares the pre-tokenssignatures forOrderFilled,EscrowReleasedandEscrowRefundedtoo — worth bringing in line while adding the new event.vm.expectEmitfor each path inevm/tests/foundry/IntentGatewayV2Test.sol(testCancelOrder*) andIntentGatewayV2SameChainTest.sol(testSameChainCancel_*).