• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cowprotocol / cow-sdk / 6027054998

30 Aug 2023 03:39PM UTC coverage: 72.617% (-3.8%) from 76.379%
6027054998

Pull #153

github

anxolin
Decode cabinet and verify its value
Pull Request #153: Handle Expired TWAP and not started TWAP

185 of 273 branches covered (0.0%)

Branch coverage included in aggregate %.

31 of 31 new or added lines in 3 files covered. (100.0%)

356 of 472 relevant lines covered (75.42%)

19.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

71.22
/src/composable/orderTypes/Twap.ts
1
import { BigNumber, constants, utils } from 'ethers'
2

3
import { ConditionalOrder } from '../ConditionalOrder'
4
import {
5
  ConditionalOrderArguments,
6
  ConditionalOrderParams,
7
  ContextFactory,
8
  IsNotValid,
9
  IsValid,
10
  OwnerContext,
11
  PollParams,
12
  PollResultCode,
13
  PollResultErrors,
14
} from '../types'
15
import { encodeParams, formatEpoch, getBlockInfo, isValidAbi } from '../utils'
16

17
// The type of Conditional Order
18
const TWAP_ORDER_TYPE = 'twap'
4✔
19
// The address of the TWAP handler contract
20
export const TWAP_ADDRESS = '0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5'
4✔
21
/**
22
 * The address of the `CurrentBlockTimestampFactory` contract
23
 *
24
 * **NOTE**: This is used in the event that TWAP's have a `t0` of `0`.
25
 */
26
export const CURRENT_BLOCK_TIMESTAMP_FACTORY_ADDRESS = '0x52eD56Da04309Aca4c3FECC595298d80C2f16BAc'
4✔
27

28
export const MAX_UINT32 = BigNumber.from(2).pow(32).sub(1) // 2^32 - 1
4✔
29
export const MAX_FREQUENCY = BigNumber.from(365 * 24 * 60 * 60) // 1 year
4✔
30

31
// Define the ABI tuple for the TWAPData struct
32
const TWAP_STRUCT_ABI = [
4✔
33
  'tuple(address sellToken, address buyToken, address receiver, uint256 partSellAmount, uint256 minPartLimit, uint256 t0, uint256 n, uint256 t, uint256 span, bytes32 appData)',
34
]
35

36
const DEFAULT_TOKEN_FORMATTER = (address: string, amount: BigNumber) => `${address}@${amount}`
8✔
37

38
/**
39
 * Base parameters for a TWAP order. Shared by:
40
 *   - TwapStruct (modeling the contract's struct used for `staticInput`).
41
 *   - TwapData (modeling the friendly SDK interface).
42
 */
43
export type TwapDataBase = {
44
  /**
45
   * which token to sell
46
   */
47
  readonly sellToken: string
48

49
  /**
50
   * which token to buy
51
   */
52
  readonly buyToken: string
53

54
  /**
55
   * who to send the tokens to
56
   */
57
  readonly receiver: string
58

59
  /**
60
   * Meta-data associated with the order. Normally would be the keccak256 hash of the document generated in http://github.com/cowprotocol/app-data
61
   *
62
   * This hash should have been uploaded to the API https://api.cow.fi/docs/#/default/put_api_v1_app_data__app_data_hash_ and potentially to other data availability protocols like IPFS.
63
   *
64
   */
65
  readonly appData: string
66
}
67

68
/**
69
 * Parameters for a TWAP order, as expected by the contract's `staticInput`.
70
 */
71
export interface TwapStruct extends TwapDataBase {
72
  /**
73
   * amount of sellToken to sell in each part
74
   */
75
  readonly partSellAmount: BigNumber
76

77
  /**
78
   * minimum amount of buyToken that must be bought in each part
79
   */
80
  readonly minPartLimit: BigNumber
81

82
  /**
83
   * start time of the TWAP
84
   */
85
  readonly t0: BigNumber
86

87
  /**
88
   * number of parts
89
   */
90
  readonly n: BigNumber
91

92
  /**
93
   * duration of the TWAP interval
94
   */
95
  readonly t: BigNumber
96

97
  /**
98
   * whether the TWAP is valid for the entire interval or not
99
   */
100
  readonly span: BigNumber
101
}
102

103
/**
104
 * Parameters for a TWAP order, made a little more user-friendly for SDK users.
105
 *
106
 * @see {@link TwapStruct} for the native struct.
107
 */
