ERC-7944: Async Cancellation
ERC-7944 closes a known UX gap in ERC-7540 async vaults: once a user submits a deposit/withdraw request they may need to cancel it before the vault settles. Without cancellation, requests sit indefinitely, locking user funds in pending state. ERC-7944 standardises the cancel-request flow as a small interface delta on top of ERC-7540.
Required Interface (delta from ERC-7540)
interface IERC7944 {
event CancelDepositRequest(uint256 indexed requestId, address indexed controller);
event CancelRedeemRequest(uint256 indexed requestId, address indexed controller);
function cancelDepositRequest(uint256 requestId, address controller) external;
function cancelRedeemRequest(uint256 requestId, address controller) external;
function pendingCancelDepositRequest(uint256 requestId, address controller)
external view returns (uint256 cancelled);
function pendingCancelRedeemRequest(uint256 requestId, address controller)
external view returns (uint256 cancelled);
}The vault validates that controller is the request owner (or approved), marks the request cancelled, and on the next settlement cycle returns the locked principal to the user instead of issuing shares. Cancellations may be immediate (synchronous refund) or queued (asynchronous like the original request); the spec permits both and exposes pendingCancel*Request so off-chain UIs can show "your cancel request is in flight".
Neo Equivalent: Cancel-by-id on the Request Queue
The ERC-7540 mirror's request/claim pattern stores per-request state in the vault's storage keyed by requestId. Adding cancel is one extra storage flag plus a settlement-side branch:
public static void CancelDepositRequest(BigInteger requestId, UInt160 controller)
{
if (!Runtime.CheckWitness(controller)) throw new Exception("not controller");
var key = DepositRequestKey(requestId);
var req = (DepositRequest)Storage.Get(Storage.CurrentContext, key);
if (req is null) throw new Exception("no such request");
if (req.Controller != controller) throw new Exception("not owner");
if (req.Settled) throw new Exception("already settled");
if (req.Cancelled) return; // idempotent
req.Cancelled = true;
Storage.Put(Storage.CurrentContext, key, req);
OnCancelDepositRequest(requestId, controller);
}
// The next settlement cycle reads req.Cancelled and refunds principal
// instead of issuing shares.| ERC-7944 (Ethereum) | Neo Equivalent | Notes |
|---|---|---|
cancelDepositRequest(id, controller) | CancelDepositRequest(id, controller) with witness check | Same semantics |
pendingCancelDepositRequest(id, controller) | PendingCancelDepositRequest(id, controller) view returning the cancelled amount | UI-facing helper |
| Settlement cycle returns principal on cancellation | Settlement cycle reads req.Cancelled and uses Contract.Call(asset, "transfer", …) to refund | Native NEP-17 transfer back |
Approved-operator path (controller != msg.sender) | Witness-scope or approved-operator check on the vault | Flexible |
Composition With Existing Mirror Pages
- ERC-7540 — base async vault. ERC-7944 is a strict superset; implement both for a complete async-vault offering.
- ERC-7575 — multi-asset vault. Cancellation extends per-asset since each sub-vault has its own request queue.
- ERC-3009 — meta-tx authorization. Cancel can be issued via a relayer using the same witness-scope flow.
Migration Notes
Existing ERC-7540 ports get this for free if the request struct already has room for a bool Cancelled flag. The settlement loop's existing "if pending then settle" branch becomes "if pending and not cancelled then settle, else refund principal". Two-line patch in most implementations.
