Skip to content

Commit

Permalink
Twap test structure (#154)
Browse files Browse the repository at this point in the history
* Improve tests structure

* Restructure conditional order tests

* Delete dead test

* Correct name of test

* Delete duplicated test

* Correct name of tests

* Declare the context
  • Loading branch information
anxolin authored Aug 30, 2023
1 parent 3a61922 commit ef5bbf9
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 46 deletions.
69 changes: 44 additions & 25 deletions src/composable/ConditionalOrder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,43 +25,68 @@ const TWAP_SERIALIZED = (salt?: string, handler?: string): string => {
)
}

describe('ConditionalOrder', () => {
test('Create: constructor fails if invalid params', () => {
describe('Constuctor', () => {
test('Create TestConditionalOrder', () => {
// bad address
expect(() => new TestConditionalOrder('0xdeadbeef')).toThrow('Invalid handler: 0xdeadbeef')
// bad salt
})

expect(() => new TestConditionalOrder('0x910d00a310f7Dc5B29FE73458F47f519be547D3d', 'cowtomoon')).toThrow(
'Invalid salt: cowtomoon'
)
expect(() => new TestConditionalOrder('0x910d00a310f7Dc5B29FE73458F47f519be547D3d', '0xdeadbeef')).toThrow(
'Invalid salt: 0xdeadbeef'
)
expect(
() =>
new TestConditionalOrder(
'0x910d00a310f7Dc5B29FE73458F47f519be547D3d',
'0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'
)
).toThrow(
'Invalid salt: 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'
test('Fail if bad address', () => {
// bad address
expect(() => new TestConditionalOrder('0xdeadbeef')).toThrow('Invalid handler: 0xdeadbeef')
})

describe('Fail if bad salt', () => {
test('Fails if salt is not an hex', () => {
expect(() => new TestConditionalOrder('0x910d00a310f7Dc5B29FE73458F47f519be547D3d', 'cowtomoon')).toThrow(
'Invalid salt: cowtomoon'
)
})

test('Fails if salt is too short (not 32 bytes)', () => {
expect(() => new TestConditionalOrder('0x910d00a310f7Dc5B29FE73458F47f519be547D3d', '0xdeadbeef')).toThrow(
'Invalid salt: 0xdeadbeef'
)
})

test('Fails if salt is too long (not 32 bytes)', () => {
expect(
() =>
new TestConditionalOrder(
'0x910d00a310f7Dc5B29FE73458F47f519be547D3d',
'0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'
)
).toThrow(
'Invalid salt: 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'
)
})
})
})
describe('Deserialize: Decode static input', () => {
test('Fails if handler mismatch', () => {
expect(() => Twap.deserialize(TWAP_SERIALIZED(undefined, '0x9008D19f58AAbD9eD0D60971565AA8510560ab41'))).toThrow(
'HandlerMismatch'
)
})
})

describe('Serialize: Encode static input', () => {
test('Serialize: Fails if invalid params', () => {
const order = new TestConditionalOrder('0x910d00a310f7Dc5B29FE73458F47f519be547D3d')
expect(() => order.testEncodeStaticInput()).toThrow()
})
})

test('id: Returns correct id', () => {
describe('Compute orderUid', () => {
test('Returns correct id', () => {
const order = new TestConditionalOrder(
'0x910d00a310f7Dc5B29FE73458F47f519be547D3d',
'0x9379a0bf532ff9a66ffde940f94b1a025d6f18803054c1aef52dc94b15255bbe'
)
expect(order.id).toEqual('0x88ca0698d8c5500b31015d84fa0166272e1812320d9af8b60e29ae00153363b3')
})

test('leafToId: Returns correct id', () => {
test('Derive OrderId from leaf data', () => {
const order = new TestConditionalOrder(
'0x910d00a310f7Dc5B29FE73458F47f519be547D3d',
'0x9379a0bf532ff9a66ffde940f94b1a025d6f18803054c1aef52dc94b15255bbe'
Expand All @@ -70,12 +95,6 @@ describe('ConditionalOrder', () => {
'0x88ca0698d8c5500b31015d84fa0166272e1812320d9af8b60e29ae00153363b3'
)
})

test('Deserialize: Fails if handler mismatch', () => {
expect(() => Twap.deserialize(TWAP_SERIALIZED(undefined, '0x9008D19f58AAbD9eD0D60971565AA8510560ab41'))).toThrow(
'HandlerMismatch'
)
})
})

class TestConditionalOrder extends ConditionalOrder<string, string> {
Expand Down
14 changes: 11 additions & 3 deletions src/composable/ConditionalOrder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export abstract class ConditionalOrder<D, S> {
this.hasOffChainInput = hasOffChainInput
}

// TODO: https://github.com/cowprotocol/cow-sdk/issues/155
abstract get isSingleOrder(): boolean

/**
Expand Down Expand Up @@ -155,6 +156,15 @@ export abstract class ConditionalOrder<D, S> {
return utils.keccak256(this.serialize())
}

/**
* The context key of the order (bytes32(0) if a merkle tree is used, otherwise H(params)) with which to lookup the cabinet
*
* The context, relates to the 'ctx' in the contract: https://github.com/cowprotocol/composable-cow/blob/c7fb85ab10c05e28a1632ba97a1749fb261fcdfb/src/interfaces/IConditionalOrder.sol#L38
*/
protected get ctx(): string {
return this.isSingleOrder ? this.id : constants.HashZero
}

/**
* Get the `leaf` of the conditional order. This is the data that is used to create the merkle tree.
*
Expand Down Expand Up @@ -304,10 +314,8 @@ export abstract class ConditionalOrder<D, S> {
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)
return composableCow.callStatic.cabinet(owner, this.ctx)
}

/**
Expand Down
75 changes: 60 additions & 15 deletions src/composable/orderTypes/Twap.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,47 +59,81 @@ export function generateRandomTWAPData(): TwapData {
}
}

describe('Twap', () => {
test('Create: constructor creates valid TWAP', () => {
const twap = Twap.fromData(TWAP_PARAMS_TEST)
describe('Constructor', () => {
test('Create new valid TWAP', () => {
const twap = new Twap({ handler: TWAP_ADDRESS, data: TWAP_PARAMS_TEST })
expect(twap.orderType).toEqual('twap')
expect(twap.hasOffChainInput).toEqual(false)
expect(twap.offChainInput).toEqual('0x')
expect(twap.context?.address).not.toBeUndefined()
})

const twap2 = Twap.fromData({ ...TWAP_PARAMS_TEST, t0: BigNumber.from(1) })
expect(twap2.context).toBeUndefined()

test('Create Twap with invalid handler', () => {
expect(() => new Twap({ handler: '0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead', data: TWAP_PARAMS_TEST })).toThrow(
'InvalidHandler'
)
})
})

test('isValid: valid twap', () => {
describe('Twap.fromData', () => {
test('Creates valid TWAP: Start at mining time', () => {
const twap = Twap.fromData(TWAP_PARAMS_TEST)
expect(twap.orderType).toEqual('twap')
expect(twap.hasOffChainInput).toEqual(false)
expect(twap.offChainInput).toEqual('0x')
expect(twap.context?.address).not.toBeUndefined()
})

test('Creates valid TWAP: Start at epoch', () => {
const twap = Twap.fromData({
...TWAP_PARAMS_TEST,
startTime: { startType: StartTimeValue.AT_EPOC, epoch: BigNumber.from(1) },
})
expect(twap.context).toBeUndefined()
})
})

describe('Validate', () => {
test('Valid twap', () => {
expect(Twap.fromData({ ...TWAP_PARAMS_TEST }).isValid()).toEqual({ isValid: true })
})

test('isValid: invalid twap', () => {
test('Invalid twap: InvalidSameToken', () => {
expect(Twap.fromData({ ...TWAP_PARAMS_TEST, sellToken: TWAP_PARAMS_TEST.buyToken }).isValid()).toEqual({
isValid: false,
reason: 'InvalidSameToken',
})
})

test('Invalid twap: InvalidToken (sell)', () => {
expect(Twap.fromData({ ...TWAP_PARAMS_TEST, sellToken: constants.AddressZero }).isValid()).toEqual({
isValid: false,
reason: 'InvalidToken',
})
})

test('Invalid twap: InvalidToken (buy)', () => {
expect(Twap.fromData({ ...TWAP_PARAMS_TEST, buyToken: constants.AddressZero }).isValid()).toEqual({
isValid: false,
reason: 'InvalidToken',
})
})

test('Invalid twap: InvalidSellAmount', () => {
expect(Twap.fromData({ ...TWAP_PARAMS_TEST, sellAmount: BigNumber.from(0) }).isValid()).toEqual({
isValid: false,
reason: 'InvalidSellAmount',
})
})

test('Invalid twap: InvalidMinBuyAmount', () => {
expect(Twap.fromData({ ...TWAP_PARAMS_TEST, buyAmount: BigNumber.from(0) }).isValid()).toEqual({
isValid: false,
reason: 'InvalidMinBuyAmount',
})
})

test('Invalid twap: InvalidStartTime', () => {
expect(
Twap.fromData({
...TWAP_PARAMS_TEST,
Expand All @@ -109,14 +143,23 @@ describe('Twap', () => {
isValid: false,
reason: 'InvalidStartTime',
})
})

test('Invalid twap: InvalidNumParts', () => {
expect(Twap.fromData({ ...TWAP_PARAMS_TEST, numberOfParts: BigNumber.from(0) }).isValid()).toEqual({
isValid: false,
reason: 'InvalidNumParts',
})
})

test('Invalid twap: InvalidFrequency', () => {
expect(Twap.fromData({ ...TWAP_PARAMS_TEST, timeBetweenParts: BigNumber.from(0) }).isValid()).toEqual({
isValid: false,
reason: 'InvalidFrequency',
})
})

test('Invalid twap: InvalidSpan (limit duration)', () => {
expect(
Twap.fromData({
...TWAP_PARAMS_TEST,
Expand All @@ -129,35 +172,37 @@ describe('Twap', () => {
isValid: false,
reason: 'InvalidSpan',
})
expect(Twap.fromData({ ...TWAP_PARAMS_TEST, appData: constants.AddressZero }).isValid()).toEqual({
isValid: false,
reason: 'InvalidData',
})
})

test('isValid: Fails if appData has a wrong number of bytes', () => {
test('Invalid twap: InvalidData (ABI parse error in appData)', () => {
// The isValid below test triggers a throw by trying to ABI parse `appData` as a `bytes32` when
// it only has 20 bytes (ie. an address)
expect(Twap.fromData({ ...TWAP_PARAMS_TEST, appData: constants.AddressZero }).isValid()).toEqual({
isValid: false,
reason: 'InvalidData',
})
})
})

describe('Serialize', () => {
test('serialize: Serializes correctly', () => {
const twap = Twap.fromData(TWAP_PARAMS_TEST)
expect(twap.serialize()).toEqual(TWAP_SERIALIZED(twap.salt))
})
})

test('deserialize: Deserializes correctly', () => {
describe('Deserialize', () => {
test('Deserializes correctly', () => {
const twap = Twap.fromData(TWAP_PARAMS_TEST)
expect(Twap.deserialize(TWAP_SERIALIZED(twap.salt))).toMatchObject(twap)
})

test('deserialize: Throws if invalid', () => {
test('Throws if invalid', () => {
expect(() => Twap.deserialize('0x')).toThrow('InvalidSerializedConditionalOrder')
})
})

describe('To String', () => {
test('toString: Formats correctly', () => {
expect(Twap.fromData(TWAP_PARAMS_TEST).toString()).toEqual(
'twap: Sell total 0x6810e776880C02933D47DB1b9fc05908e5386b96@1000000000000000000 for a minimum of 0xDAE5F1590db13E3B40423B5b5c5fbf175515910b@1000000000000000000 over 10 parts with a spacing of 3600s beginning at time of mining'
Expand Down
5 changes: 2 additions & 3 deletions src/composable/orderTypes/Twap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import {
ConditionalOrderArguments,
ConditionalOrderParams,
ContextFactory,
IsNotValid,
IsValid,
OwnerContext,
IsValidResult,
PollParams,
PollResultCode,
PollResultErrors,
Expand Down Expand Up @@ -229,7 +228,7 @@ export class Twap extends ConditionalOrder<TwapData, TwapStruct> {
* @throws If the TWAP order is invalid.
* @see {@link TwapStruct} for the native struct.
*/
isValid(): IsValid | IsNotValid {
isValid(): IsValidResult {
const error = (() => {
const {
sellToken,
Expand Down

0 comments on commit ef5bbf9

Please sign in to comment.