108
export interface TwapData extends TwapDataBase {
109
  /**
110
   * total amount of sellToken to sell across the entire TWAP
111
   */
112
  readonly sellAmount: BigNumber
113

114
  /**
115
   * minimum amount of buyToken that must be bought across the entire TWAP
116
   */
117
  readonly buyAmount: BigNumber
118

119
  /**
120
   * start time of the TWAP
121
   */
122
  readonly startTime?: StartTime
123

124
  /**
125
   * number of parts
126
   */
127
  readonly numberOfParts: BigNumber
128

129
  /**
130
   * duration of the TWAP interval
131
   */
132
  readonly timeBetweenParts: BigNumber
133

134
  /**
135
   * whether the TWAP is valid for the entire interval or not
136
   */
137
  readonly durationOfPart?: DurationOfPart
138
}
139

140
export type DurationOfPart =
141
  | { durationType: DurationType.AUTO }
142
  | { durationType: DurationType.LIMIT_DURATION; duration: BigNumber }
143

144
export enum DurationType {
145
  AUTO = 'AUTO',
146
  LIMIT_DURATION = 'LIMIT_DURATION',
147
}
148

149
export type StartTime =
150
  | { startType: StartTimeValue.AT_MINING_TIME }
151
  | { startType: StartTimeValue.AT_EPOC; epoch: BigNumber }
152

153
export enum StartTimeValue {
154
  AT_MINING_TIME = 'AT_MINING_TIME',
155
  AT_EPOC = 'AT_EPOC',
156
}
157

158
const DEFAULT_START_TIME: StartTime = { startType: StartTimeValue.AT_MINING_TIME }
4✔
159
const DEFAULT_DURATION_OF_PART: DurationOfPart = { durationType: DurationType.AUTO }
4✔
160

161
/**
162
 * `ComposableCoW` implementation of a TWAP order.
163
 * @author mfw78 <mfw78@rndlabs.xyz>
164
 */
165
export class Twap extends ConditionalOrder<TwapData, TwapStruct> {
166
  isSingleOrder = true
89✔
167

168
  /**
169
   * @see {@link ConditionalOrder.constructor}
170
   * @throws If the TWAP order is invalid.
171
   * @throws If the TWAP order is not ABI-encodable.
172
   * @throws If the handler is not the TWAP address.
173
   */
174
  constructor(params: ConditionalOrderArguments<TwapData>) {
175
    const { handler, salt, data: staticInput, hasOffChainInput } = params
91✔
176

177
    // First, verify that the handler is the TWAP address
178
    if (handler !== TWAP_ADDRESS) throw new Error(`InvalidHandler: Expected: ${TWAP_ADDRESS}, provided: ${handler}`)
91✔
179

180
    // Third, construct the base class using transformed parameters
181
    super({ handler: TWAP_ADDRESS, salt, data: staticInput, hasOffChainInput })
89✔
182
  }
183

184
  /**
185
   * Create a TWAP order with sound defaults.
186
   * @param data The TWAP order parameters in a more user-friendly format.
187
   * @returns An instance of the TWAP order.
188
   */
189
  static fromData(data: TwapData): Twap {
190
    return new Twap({ handler: TWAP_ADDRESS, data })
77✔
191
  }
192

193
  /**
194
   * Create a TWAP order with sound defaults.
195
   * @param data The TWAP order parameters in a more user-friendly format.
196
   * @returns An instance of the TWAP order.
197
   */
198
  static fromParams(params: ConditionalOrderParams): Twap {
199
    return Twap.deserialize(encodeParams(params))
×
200
  }
201

202
  /**
203
   * Enforces that TWAPs will commence at the beginning of a block by use of the
204
   * `CurrentBlockTimestampFactory` contract to provide the current block timestamp
205
   * as the start time of the TWAP.
206
   */
207
  get context(): ContextFactory | undefined {
208
    if (this.staticInput.t0.gt(0)) {
4✔
209
      return super.context
2✔
210
    } else {
211
      return {
2✔
212
        address: CURRENT_BLOCK_TIMESTAMP_FACTORY_ADDRESS,
213
        factoryArgs: undefined,
214
      }
215
    }
216
  }
217

218
  /**
219
   * @inheritdoc {@link ConditionalOrder.orderType}
220
   */
221
  get orderType(): string {
222
    return TWAP_ORDER_TYPE
33✔
223
  }
224

225
  /**
226
   * Validate the TWAP order.
227
   * @param data The TWAP order to validate.
228
   * @returns Whether the TWAP order is valid.
229
   * @throws If the TWAP order is invalid.
230
   * @see {@link TwapStruct} for the native struct.
231
   */
232
  isValid(): IsValid | IsNotValid {
233
    const error = (() => {
62✔
234
      const {
235
        sellToken,
236
        sellAmount,
237
        buyToken,
238
        buyAmount,
239
        startTime = DEFAULT_START_TIME,
×
240
        numberOfParts,
241
        timeBetweenParts,
242
        durationOfPart = DEFAULT_DURATION_OF_PART,
×
243
      } = this.data
62✔
244

245
      // Verify that the order params are logically valid
246
      if (!(sellToken != buyToken)) return 'InvalidSameToken'
62✔
247
      if (!(sellToken != constants.AddressZero && buyToken != constants.AddressZero)) return 'InvalidToken'
60✔
248
      if (!sellAmount.gt(constants.Zero)) return 'InvalidSellAmount'
56✔
249
      if (!buyAmount.gt(constants.Zero)) return 'InvalidMinBuyAmount'
54✔
250
      if (startTime.startType === StartTimeValue.AT_EPOC) {
52✔
251
        const t0 = startTime.epoch
2✔
252
        if (!(t0.gte(constants.Zero) && t0.lt(MAX_UINT32))) return 'InvalidStartTime'
2!
253
      }
254
      if (!(numberOfParts.gt(constants.One) && numberOfParts.lte(MAX_UINT32))) return 'InvalidNumParts'
50✔
255
      if (!(timeBetweenParts.gt(constants.Zero) && timeBetweenParts.lte(MAX_FREQUENCY))) return 'InvalidFrequency'
48✔
256
      if (durationOfPart.durationType === DurationType.LIMIT_DURATION) {
45✔
257
        if (!durationOfPart.duration.lte(timeBetweenParts)) return 'InvalidSpan'
2!
258
      }
259

260
      // Verify that the staticInput derived from the data is ABI-encodable
261
      if (!isValidAbi(TWAP_STRUCT_ABI, [this.staticInput])) return 'InvalidData'
43✔
262

263
      // No errors
264
      return undefined
39✔
265
    })()
266

267
    return error ? { isValid: false, reason: error } : { isValid: true }
62✔
268
  }
269

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

273
    if (startTime?.startType === StartTimeValue.AT_EPOC) {
×
274
      return startTime.epoch.toNumber()
×
275
    }
276

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

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

284
    return parseInt(cabinet, 16)
×
285
  }
