Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion packages/composable/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,27 @@ const signedRegisterCalldata = poller.encodeRegisterWithSignature(schedule, dead

// Direct revocation must be submitted by schedule.funder.
const revokeCalldata = poller.encodeRevoke(schedule)

// Or authorize revocation for submission by another account.
const revokeAuthorization = {
handler: schedule.handler,
authEpoch: schedule.authEpoch,
funder: schedule.funder,
owner: schedule.owner,
salt: schedule.salt,
}
const revokeDeadline = Math.floor(Date.now() / 1000) + 15 * 60
const revokeTypedData = poller.getRevokeTypedData({
chainId,
...revokeAuthorization,
deadline: revokeDeadline,
})
const revokeSignature = await adapter.signer.signTypedData(
revokeTypedData.domain,
revokeTypedData.types,
revokeTypedData.message,
)
const signedRevokeCalldata = poller.encodeRevokeWithSignature(revokeAuthorization, revokeDeadline, revokeSignature)
```

The schedule fields are:
Expand All @@ -130,7 +151,7 @@ The schedule fields are:
- `salt`: the registered conditional order's salt.
- `staticInput`: the registered conditional order's encoded static input.

The SDK only encodes these transactions; the consumer owns signing, gas policy, submission, and confirmation. Submit direct registration and revocation calldata to `pollerAddress` from `schedule.funder`, or submit `signedRegisterCalldata` through a relayer. Signed registration calldata contains the schedule, deadline, and signature. Replay protection is scoped to the schedule ID through `authEpoch`. Use `poller.getComposableCowAddress()` and `poller.getSchedule(scheduleId)` to read Poller state.
The SDK only encodes these transactions; the consumer owns signing, gas policy, submission, and confirmation. Submit direct registration and revocation calldata to `pollerAddress` from `schedule.funder`, or submit `signedRegisterCalldata` or `signedRevokeCalldata` through a relayer. Signed registration calldata contains the schedule, deadline, and signature. Signed revocation calldata contains the handler, funder, owner, salt, `authEpoch`, deadline, and signature. Replay protection is scoped to the schedule ID through `authEpoch`. Use `poller.getComposableCowAddress()` and `poller.getSchedule(scheduleId)` to read Poller state.

## Usage

Expand Down
56 changes: 56 additions & 0 deletions packages/composable/src/ComposableCowPoller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { ComposableCowPollerAbi } from './abis/ComposableCowPollerAbi'
import type {
ComposableCowPollerDirectRevoke,
ComposableCowPollerRegisterTypedDataParams,
ComposableCowPollerRevokeTypedDataParams,
ComposableCowPollerSchedule,
ComposableCowPollerScheduleAuthorization,
ComposableCowPollerScheduleKey,
ComposableCowPollerTypedData,
} from './types'
Expand All @@ -21,6 +23,16 @@ const REGISTER_TYPES = {
{ name: 'deadline', type: 'uint256' },
],
}
const REVOKE_TYPES = {
Revoke: [
{ name: 'handler', type: 'address' },
{ name: 'authEpoch', type: 'uint96' },
{ name: 'funder', type: 'address' },
{ name: 'owner', type: 'address' },
{ name: 'salt', type: 'bytes32' },
{ name: 'deadline', type: 'uint256' },
],
}

type RegisterMessage = {
readonly handler: string
Expand All @@ -32,6 +44,15 @@ type RegisterMessage = {
readonly deadline: BigIntish
}

type RevokeMessage = {
readonly handler: string
readonly authEpoch: BigIntish
readonly funder: string
readonly owner: string
readonly salt: string
readonly deadline: BigIntish
}

/** Utilities for interacting with a ComposableCowPoller deployment. */
export class ComposableCowPoller {
constructor(public readonly pollerAddress?: string) {}
Expand Down Expand Up @@ -108,6 +129,24 @@ export class ComposableCowPoller {
}
}

/** Builds the EIP-712 payload authorized by revokeWithSignature. */
public getRevokeTypedData({
chainId,
handler,
authEpoch,
funder,
owner,
salt,
deadline,
}: ComposableCowPollerRevokeTypedDataParams): ComposableCowPollerTypedData<'Revoke', RevokeMessage> {
return {
domain: this.getEip712Domain(chainId),
types: REVOKE_TYPES,
primaryType: 'Revoke',
message: { handler, authEpoch, funder, owner, salt, deadline },
}
}

/** Returns the app-data-independent schedule ID. */
public getScheduleId(schedule: ComposableCowPollerScheduleKey): string {
const encoded = getGlobalAdapter().utils.encodeAbi(SCHEDULE_ID_ABI, [
Expand Down Expand Up @@ -147,4 +186,21 @@ export class ComposableCowPoller {
public encodeRevoke({ handler, owner, salt }: ComposableCowPollerDirectRevoke): string {
return getGlobalAdapter().utils.encodeFunction(ComposableCowPollerAbi, 'revoke', [handler, owner, salt]) as string
}

/** Encodes Poller.revokeWithSignature. */
public encodeRevokeWithSignature(
{ handler, funder, owner, salt, authEpoch }: ComposableCowPollerScheduleAuthorization,
deadline: BigIntish,
signature: string,
): string {
return getGlobalAdapter().utils.encodeFunction(ComposableCowPollerAbi, 'revokeWithSignature', [
handler,
funder,
owner,
salt,
authEpoch,
deadline,
signature,
]) as string
}
}
15 changes: 15 additions & 0 deletions packages/composable/src/abis/ComposableCowPollerAbi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@ export const ComposableCowPollerAbi = [
outputs: ID_OUTPUT,
stateMutability: 'nonpayable',
},
{
type: 'function',
name: 'revokeWithSignature',
inputs: [
{ name: 'handler', type: 'address', internalType: 'contract IConditionalOrderGenerator' },
{ name: 'funder', type: 'address', internalType: 'address' },
{ name: 'owner', type: 'address', internalType: 'address' },
{ name: 'salt', type: 'bytes32', internalType: 'bytes32' },
{ name: 'authEpoch', type: 'uint96', internalType: 'uint96' },
{ name: 'deadline', type: 'uint256', internalType: 'uint256' },
{ name: 'signature', type: 'bytes', internalType: 'bytes' },
],
outputs: ID_OUTPUT,
stateMutability: 'nonpayable',
},
{
type: 'function',
name: 'schedules',
Expand Down
5 changes: 5 additions & 0 deletions packages/composable/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ export type ComposableCowPollerRegisterTypedDataParams = {
readonly deadline: BigIntish
}

export type ComposableCowPollerRevokeTypedDataParams = ComposableCowPollerScheduleAuthorization & {
readonly chainId: number
readonly deadline: BigIntish
}

export enum ProofLocation {
// The location of the proofs is private to the caller.
PRIVATE = 0,
Expand Down
61 changes: 60 additions & 1 deletion packages/composable/tests/ComposableCowPoller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { setGlobalAdapter } from '@cowprotocol/sdk-common'
import { BigNumber } from 'ethers-v5'

import * as composable from '../src'
import { ComposableCowPoller, type ComposableCowPollerDirectRevoke, type ComposableCowPollerSchedule } from '../src'
import {
ComposableCowPoller,
type ComposableCowPollerDirectRevoke,
type ComposableCowPollerSchedule,
type ComposableCowPollerScheduleAuthorization,
} from '../src'
import { ComposableCowPollerAbi } from '../src/abis/ComposableCowPollerAbi'
import { createAdapters } from './setup'

Expand All @@ -22,9 +27,18 @@ const CHAIN_ID = 1
const REGISTER_SELECTOR = '0x199d771f'
const REGISTER_WITH_SIGNATURE_SELECTOR = '0x9e72edd4'
const REVOKE_SELECTOR = '0xd96054c4'
const REVOKE_WITH_SIGNATURE_SELECTOR = '0xbfd83f22'
const DEADLINE = 2_000_000_000n
const SIGNATURE = '0x123456'
const REGISTER_DIGEST = '0x7abf30523ae51092914ae8230bc2af45078d9b4f47062f3c9cb6e39cd13106bf'
const REVOKE_DIGEST = '0x11b6cb77364f3c3f8a454cb36f0d22166c6acd9575c066b309f95c9aff712c5f'
const AUTHORIZATION: ComposableCowPollerScheduleAuthorization = {
handler: SCHEDULE.handler,
authEpoch: SCHEDULE.authEpoch,
funder: SCHEDULE.funder,
owner: SCHEDULE.owner,
salt: SCHEDULE.salt,
}

describe('ComposableCowPoller ABI', () => {
test('keeps the ABI internal', () => {
Expand All @@ -38,6 +52,7 @@ describe('ComposableCowPoller ABI', () => {
'register',
'registerWithSignature',
'revoke',
'revokeWithSignature',
'schedules',
])
})
Expand Down Expand Up @@ -171,6 +186,13 @@ describe('ComposableCowPoller', () => {
expect(() => instance.getRegisterTypedData({ chainId: CHAIN_ID, schedule: SCHEDULE, deadline: DEADLINE })).toThrow(
'pollerAddress is required',
)
expect(() =>
instance.getRevokeTypedData({
chainId: CHAIN_ID,
...AUTHORIZATION,
deadline: DEADLINE,
}),
).toThrow('pollerAddress is required')
})

test('builds the register digest across adapters', () => {
Expand Down Expand Up @@ -213,4 +235,41 @@ 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,
...AUTHORIZATION,
deadline: DEADLINE,
})

expect(typedData.primaryType).toEqual('Revoke')
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(AUTHORIZATION, DEADLINE, SIGNATURE))
}

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]!)

expect(handler.toLowerCase()).toEqual(AUTHORIZATION.handler)
expect(funder.toLowerCase()).toEqual(AUTHORIZATION.funder)
expect(owner.toLowerCase()).toEqual(AUTHORIZATION.owner)
expect(salt).toEqual(AUTHORIZATION.salt)
expect(authEpoch).toEqual(AUTHORIZATION.authEpoch)
expect(deadline).toEqual(DEADLINE)
expect(signature).toEqual(SIGNATURE)
})
})
Loading