From 3a61922b72cf226eda4adaa085d5129f07715461 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Wed, 30 Aug 2023 19:30:26 +0200 Subject: [PATCH] Handle Expired TWAP and not started TWAP (#153) * 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 --- package.json | 4 +- src/composable/ConditionalOrder.spec.ts | 2 + src/composable/ConditionalOrder.ts | 51 +++++++++++------- src/composable/orderTypes/Twap.ts | 72 +++++++++++++++++++++---- src/composable/types.ts | 26 ++++++++- src/composable/utils.ts | 15 +++++- 6 files changed, 136 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 9fcac0a0..5367a5d2 100644 --- a/package.json +++ b/package.json @@ -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" @@ -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": { @@ -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", diff --git a/src/composable/ConditionalOrder.spec.ts b/src/composable/ConditionalOrder.spec.ts index e49e0629..e2464191 100644 --- a/src/composable/ConditionalOrder.spec.ts +++ b/src/composable/ConditionalOrder.spec.ts @@ -79,6 +79,8 @@ describe('ConditionalOrder', () => { }) class TestConditionalOrder extends ConditionalOrder { + isSingleOrder = true + constructor(address: string, salt?: string, data = '0x') { super({ handler: address, diff --git a/src/composable/ConditionalOrder.ts b/src/composable/ConditionalOrder.ts index 5d0b58c7..d347ccec 100644 --- a/src/composable/ConditionalOrder.ts +++ b/src/composable/ConditionalOrder.ts @@ -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' @@ -7,11 +7,12 @@ import { ConditionalOrderParams, ContextFactory, IsValidResult, + OwnerContext, + PollParams, PollResult, PollResultCode, PollResultErrors, } from './types' -import { SupportedChainId } from '../common' import { getComposableCow, getComposableCowInterface } from './contracts' /** @@ -69,6 +70,8 @@ export abstract class ConditionalOrder { this.hasOffChainInput = hasOffChainInput } + abstract get isSingleOrder(): boolean + /** * Get a descriptive name for the type of the conditional order (i.e twap, dca, etc). * @@ -231,8 +234,9 @@ export abstract class ConditionalOrder { * @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 { - const composableCow = getComposableCow(chain, provider) + async poll(params: PollParams): Promise { + const { chainId, owner, provider } = params + const composableCow = getComposableCow(chainId, provider) try { const isValid = this.isValid() @@ -245,17 +249,17 @@ export abstract class ConditionalOrder { } // 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}`, } } @@ -283,33 +287,40 @@ export abstract class ConditionalOrder { /** * 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 { - const composableCow = getComposableCow(chain, provider) + public isAuthorized(params: OwnerContext): Promise { + 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 { + 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 + protected abstract pollValidate(params: PollParams): Promise /** * Convert the struct that the contract expect as an encoded `staticInput` into a friendly data object modeling the smart order. diff --git a/src/composable/orderTypes/Twap.ts b/src/composable/orderTypes/Twap.ts index 03fa9559..dece8231 100644 --- a/src/composable/orderTypes/Twap.ts +++ b/src/composable/orderTypes/Twap.ts @@ -1,4 +1,4 @@ -import { BigNumber, constants, providers } from 'ethers' +import { BigNumber, constants, utils } from 'ethers' import { ConditionalOrder } from '../ConditionalOrder' import { @@ -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' @@ -161,6 +163,8 @@ const DEFAULT_DURATION_OF_PART: DurationOfPart = { durationType: DurationType.AU * @author mfw78 */ export class Twap extends ConditionalOrder { + isSingleOrder = true + /** * @see {@link ConditionalOrder.constructor} * @throws If the TWAP order is invalid. @@ -263,6 +267,23 @@ export class Twap extends ConditionalOrder { return error ? { isValid: false, reason: error } : { isValid: true } } + private async startTimestamp(params: OwnerContext): Promise { + 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. * @@ -271,13 +292,44 @@ export class Twap extends ConditionalOrder { * @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 { - // 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 { + 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 } diff --git a/src/composable/types.ts b/src/composable/types.ts index 8edf7e0c..67f44ce0 100644 --- a/src/composable/types.ts +++ b/src/composable/types.ts @@ -1,4 +1,6 @@ +import { SupportedChainId } from '../common' import { GPv2Order } from './generated/ComposableCoW' +import { providers } from 'ethers' export interface ConditionalOrderArguments { handler: string @@ -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 = @@ -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 { @@ -105,6 +128,7 @@ export interface PollResultSuccess { export interface PollResultUnexpectedError { readonly result: PollResultCode.UNEXPECTED_ERROR readonly error: unknown + reason?: string } export interface PollResultTryNextBlock { diff --git a/src/composable/utils.ts b/src/composable/utils.ts index 004c6f08..d21ff4fc 100644 --- a/src/composable/utils.ts +++ b/src/composable/utils.ts @@ -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)'] @@ -74,3 +74,16 @@ export function isValidAbi(types: readonly (string | utils.ParamType)[], values: } return true } + +export async function getBlockInfo(provider: providers.Provider): Promise { + 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() +}