286

287
  /**
288
   * Checks if the owner authorized the conditional order.
289
   *
290
   * @param owner The owner of the conditional order.
291
   * @param chain Which chain to use for the ComposableCoW contract.
292
   * @param provider An RPC provider for the chain.
293
   * @returns true if the owner authorized the order, false otherwise.
294
   */
295
  protected async pollValidate(params: PollParams): Promise<PollResultErrors | undefined> {
296
    const { blockInfo = await getBlockInfo(params.provider) } = params
×
297
    const { blockTimestamp } = blockInfo
×
298
    const { numberOfParts, timeBetweenParts } = this.data
×
299

300
    const startTimestamp = await this.startTimestamp(params)
×
301

302
    if (startTimestamp > blockTimestamp) {
×
303
      // The start time hasn't started
304
      return {
×
305
        result: PollResultCode.TRY_AT_EPOCH,
306
        epoch: startTimestamp,
307
        reason: `TWAP hasn't started yet. Starts at ${startTimestamp} (${formatEpoch(startTimestamp)})`,
308
      }
309
    }
310

311
    const expirationTimestamp = startTimestamp + numberOfParts.mul(timeBetweenParts).toNumber()
×
312
    if (blockTimestamp >= expirationTimestamp) {
×
313
      // The order has expired
314
      return {
×
315
        result: PollResultCode.DONT_TRY_AGAIN,
316
        reason: `TWAP has expired. Expired at ${expirationTimestamp} (${formatEpoch(expirationTimestamp)})`,
317
      }
318
    }
319

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

333
    return undefined
×
334
  }
335

336
  /**
337
   * Serialize the TWAP order into it's ABI-encoded form.
338
   * @returns {string} The ABI-encoded TWAP order.
339
   */
340
  serialize(): string {
341
    return encodeParams(this.leaf)
48✔
342
  }
343

344
  /**
345
   * Get the encoded static input for the TWAP order.
346
   * @returns {string} The ABI-encoded TWAP order.
347
   */
348
  encodeStaticInput(): string {
349
    return super.encodeStaticInputHelper(TWAP_STRUCT_ABI, this.staticInput)
97✔
350
  }
351

352
  /**
353
   * Deserialize a TWAP order from it's ABI-encoded form.
354
   * @param {string} twapSerialized ABI-encoded TWAP order to deserialize.
355
   * @returns A deserialized TWAP order.
356
   */
357
  static deserialize(twapSerialized: string): Twap {
358
    return super.deserializeHelper(
5✔
359
      twapSerialized,
360
      TWAP_ADDRESS,
361
      TWAP_STRUCT_ABI,
362
      (struct: TwapStruct, salt: string) =>
363
        new Twap({
2✔
364
          handler: TWAP_ADDRESS,
365
          salt,
366
          data: transformStructToData(struct),
367
        })
368
    )
369
  }
370

