Skip to content

Commit

Permalink
Handle Expired TWAP and not started TWAP (#153)
Browse files Browse the repository at this point in the history
* Get block info

* Read from the cabinet the start date ans do custom twap validation

* Add descriptive message

* Fix condition check

* Handle expiration

* Leave todo for handling next part at a specific time

* Handle specific polling for TWAP

* Update version

* Fix typos

* Create OwnerContext type

* Add isSingleOrder flag

* Get cabinet also for merkle root orders

* Decode cabinet and verify its value

* Add offchain and proof to poll params
  • Loading branch information
anxolin committed Aug 30, 2023
1 parent 34fbd9a commit 3a61922
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 34 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cowprotocol/cow-sdk",
"version": "3.0.0-rc.3",
"version": "3.0.0-rc.5",
"license": "(MIT OR Apache-2.0)",
"files": [
"/dist"
Expand Down Expand Up @@ -37,6 +37,7 @@
"cross-fetch": "^3.1.5",
"exponential-backoff": "^3.1.1",
"graphql-request": "^4.3.0",
"graphql": "^16.3.0",
"limiter": "^2.1.0"
},
"peerDependencies": {
Expand All @@ -62,7 +63,6 @@
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-unused-imports": "^3.0.0",
"ethers": "^5.7.2",
"graphql": "^16.3.0",
"jest": "^29.4.2",
"jest-fetch-mock": "^3.0.3",
"microbundle": "^0.15.1",
Expand Down
2 changes: 2 additions & 0 deletions src/composable/ConditionalOrder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ describe('ConditionalOrder', () => {
})

class TestConditionalOrder extends ConditionalOrder<string, string> {
isSingleOrder = true

constructor(address: string, salt?: string, data = '0x') {
super({
handler: address,
Expand Down
51 changes: 31 additions & 20 deletions src/composable/ConditionalOrder.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BigNumber, ethers, utils, providers } from 'ethers'
import { BigNumber, constants, ethers, utils } from 'ethers'
import { IConditionalOrder } from './generated/ComposableCoW'

import { decodeParams, encodeParams } from './utils'
Expand All @@ -7,11 +7,12 @@ import {
ConditionalOrderParams,
ContextFactory,
IsValidResult,
OwnerContext,
PollParams,
PollResult,
PollResultCode,
PollResultErrors,
} from './types'
import { SupportedChainId } from '../common'
import { getComposableCow, getComposableCowInterface } from './contracts'

/**
Expand Down Expand Up @@ -69,6 +70,8 @@ export abstract class ConditionalOrder<D, S> {
this.hasOffChainInput = hasOffChainInput
}

abstract get isSingleOrder(): boolean

/**
* Get a descriptive name for the type of the conditional order (i.e twap, dca, etc).
*
Expand Down Expand Up @@ -231,8 +234,9 @@ export abstract class ConditionalOrder<D, S> {
* @throws If the conditional order is not tradeable.
* @returns The tradeable `GPv2Order.Data` struct and the `signature` for the conditional order.
*/
async poll(owner: string, chain: SupportedChainId, provider: providers.Provider): Promise<PollResult> {
const composableCow = getComposableCow(chain, provider)
async poll(params: PollParams): Promise<PollResult> {
const { chainId, owner, provider } = params
const composableCow = getComposableCow(chainId, provider)

try {
const isValid = this.isValid()
Expand All @@ -245,17 +249,17 @@ export abstract class ConditionalOrder<D, S> {
}

// Let the concrete Conditional Order decide about the poll result
const pollResult = await this.pollValidate(owner, chain, provider)
const pollResult = await this.pollValidate(params)
if (pollResult) {
return pollResult
}

// Check if the owner authorised the order
const isAuthorized = await this.isAuthorized(owner, chain, provider)
const isAuthorized = await this.isAuthorized(params)
if (!isAuthorized) {
return {
result: PollResultCode.DONT_TRY_AGAIN,
reason: `NotAuthorised: Order ${this.id} is not authorised for ${owner} on chain ${chain}`,
reason: `NotAuthorised: Order ${this.id} is not authorised for ${owner} on chain ${chainId}`,
}
}

Expand Down Expand Up @@ -283,33 +287,40 @@ export abstract class ConditionalOrder<D, S> {
/**
* Checks if the owner authorized the conditional order.
*
* @param owner The owner of the conditional order.
* @param chain Which chain to use for the ComposableCoW contract.
* @param provider An RPC provider for the chain.
* @param params owner context, to be able to check if the order is authorized
* @returns true if the owner authorized the order, false otherwise.
*/
public isAuthorized(owner: string, chain: SupportedChainId, provider: providers.Provider): Promise<boolean> {
const composableCow = getComposableCow(chain, provider)
public isAuthorized(params: OwnerContext): Promise<boolean> {
const { chainId, owner, provider } = params
const composableCow = getComposableCow(chainId, provider)
return composableCow.callStatic.singleOrders(owner, this.id)
}

/**
* Checks the value in the cabinet for a given owner and chain
*
* @param params owner context, to be able to check the cabinet
*/
public cabinet(params: OwnerContext): Promise<string> {
const { chainId, owner, provider } = params

const slotId = this.isSingleOrder ? this.id : constants.HashZero

const composableCow = getComposableCow(chainId, provider)
return composableCow.callStatic.cabinet(owner, slotId)
}

/**
* Allow concrete conditional orders to perform additional validation for the poll method.
*
* This will allow the concrete orders to decide when an order shouldn't be polled again. For example, if the orders is expired.
* It also allows to signal when should the next check be done. For example, an order could signal that the validations will fail until a certain time or block.
*
* @param owner The owner of the conditional order.
* @param chain Which chain to use for the ComposableCoW contract.
* @param provider An RPC provider for the chain.
* @param params The poll parameters
*
* @returns undefined if the concrete order can't make a decision. Otherwise, it returns a PollResultErrors object.
*/
protected abstract pollValidate(
owner: string,
chain: SupportedChainId,
provider: providers.Provider
): Promise<PollResultErrors | undefined>
protected abstract pollValidate(params: PollParams): Promise<PollResultErrors | undefined>

/**
* Convert the struct that the contract expect as an encoded `staticInput` into a friendly data object modeling the smart order.
Expand Down
72 changes: 62 additions & 10 deletions src/composable/orderTypes/Twap.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BigNumber, constants, providers } from 'ethers'
import { BigNumber, constants, utils } from 'ethers'

import { ConditionalOrder } from '../ConditionalOrder'
import {
Expand All @@ -7,10 +7,12 @@ import {
ContextFactory,
IsNotValid,
IsValid,
OwnerContext,
PollParams,
PollResultCode,
PollResultErrors,
} from '../types'
import { encodeParams, isValidAbi } from '../utils'
import { SupportedChainId } from '../../common'
import { encodeParams, formatEpoch, getBlockInfo, isValidAbi } from '../utils'

// The type of Conditional Order
const TWAP_ORDER_TYPE = 'twap'
Expand Down Expand Up @@ -161,6 +163,8 @@ const DEFAULT_DURATION_OF_PART: DurationOfPart = { durationType: DurationType.AU
* @author mfw78 <mfw78@rndlabs.xyz>
*/
export class Twap extends ConditionalOrder<TwapData, TwapStruct> {
isSingleOrder = true

/**
* @see {@link ConditionalOrder.constructor}
* @throws If the TWAP order is invalid.
Expand Down Expand Up @@ -263,6 +267,23 @@ export class Twap extends ConditionalOrder<TwapData, TwapStruct> {
return error ? { isValid: false, reason: error } : { isValid: true }
}

private async startTimestamp(params: OwnerContext): Promise<number> {
const { startTime } = this.data

if (startTime?.startType === StartTimeValue.AT_EPOC) {
return startTime.epoch.toNumber()
}

const cabinet = await this.cabinet(params)
const cabinetEpoc = utils.defaultAbiCoder.decode(['uint256'], cabinet)[0]

if (cabinetEpoc === 0) {
throw new Error('Cabinet is not set. Required for TWAP orders that start at mining time.')
}

return parseInt(cabinet, 16)
}

/**
* Checks if the owner authorized the conditional order.
*
Expand All @@ -271,13 +292,44 @@ export class Twap extends ConditionalOrder<TwapData, TwapStruct> {
* @param provider An RPC provider for the chain.
* @returns true if the owner authorized the order, false otherwise.
*/
protected async pollValidate(
_owner: string,
_chain: SupportedChainId,
_provider: providers.Provider
): Promise<PollResultErrors | undefined> {
// TODO: Do not check again expired order
// TODO: Calculate the next part start time, signal to not check again until then
protected async pollValidate(params: PollParams): Promise<PollResultErrors | undefined> {
const { blockInfo = await getBlockInfo(params.provider) } = params
const { blockTimestamp } = blockInfo
const { numberOfParts, timeBetweenParts } = this.data

const startTimestamp = await this.startTimestamp(params)

if (startTimestamp > blockTimestamp) {
// The start time hasn't started
return {
result: PollResultCode.TRY_AT_EPOCH,
epoch: startTimestamp,
reason: `TWAP hasn't started yet. Starts at ${startTimestamp} (${formatEpoch(startTimestamp)})`,
}
}

const expirationTimestamp = startTimestamp + numberOfParts.mul(timeBetweenParts).toNumber()
if (blockTimestamp >= expirationTimestamp) {
// The order has expired
return {
result: PollResultCode.DONT_TRY_AGAIN,
reason: `TWAP has expired. Expired at ${expirationTimestamp} (${formatEpoch(expirationTimestamp)})`,
}
}

// TODO: Do not check between parts
// - 1. Check whats the order parameters for the current partNumber
// - 2. Derive discrete orderUid
// - 3. Verify if this is already created in the API
// - 4. If so, we know we should return
// return {
// result: PollResultCode.TRY_AT_EPOCH,
// epoch: nextPartStartTime,
// reason: `Current active TWAP part is already created. The next one doesn't start until ${nextPartStartTime} (${formatEpoch(nextPartStartTime)})`,
// }
// // Get current part number
// const partNumber = Math.floor(blockTimestamp - startTimestamp / timeBetweenParts.toNumber())

return undefined
}

Expand Down
26 changes: 25 additions & 1 deletion src/composable/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { SupportedChainId } from '../common'
import { GPv2Order } from './generated/ComposableCoW'
import { providers } from 'ethers'

export interface ConditionalOrderArguments<T> {
handler: string
Expand Down Expand Up @@ -79,6 +81,27 @@ export type ProofWithParams = {
params: ConditionalOrderParams
}

export type OwnerContext = {
owner: string
chainId: SupportedChainId
provider: providers.Provider
}

export type PollParams = OwnerContext & {
offchainInput: string
proof: string[]

/**
* If present, it can be used for custom conditional order validations. If not present, the orders will need to get the block info themselves
*/
blockInfo?: BlockInfo
}

export type BlockInfo = {
blockNumber: number
blockTimestamp: number
}

export type PollResult = PollResultSuccess | PollResultErrors

export type PollResultErrors =
Expand All @@ -93,7 +116,7 @@ export enum PollResultCode {
UNEXPECTED_ERROR = 'UNEXPECTED_ERROR',
TRY_NEXT_BLOCK = 'TRY_NEXT_BLOCK',
TRY_ON_BLOCK = 'TRY_ON_BLOCK',
TRY_AT_EPOCH = 'TRY_AT_DATE',
TRY_AT_EPOCH = 'TRY_AT_EPOCH',
DONT_TRY_AGAIN = 'DONT_TRY_AGAIN',
}
export interface PollResultSuccess {
Expand All @@ -105,6 +128,7 @@ export interface PollResultSuccess {
export interface PollResultUnexpectedError {
readonly result: PollResultCode.UNEXPECTED_ERROR
readonly error: unknown
reason?: string
}

export interface PollResultTryNextBlock {
Expand Down
15 changes: 14 additions & 1 deletion src/composable/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
SupportedChainId,
} from '../common'
import { ExtensibleFallbackHandler__factory } from './generated'
import { ConditionalOrderParams } from './types'
import { BlockInfo, ConditionalOrderParams } from './types'

// Define the ABI tuple for the ConditionalOrderParams struct
export const CONDITIONAL_ORDER_PARAMS_ABI = ['tuple(address handler, bytes32 salt, bytes staticInput)']
Expand Down Expand Up @@ -74,3 +74,16 @@ export function isValidAbi(types: readonly (string | utils.ParamType)[], values:
}
return true
}

export async function getBlockInfo(provider: providers.Provider): Promise<BlockInfo> {
const block = await provider.getBlock('latest')

return {
blockNumber: block.number,
blockTimestamp: block.timestamp,
}
}

export function formatEpoch(epoch: number): string {
return new Date(epoch * 1000).toISOString()
}

0 comments on commit 3a61922

Please sign in to comment.