Skip to content

Commit a65aab7

Browse files
committed
feat(composable): add signed Poller revocation
# Conflicts: # packages/composable/README.md # packages/composable/src/ComposableCowPoller.ts # packages/composable/tests/ComposableCowPoller.spec.ts
1 parent 1d01b70 commit a65aab7

4 files changed

Lines changed: 110 additions & 0 deletions

File tree

packages/composable/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,23 @@ const signedRegisterCalldata = poller.encodeRegisterWithSignature(schedule, dead
116116

117117
// Submit this calldata to pollerAddress from schedule.funder.
118118
const revokeCalldata = poller.encodeRevoke(scheduleId)
119+
120+
// After registration succeeds, read the funder's shared action nonce again before signing revocation.
121+
const revokeNonce = await poller.getNonce(schedule.funder)
122+
const revokeDeadline = Math.floor(Date.now() / 1000) + 15 * 60
123+
const revokeTypedData = poller.getRevokeTypedData({
124+
chainId,
125+
id: scheduleId,
126+
funder: schedule.funder,
127+
nonce: revokeNonce,
128+
deadline: revokeDeadline,
129+
})
130+
const revokeSignature = await signer.signTypedData(
131+
revokeTypedData.domain,
132+
revokeTypedData.types,
133+
revokeTypedData.message,
134+
)
135+
const signedRevokeCalldata = poller.encodeRevokeWithSignature(scheduleId, revokeDeadline, revokeSignature)
119136
```
120137

121138
## Usage

packages/composable/src/ComposableCowPoller.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { BigIntish, getGlobalAdapter, Provider } from '@cowprotocol/sdk-common'
33
import { ComposableCowPollerAbi } from './abis/ComposableCowPollerAbi'
44
import type {
55
ComposableCowPollerRegisterTypedDataParams,
6+
ComposableCowPollerRevokeTypedDataParams,
67
ComposableCowPollerSchedule,
78
ComposableCowPollerScheduleKey,
89
ComposableCowPollerTypedData,
@@ -20,6 +21,14 @@ const REGISTER_TYPES = {
2021
{ name: 'deadline', type: 'uint256' },
2122
],
2223
}
24+
const REVOKE_TYPES = {
25+
Revoke: [
26+
{ name: 'id', type: 'bytes32' },
27+
{ name: 'funder', type: 'address' },
28+
{ name: 'nonce', type: 'uint256' },
29+
{ name: 'deadline', type: 'uint256' },
30+
],
31+
}
2332

2433
type RegisterMessage = {
2534
readonly handler: string
@@ -31,6 +40,13 @@ type RegisterMessage = {
3140
readonly deadline: BigIntish
3241
}
3342

43+
type RevokeMessage = {
44+
readonly id: string
45+
readonly funder: string
46+
readonly nonce: BigIntish
47+
readonly deadline: BigIntish
48+
}
49+
3450
/** Utilities for interacting with a ComposableCowPoller deployment. */
3551
export class ComposableCowPoller {
3652
private composableCowAddress?: { adapter: ReturnType<typeof getGlobalAdapter>; address: string }
@@ -99,6 +115,22 @@ export class ComposableCowPoller {
99115
}
100116
}
101117

118+
/** Builds the EIP-712 payload authorized by revokeWithSignature. */
119+
public getRevokeTypedData({
120+
chainId,
121+
id,
122+
funder,
123+
nonce,
124+
deadline,
125+
}: ComposableCowPollerRevokeTypedDataParams): ComposableCowPollerTypedData<'Revoke', RevokeMessage> {
126+
return {
127+
domain: this.getEip712Domain(chainId),
128+
types: REVOKE_TYPES,
129+
primaryType: 'Revoke',
130+
message: { id, funder, nonce, deadline },
131+
}
132+
}
133+
102134
/** Returns the app-data-independent schedule ID. */
103135
public getScheduleId(schedule: ComposableCowPollerScheduleKey): string {
104136
const encoded = getGlobalAdapter().utils.encodeAbi(SCHEDULE_ID_ABI, [
@@ -138,4 +170,13 @@ export class ComposableCowPoller {
138170
public encodeRevoke(id: string): string {
139171
return getGlobalAdapter().utils.encodeFunction(ComposableCowPollerAbi, 'revoke', [id]) as string
140172
}
173+
174+
/** Encodes Poller.revokeWithSignature. */
175+
public encodeRevokeWithSignature(id: string, deadline: BigIntish, signature: string): string {
176+
return getGlobalAdapter().utils.encodeFunction(ComposableCowPollerAbi, 'revokeWithSignature', [
177+
id,
178+
deadline,
179+
signature,
180+
]) as string
181+
}
141182
}

packages/composable/src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,14 @@ export type ComposableCowPollerRegisterTypedDataParams = {
108108
readonly deadline: BigIntish
109109
}
110110

111+
export type ComposableCowPollerRevokeTypedDataParams = {
112+
readonly chainId: number
113+
readonly id: string
114+
readonly funder: string
115+
readonly nonce: BigIntish
116+
readonly deadline: BigIntish
117+
}
118+
111119
export enum ProofLocation {
112120
// The location of the proofs is private to the caller.
113121
PRIVATE = 0,

packages/composable/tests/ComposableCowPoller.spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const NONCE = 7n
2424
const DEADLINE = 2_000_000_000n
2525
const SIGNATURE = '0x123456'
2626
const REGISTER_DIGEST = '0x19c2f3157fd433af46f24ac718b47fb3b8a9b456d2a2f2b6c31cc07820bec2d1'
27+
const REVOKE_DIGEST = '0x904595cae3f7402646e3fba3b2683afaee0b71fd0eaf835ce0bb9c2bd7a3c9fd'
2728

2829
describe('ComposableCowPoller ABI', () => {
2930
test('keeps the ABI internal', () => {
@@ -138,6 +139,15 @@ describe('ComposableCowPoller', () => {
138139
expect(() =>
139140
instance.getRegisterTypedData({ chainId: CHAIN_ID, schedule: SCHEDULE, nonce: NONCE, deadline: DEADLINE }),
140141
).toThrow('pollerAddress is required')
142+
expect(() =>
143+
instance.getRevokeTypedData({
144+
chainId: CHAIN_ID,
145+
id: SCHEDULE_ID,
146+
funder: SCHEDULE.funder,
147+
nonce: NONCE,
148+
deadline: DEADLINE,
149+
}),
150+
).toThrow('pollerAddress is required')
141151
})
142152

143153
test('builds the register digest across adapters', () => {
@@ -178,4 +188,38 @@ describe('ComposableCowPoller', () => {
178188
expect(adapters.viemAdapter.utils.toBigIntish(deadline)).toEqual(DEADLINE)
179189
expect(signature).toEqual(SIGNATURE)
180190
})
191+
192+
test('builds the revoke digest across adapters', () => {
193+
for (const adapter of Object.values(adapters)) {
194+
setGlobalAdapter(adapter)
195+
const typedData = poller.getRevokeTypedData({
196+
chainId: CHAIN_ID,
197+
id: SCHEDULE_ID,
198+
funder: SCHEDULE.funder,
199+
nonce: NONCE,
200+
deadline: DEADLINE,
201+
})
202+
203+
expect(adapter.utils.hashTypedData(typedData.domain, typedData.types, typedData.message)).toEqual(REVOKE_DIGEST)
204+
}
205+
})
206+
207+
test('encodes signed revocation calldata across adapters', () => {
208+
const encodedCalls = []
209+
210+
for (const adapter of Object.values(adapters)) {
211+
setGlobalAdapter(adapter)
212+
encodedCalls.push(poller.encodeRevokeWithSignature(SCHEDULE_ID, DEADLINE, SIGNATURE))
213+
}
214+
215+
expect(new Set(encodedCalls).size).toEqual(1)
216+
217+
const call = adapters.viemAdapter.utils.decodeFunctionData(
218+
ComposableCowPollerAbi,
219+
'revokeWithSignature',
220+
encodedCalls[0]!,
221+
)
222+
223+
expect(Array.from(call)).toEqual([SCHEDULE_ID, BigInt(DEADLINE), SIGNATURE])
224+
})
181225
})

0 commit comments

Comments
 (0)