371
  /**
372
   * Create a human-readable string representation of the TWAP order.
373
   * @param {((address: string, amount: BigNumber) => string) | undefined} tokenFormatter An optional
374
   *        function that takes an address and an amount and returns a human-readable string.
375
   * @returns {string} A human-readable string representation of the TWAP order.
376
   */
377
  toString(tokenFormatter = DEFAULT_TOKEN_FORMATTER): string {
4✔
378
    const {
379
      sellToken,
380
      buyToken,
381
      numberOfParts,
382
      timeBetweenParts = DEFAULT_DURATION_OF_PART,
×
383
      startTime = DEFAULT_START_TIME,
×
384
      sellAmount,
385
      buyAmount,
386
    } = this.data
4✔
387

388
    const sellAmountFormatted = tokenFormatter(sellToken, sellAmount)
4✔
389
    const buyAmountFormatted = tokenFormatter(buyToken, buyAmount)
4✔
390
    const t0Formatted =
391
      startTime.startType === StartTimeValue.AT_MINING_TIME ? 'time of mining' : 'epoch ' + startTime.epoch.toString()
4✔
392
    return `${this.orderType}: Sell total ${sellAmountFormatted} for a minimum of ${buyAmountFormatted} over ${numberOfParts} parts with a spacing of ${timeBetweenParts}s beginning at ${t0Formatted}`
4✔
393
  }
394

395
  /**
396
   * Transform parameters into a native struct.
397
   *
398
   * @param {TwapData} data As passed by the consumer of the API.
399
   * @returns {TwapStruct} A formatted struct as expected by the smart contract.
400
   */
401
  transformDataToStruct(data: TwapData): TwapStruct {
402
    return transformDataToStruct(data)
89✔
403
  }
404

405
  /**
406
   * Transform parameters into a TWAP order struct.
407
   *
408
   * @param {TwapData} params As passed by the consumer of the API.
409
   * @returns {TwapStruct} A formatted struct as expected by the smart contract.
410
   */
411
  transformStructToData(struct: TwapStruct): TwapData {
412
    return transformStructToData(struct)
×
413
  }
414
}
415

416
/**
417
 * Transform parameters into a native struct.
418
 *
419
 * @param {TwapData} data As passed by the consumer of the API.
420
 * @returns {TwapStruct} A formatted struct as expected by the smart contract.
421
 */
422
export function transformDataToStruct(data: TwapData): TwapStruct {
423
  const {
424
    sellAmount,
425
    buyAmount,
426
    numberOfParts,
427
    startTime: startTime = DEFAULT_START_TIME,
×
428
    timeBetweenParts,
429
    durationOfPart = DEFAULT_DURATION_OF_PART,
×
430
    ...rest
431
  } = data
90✔
432

433
  const { partSellAmount, minPartLimit } =
434
    numberOfParts && !numberOfParts.isZero()
90✔
435
      ? {
436
          partSellAmount: sellAmount.div(numberOfParts),
437
          minPartLimit: buyAmount.div(numberOfParts),
438
        }
439
      : {
440
          partSellAmount: constants.Zero,
441
          minPartLimit: constants.Zero,
442
        }
443

444
  const span = durationOfPart.durationType === DurationType.AUTO ? constants.Zero : durationOfPart.duration
90✔
445
  const t0 = startTime.startType === StartTimeValue.AT_MINING_TIME ? constants.Zero : startTime.epoch
90✔
446

447
  return {
90✔
448
    partSellAmount,
449
    minPartLimit,
450
    t0,
451
    n: numberOfParts,
452
    t: timeBetweenParts,
453
    span,
454
    ...rest,
455
  }
456
}
457

458
/**
459
 * Transform parameters into a TWAP order struct.
460
 *
461
 * @param {TwapData} params As passed by the consumer of the API.
462
 * @returns {TwapStruct} A formatted struct as expected by the smart contract.
463
 */
464
export function transformStructToData(struct: TwapStruct): TwapData {
465
  const { n: numberOfParts, partSellAmount, minPartLimit, t: timeBetweenParts, t0: startEpoch, span, ...rest } = struct
2✔
466

467
  const durationOfPart: DurationOfPart = span.isZero()
2!
468
    ? { durationType: DurationType.AUTO }
469
    : { durationType: DurationType.LIMIT_DURATION, duration: span }
470

471
  const startTime: StartTime = span.isZero()
2!
472
    ? { startType: StartTimeValue.AT_MINING_TIME }
473
    : { startType: StartTimeValue.AT_EPOC, epoch: startEpoch }
474

475
  return {
2✔
476
    sellAmount: partSellAmount.mul(numberOfParts),
477
    buyAmount: minPartLimit.mul(numberOfParts),
478
    startTime,
479
    numberOfParts,
480
    timeBetweenParts,
481
    durationOfPart,
482
    ...rest,
483
  }
484
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc