From a65aab7249080bdf4ff4eb533bf47423e00b8e76 Mon Sep 17 00:00:00 2001 From: igorroncevic <57319163+igorroncevic@users.noreply.github.com> Date: Thu, 6 Aug 2026 14:34:01 +0200 Subject: [PATCH 1/2] feat(composable): add signed Poller revocation # Conflicts: # packages/composable/README.md # packages/composable/src/ComposableCowPoller.ts # packages/composable/tests/ComposableCowPoller.spec.ts --- packages/composable/README.md | 17 +++++++ .../composable/src/ComposableCowPoller.ts | 41 +++++++++++++++++ packages/composable/src/types.ts | 8 ++++ .../tests/ComposableCowPoller.spec.ts | 44 +++++++++++++++++++ 4 files changed, 110 insertions(+) diff --git a/packages/composable/README.md b/packages/composable/README.md index 9c09dc29c..fce448796 100644 --- a/packages/composable/README.md +++ b/packages/composable/README.md @@ -116,6 +116,23 @@ const signedRegisterCalldata = poller.encodeRegisterWithSignature(schedule, dead // Submit this calldata to pollerAddress from schedule.funder. const revokeCalldata = poller.encodeRevoke(scheduleId) + +// After registration succeeds, read the funder's shared action nonce again before signing revocation. +const revokeNonce = await poller.getNonce(schedule.funder) +const revokeDeadline = Math.floor(Date.now() / 1000) + 15 * 60 +const revokeTypedData = poller.getRevokeTypedData({ + chainId, + id: scheduleId, + funder: schedule.funder, + nonce: revokeNonce, + deadline: revokeDeadline, +}) +const revokeSignature = await signer.signTypedData( + revokeTypedData.domain, + revokeTypedData.types, + revokeTypedData.message, +) +const signedRevokeCalldata = poller.encodeRevokeWithSignature(scheduleId, revokeDeadline, revokeSignature) ``` ## Usage diff --git a/packages/composable/src/ComposableCowPoller.ts b/packages/composable/src/ComposableCowPoller.ts index 8a8f61383..ec1640465 100644 --- a/packages/composable/src/ComposableCowPoller.ts +++ b/packages/composable/src/ComposableCowPoller.ts @@ -3,6 +3,7 @@ import { BigIntish, getGlobalAdapter, Provider } from '@cowprotocol/sdk-common' import { ComposableCowPollerAbi } from './abis/ComposableCowPollerAbi' import type { ComposableCowPollerRegisterTypedDataParams, + ComposableCowPollerRevokeTypedDataParams, ComposableCowPollerSchedule, ComposableCowPollerScheduleKey, ComposableCowPollerTypedData, @@ -20,6 +21,14 @@ const REGISTER_TYPES = { { name: 'deadline', type: 'uint256' }, ], } +const REVOKE_TYPES = { + Revoke: [ + { name: 'id', type: 'bytes32' }, + { name: 'funder', type: 'address' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], +} type RegisterMessage = { readonly handler: string @@ -31,6 +40,13 @@ type RegisterMessage = { readonly deadline: BigIntish } +type RevokeMessage = { + readonly id: string + readonly funder: string + readonly nonce: BigIntish + readonly deadline: BigIntish +} + /** Utilities for interacting with a ComposableCowPoller deployment. */ export class ComposableCowPoller { private composableCowAddress?: { adapter: ReturnType; address: string } @@ -99,6 +115,22 @@ export class ComposableCowPoller { } } + /** Builds the EIP-712 payload authorized by revokeWithSignature. */ + public getRevokeTypedData({ + chainId, + id, + funder, + nonce, + deadline, + }: ComposableCowPollerRevokeTypedDataParams): ComposableCowPollerTypedData<'Revoke', RevokeMessage> { + return { + domain: this.getEip712Domain(chainId), + types: REVOKE_TYPES, + primaryType: 'Revoke', + message: { id, funder, nonce, deadline }, + } + } + /** Returns the app-data-independent schedule ID. */ public getScheduleId(schedule: ComposableCowPollerScheduleKey): string { const encoded = getGlobalAdapter().utils.encodeAbi(SCHEDULE_ID_ABI, [ @@ -138,4 +170,13 @@ export class ComposableCowPoller { public encodeRevoke(id: string): string { return getGlobalAdapter().utils.encodeFunction(ComposableCowPollerAbi, 'revoke', [id]) as string } + + /** Encodes Poller.revokeWithSignature. */ + public encodeRevokeWithSignature(id: string, deadline: BigIntish, signature: string): string { + return getGlobalAdapter().utils.encodeFunction(ComposableCowPollerAbi, 'revokeWithSignature', [ + id, + deadline, + signature, + ]) as string + } } diff --git a/packages/composable/src/types.ts b/packages/composable/src/types.ts index 70f320938..d7ac66c78 100644 --- a/packages/composable/src/types.ts +++ b/packages/composable/src/types.ts @@ -108,6 +108,14 @@ export type ComposableCowPollerRegisterTypedDataParams = { readonly deadline: BigIntish } +export type ComposableCowPollerRevokeTypedDataParams = { + readonly chainId: number + readonly id: string + readonly funder: string + readonly nonce: BigIntish + readonly deadline: BigIntish +} + export enum ProofLocation { // The location of the proofs is private to the caller. PRIVATE = 0, diff --git a/packages/composable/tests/ComposableCowPoller.spec.ts b/packages/composable/tests/ComposableCowPoller.spec.ts index 29df366d7..f38e03401 100644 --- a/packages/composable/tests/ComposableCowPoller.spec.ts +++ b/packages/composable/tests/ComposableCowPoller.spec.ts @@ -24,6 +24,7 @@ const NONCE = 7n const DEADLINE = 2_000_000_000n const SIGNATURE = '0x123456' const REGISTER_DIGEST = '0x19c2f3157fd433af46f24ac718b47fb3b8a9b456d2a2f2b6c31cc07820bec2d1' +const REVOKE_DIGEST = '0x904595cae3f7402646e3fba3b2683afaee0b71fd0eaf835ce0bb9c2bd7a3c9fd' describe('ComposableCowPoller ABI', () => { test('keeps the ABI internal', () => { @@ -138,6 +139,15 @@ describe('ComposableCowPoller', () => { expect(() => instance.getRegisterTypedData({ chainId: CHAIN_ID, schedule: SCHEDULE, nonce: NONCE, deadline: DEADLINE }), ).toThrow('pollerAddress is required') + expect(() => + instance.getRevokeTypedData({ + chainId: CHAIN_ID, + id: SCHEDULE_ID, + funder: SCHEDULE.funder, + nonce: NONCE, + deadline: DEADLINE, + }), + ).toThrow('pollerAddress is required') }) test('builds the register digest across adapters', () => { @@ -178,4 +188,38 @@ describe('ComposableCowPoller', () => { expect(adapters.viemAdapter.utils.toBigIntish(deadline)).toEqual(DEADLINE) expect(signature).toEqual(SIGNATURE) }) + + test('builds the revoke digest across adapters', () => { + for (const adapter of Object.values(adapters)) { + setGlobalAdapter(adapter) + const typedData = poller.getRevokeTypedData({ + chainId: CHAIN_ID, + id: SCHEDULE_ID, + funder: SCHEDULE.funder, + nonce: NONCE, + deadline: DEADLINE, + }) + + expect(adapter.utils.hashTypedData(typedData.domain, typedData.types, typedData.message)).toEqual(REVOKE_DIGEST) + } + }) + + test('encodes signed revocation calldata across adapters', () => { + const encodedCalls = [] + + for (const adapter of Object.values(adapters)) { + setGlobalAdapter(adapter) + encodedCalls.push(poller.encodeRevokeWithSignature(SCHEDULE_ID, DEADLINE, SIGNATURE)) + } + + expect(new Set(encodedCalls).size).toEqual(1) + + const call = adapters.viemAdapter.utils.decodeFunctionData( + ComposableCowPollerAbi, + 'revokeWithSignature', + encodedCalls[0]!, + ) + + expect(Array.from(call)).toEqual([SCHEDULE_ID, BigInt(DEADLINE), SIGNATURE]) + }) }) From 97c0ef3b4290132fc2b83e229cca9219e5fc8a4d Mon Sep 17 00:00:00 2001 From: igorroncevic <57319163+igorroncevic@users.noreply.github.com> Date: Mon, 7 Sep 2026 11:17:42 +0200 Subject: [PATCH 2/2] test(composable): pin signed revoke selector --- packages/composable/tests/ComposableCowPoller.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/composable/tests/ComposableCowPoller.spec.ts b/packages/composable/tests/ComposableCowPoller.spec.ts index d548a00f6..626e0c6fc 100644 --- a/packages/composable/tests/ComposableCowPoller.spec.ts +++ b/packages/composable/tests/ComposableCowPoller.spec.ts @@ -29,6 +29,7 @@ const REGISTER_FROM_SHED_SELECTOR = '0x81b1b677' const REGISTER_WITH_SIGNATURE_SELECTOR = '0x9e72edd4' const REVOKE_SELECTOR = '0xd96054c4' const REVOKE_FROM_SHED_SELECTOR = '0x526c2744' +const REVOKE_WITH_SIGNATURE_SELECTOR = '0xbfd83f22' const DEADLINE = 2_000_000_000n const SIGNATURE = '0x123456' const REGISTER_DIGEST = '0x7abf30523ae51092914ae8230bc2af45078d9b4f47062f3c9cb6e39cd13106bf' @@ -317,6 +318,7 @@ describe('ComposableCowPoller', () => { } expect(new Set(encodedCalls).size).toEqual(1) + expect(encodedCalls[0]?.slice(0, 10)).toEqual(REVOKE_WITH_SIGNATURE_SELECTOR) const [handler, funder, owner, salt, authEpoch, deadline, signature] = adapters.viemAdapter.utils.decodeFunctionData(ComposableCowPollerAbi, 'revokeWithSignature', encodedCalls[0]!)