From 91738687149ab588750fa8b04f06b2b7e15daec9 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 14 Mar 2024 20:51:10 +0300 Subject: [PATCH] fix(whitelist): new format (#62) --- package.json | 2 +- pnpm-lock.yaml | 9 +- src/api/quoter/quote/quote.ts | 5 +- .../auction-calculator.spec.ts | 6 +- src/auction-calculator/auction-calculator.ts | 2 +- src/constants.ts | 1 + src/fusion-order/fusion-order.spec.ts | 9 +- src/fusion-order/fusion-order.ts | 27 +++- .../settlement-post-interaction-data.spec.ts | 74 ++++++++++- .../settlement-post-interaction-data.ts | 124 +++++++++++++----- .../settlement-post-interaction-data/types.ts | 8 +- src/utils/time.ts | 3 + tsconfig.json | 3 +- 13 files changed, 218 insertions(+), 55 deletions(-) create mode 100644 src/utils/time.ts diff --git a/package.json b/package.json index 7839221..1c5f71a 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "changelog:generate": "changelog generate -a" }, "dependencies": { - "@1inch/byte-utils": "2.0.0", + "@1inch/byte-utils": "2.1.0", "@1inch/limit-order-sdk": "4.2.0", "@metamask/eth-sig-util": "^5.0.2", "bn.js": "^5.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 29640e4..ec23d9f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,8 +6,8 @@ settings: dependencies: '@1inch/byte-utils': - specifier: 2.0.0 - version: 2.0.0 + specifier: 2.1.0 + version: 2.1.0 '@1inch/limit-order-sdk': specifier: 4.2.0 version: 4.2.0(assert@2.1.0)(axios@1.6.7) @@ -117,6 +117,11 @@ packages: engines: {node: '>=18.16.0'} dev: false + /@1inch/byte-utils@2.1.0: + resolution: {integrity: sha512-Exyt8hyX0SyJyikWvq9Xw8mFAf/qhYY5bHFW3KgWKpoMZKTK6BGzHptE224/i5pt1TPsWUA4M8oLHvPw9+CKjQ==} + engines: {node: '>=18.16.0'} + dev: false + /@1inch/eslint-config@1.4.3(@typescript-eslint/eslint-plugin@5.59.11)(@typescript-eslint/parser@5.51.0)(eslint-config-prettier@8.3.0)(eslint-config-standard@17.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-import@2.26.0)(eslint-plugin-n@16.6.2)(eslint-plugin-prettier@4.2.1)(eslint-plugin-promise@6.1.1)(eslint-plugin-unused-imports@2.0.0)(eslint@8.41.0)(prettier@2.8.8)(typescript@4.9.4): resolution: {integrity: sha512-I2fMNrQqoqhKCh6T+PVaXVeFEsOTawuYxBHXXa0+upgpzFkT1/IkoNkAnEJ19s+ne+9qlXIcD0ZCkptux1f9og==} peerDependencies: diff --git a/src/api/quoter/quote/quote.ts b/src/api/quoter/quote/quote.ts index a173eee..30068b5 100644 --- a/src/api/quoter/quote/quote.ts +++ b/src/api/quoter/quote/quote.ts @@ -6,6 +6,7 @@ import {Preset} from '../preset' import {AuctionWhitelistItem, FusionOrder} from '../../../fusion-order' import {QuoterRequest} from '../quoter.request' import {bpsToRatioFormat} from '../../../sdk' +import {now} from '../../../utils/time' export class Quote { /** @@ -129,14 +130,14 @@ export class Quote { return { address: resolver, - delay: isExclusive ? 0n : auctionStartTime + allowFrom: isExclusive ? now() : auctionStartTime } }) } return this.whitelist.map((resolver) => ({ address: resolver, - delay: 0n + allowFrom: 0n })) } } diff --git a/src/auction-calculator/auction-calculator.spec.ts b/src/auction-calculator/auction-calculator.spec.ts index a7d66b1..0472b81 100644 --- a/src/auction-calculator/auction-calculator.spec.ts +++ b/src/auction-calculator/auction-calculator.spec.ts @@ -14,8 +14,10 @@ describe('Auction Calculator', () => { receiver: Address.fromBigInt(1n) }, bankFee: 0n, - auctionStartTime, - whitelist: [] + resolvingStartTime: auctionStartTime, + whitelist: [ + {address: Address.ZERO_ADDRESS, allowFrom: auctionStartTime} + ] }) const auctionDetails = new AuctionDetails({ diff --git a/src/auction-calculator/auction-calculator.ts b/src/auction-calculator/auction-calculator.ts index 72e5bde..b1c671a 100644 --- a/src/auction-calculator/auction-calculator.ts +++ b/src/auction-calculator/auction-calculator.ts @@ -33,7 +33,7 @@ export class AuctionCalculator { details: AuctionDetails ): AuctionCalculator { return new AuctionCalculator( - data.auctionStartTime, + data.resolvingStartTime, details.duration, details.initialRateBump, details.points, diff --git a/src/constants.ts b/src/constants.ts index 1809bd2..480c996 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -16,6 +16,7 @@ export const ONE_INCH_LIMIT_ORDER_V4 = '0x111111125421ca6dc452d289314280a0f8842a65' export const UINT_160_MAX = (1n << 160n) - 1n +export const UINT_16_MAX = (1n << 16n) - 1n export const UINT_80_MAX = (1n << 80n) - 1n export const UINT_40_MAX = (1n << 40n) - 1n export const UINT_32_MAX = (1n << 32n) - 1n diff --git a/src/fusion-order/fusion-order.spec.ts b/src/fusion-order/fusion-order.spec.ts index c4a861f..952bbde 100644 --- a/src/fusion-order/fusion-order.spec.ts +++ b/src/fusion-order/fusion-order.spec.ts @@ -41,9 +41,10 @@ describe('Fusion Order', () => { address: new Address( '0x00000000219ab540356cbb839cbe05303d7705fa' ), - delay: 0n + allowFrom: 0n } - ] + ], + resolvingStartTime: 1673548139n } ) @@ -57,7 +58,7 @@ describe('Fusion Order', () => { takingAmount: '1420000000', makerTraits: '29852648006495581632639394572552351243421169944806257724550573036760110989312', - salt: '14832508939800728556409473652845244531014097925085' + salt: '14969955465678758833706505435513058355190519874774' }) const makerTraits = new MakerTraits(BigInt(builtOrder.makerTraits)) @@ -103,7 +104,7 @@ describe('Fusion Order', () => { address: new Address( '0x00000000219ab540356cbb839cbe05303d7705fa' ), - delay: 0n + allowFrom: 0n } ] } diff --git a/src/fusion-order/fusion-order.ts b/src/fusion-order/fusion-order.ts index f95fb0b..76cd23b 100644 --- a/src/fusion-order/fusion-order.ts +++ b/src/fusion-order/fusion-order.ts @@ -20,6 +20,7 @@ import {AuctionCalculator} from '../auction-calculator' import {add0x} from '../utils' import {ZX} from '../constants' import {calcTakingAmount} from '../utils/amounts' +import {now} from '../utils/time' export class FusionOrder { private static defaultExtra = { @@ -49,6 +50,9 @@ export class FusionOrder { * Required if `allowPartialFills` is false */ nonce?: bigint + /** + * 0x prefixed without the token address + */ permit?: string /** * Default is true @@ -182,8 +186,12 @@ export class FusionOrder { bankFee?: bigint } whitelist: AuctionWhitelistItem[] + /** + * Time from which order can be executed + */ + resolvingStartTime?: bigint }, - extra: { + extra?: { unwrapWETH?: boolean /** * Required if `allowPartialFills` is false @@ -205,7 +213,7 @@ export class FusionOrder { */ orderExpirationDelay?: bigint enablePermit2?: boolean - } = FusionOrder.defaultExtra + } ): FusionOrder { return new FusionOrder( settlementExtension, @@ -215,7 +223,7 @@ export class FusionOrder { bankFee: details.fees?.bankFee || 0n, integratorFee: details.fees?.integratorFee, whitelist: details.whitelist, - auctionStartTime: details.auction.startTime + resolvingStartTime: details.resolvingStartTime ?? now() }), extra ) @@ -332,4 +340,17 @@ export class FusionOrder { return calculator.calcAuctionTakingAmount(takingAmount, bump) } + + /** + * Check whether address allowed to execute order at the given time + * + * @param executor address of executor + * @param executionTime timestamp in sec at which order planning to execute + */ + public canExecuteAt(executor: Address, executionTime: bigint): boolean { + return this.fusionExtension.postInteractionData.canExecuteAt( + executor, + executionTime + ) + } } diff --git a/src/fusion-order/settlement-post-interaction-data/settlement-post-interaction-data.spec.ts b/src/fusion-order/settlement-post-interaction-data/settlement-post-interaction-data.spec.ts index cc8b49d..5f94e14 100644 --- a/src/fusion-order/settlement-post-interaction-data/settlement-post-interaction-data.spec.ts +++ b/src/fusion-order/settlement-post-interaction-data/settlement-post-interaction-data.spec.ts @@ -6,8 +6,13 @@ describe('PostInteractionData', () => { it('Should encode/decode with no fees and whitelist', () => { const data = SettlementPostInteractionData.new({ bankFee: 0n, - auctionStartTime: 1708117482n, - whitelist: [] + resolvingStartTime: 1708117482n, + whitelist: [ + { + address: Address.ZERO_ADDRESS, + allowFrom: 0n + } + ] }) expect( @@ -18,11 +23,11 @@ describe('PostInteractionData', () => { it('Should encode/decode with fees and whitelist', () => { const data = SettlementPostInteractionData.new({ bankFee: 0n, - auctionStartTime: 1708117482n, + resolvingStartTime: 1708117482n, whitelist: [ { address: Address.ZERO_ADDRESS, - delay: 0n + allowFrom: 0n } ], integratorFee: { @@ -35,4 +40,65 @@ describe('PostInteractionData', () => { SettlementPostInteractionData.decode(data.encode()) ).toStrictEqual(data) }) + + it('Should generate correct whitelist', () => { + const start = 1708117482n + + const data = SettlementPostInteractionData.new({ + resolvingStartTime: start, + whitelist: [ + { + address: Address.fromBigInt(2n), + allowFrom: start + 1000n + }, + { + address: Address.ZERO_ADDRESS, + allowFrom: start - 10n // should be set to start + }, + { + address: Address.fromBigInt(1n), + allowFrom: start + 10n + }, + { + address: Address.fromBigInt(3n), + allowFrom: start + 10n + } + ] + }) + + expect(data.whitelist).toStrictEqual([ + { + addressHalf: Address.ZERO_ADDRESS.toString().slice(-20), + delay: 0n + }, + { + addressHalf: Address.fromBigInt(1n).toString().slice(-20), + delay: 10n + }, + { + addressHalf: Address.fromBigInt(3n).toString().slice(-20), + delay: 0n + }, + { + addressHalf: Address.fromBigInt(2n).toString().slice(-20), + delay: 990n + } + ]) + + expect(data.canExecuteAt(Address.fromBigInt(1n), start + 10n)).toEqual( + true + ) + expect(data.canExecuteAt(Address.fromBigInt(1n), start + 9n)).toEqual( + false + ) + expect(data.canExecuteAt(Address.fromBigInt(3n), start + 10n)).toEqual( + true + ) + expect(data.canExecuteAt(Address.fromBigInt(3n), start + 9n)).toEqual( + false + ) + expect(data.canExecuteAt(Address.fromBigInt(2n), start + 50n)).toEqual( + false + ) + }) }) diff --git a/src/fusion-order/settlement-post-interaction-data/settlement-post-interaction-data.ts b/src/fusion-order/settlement-post-interaction-data/settlement-post-interaction-data.ts index c2a4c79..57d42c4 100644 --- a/src/fusion-order/settlement-post-interaction-data/settlement-post-interaction-data.ts +++ b/src/fusion-order/settlement-post-interaction-data/settlement-post-interaction-data.ts @@ -1,10 +1,11 @@ import {ethers} from 'ethers' import {Address} from '@1inch/limit-order-sdk' -import {BytesIter} from '@1inch/byte-utils' +import {BitMask, BN, BytesIter} from '@1inch/byte-utils' import assert from 'assert' import {IntegratorFee, SettlementSuffixData} from './types' import {isHexBytes} from '../../validations' import {add0x} from '../../utils' +import {UINT_16_MAX} from '../../constants' export class SettlementPostInteractionData { public readonly whitelist: WhitelistItem[] @@ -13,41 +14,65 @@ export class SettlementPostInteractionData { public readonly bankFee: bigint - public readonly auctionStartTime: bigint + public readonly resolvingStartTime: bigint private constructor(data: { whitelist: WhitelistItem[] integratorFee?: IntegratorFee - bankFee: bigint - auctionStartTime: bigint + bankFee?: bigint + resolvingStartTime: bigint }) { this.whitelist = data.whitelist this.integratorFee = data?.integratorFee - this.bankFee = data.bankFee - this.auctionStartTime = data.auctionStartTime + this.bankFee = data.bankFee || 0n + this.resolvingStartTime = data.resolvingStartTime } static new(data: SettlementSuffixData): SettlementPostInteractionData { - return new SettlementPostInteractionData({ - ...data, - whitelist: data.whitelist.map((d) => ({ + assert(data.whitelist.length, 'Whitelist can not be empty') + + // transform timestamps to cumulative delays + let sumDelay = 0n + const whitelist = data.whitelist + .map((d) => ({ addressHalf: d.address.toString().slice(-20), - delay: d.delay + allowFrom: + d.allowFrom < data.resolvingStartTime + ? data.resolvingStartTime + : d.allowFrom })) + .sort((a, b) => Number(a.allowFrom - b.allowFrom)) // ASC + .map((val) => { + const delay = val.allowFrom - data.resolvingStartTime - sumDelay + sumDelay += delay + + assert(delay < UINT_16_MAX, 'Too big diff between timestamps') + + return { + delay, + addressHalf: val.addressHalf + } + }) + + return new SettlementPostInteractionData({ + ...data, + whitelist }) } /** - * Construct `PostInteractionData` from bytes + * Construct `SettlementPostInteractionData` from bytes * @param data bytes with 0x prefix in next format: - * - uint8 feeType, first bit enabled when fee bank exists, second bit enabled when integrator fee exists - * - [uint32 feeBank] only when first bit of feeType enabled - * - [uint160 integratorFeeReceiver, uint32 integratorFeeRation] only when second bit of feeType enabled + * - [uint32 feeBank] only when first bit of `bitMask` enabled + * - [uint160 integratorFeeReceiver, uint32 integratorFeeRation] only when second bit of `bitMask` enabled * - uint32 auctionStartTime * - (bytes10 last10bytesOfAddress, uint16 auctionDelay) * N whitelist info + * - uint8 bitMask: + * 0b0000_0001 - fee bank mask + * 0b0000_0010 - integrator fee mask + * 0b1111_1000 - resolvers count mask * * All data is tight packed - * * @see SettlementPostInteractionData.encode */ static decode(data: string): SettlementPostInteractionData { @@ -58,17 +83,17 @@ export class SettlementPostInteractionData { const iter = BytesIter.BigInt(data) - const feeType = iter.nextByte() + const extra = iter.nextByte(BytesIter.SIDE.Back) let bankFee = 0n let integratorFee: IntegratorFee | undefined // fee bank presented - if ((feeType & 1n) === 1n) { + if ((extra & 1n) === 1n) { bankFee = iter.nextUint32() } // integrator fee presented - if ((feeType & 2n) === 2n) { + if ((extra & 2n) === 2n) { const integratorAddress = iter.nextUint160() const integratorFeeRatio = iter.nextUint32() @@ -78,7 +103,7 @@ export class SettlementPostInteractionData { } } - const auctionStartTime = iter.nextUint32() + const resolvingStartTime = iter.nextUint32() const whitelist = [] as WhitelistItem[] @@ -87,18 +112,18 @@ export class SettlementPostInteractionData { .nextBytes(10) .toString(16) .padStart(20, '0') - const allowance = iter.nextUint16() + const delay = iter.nextUint16() whitelist.push({ addressHalf, - delay: allowance + delay }) } return new SettlementPostInteractionData({ integratorFee, bankFee, - auctionStartTime, + resolvingStartTime, whitelist }) } @@ -107,16 +132,18 @@ export class SettlementPostInteractionData { * Serialize post-interaction data to bytes */ public encode(): string { - const fee = { - type: 'uint8', - value: 0n - } + /** + * 0b0000_0001 - fee bank mask + * 0b0000_0010 - integrator fee mask + * 0b1111_1000 - resolvers count mask + */ + let bitMask = new BN(0n) - const data = [fee] as {type: string; value: string | bigint}[] + const data = [] as {type: string; value: string | bigint}[] // Add bank fee if exists if (this.bankFee) { - fee.value |= 1n + bitMask = bitMask.setBit(0n, 1) data.push({ type: 'uint32', value: ethers.solidityPacked(['uint32'], [this.bankFee]) @@ -125,7 +152,7 @@ export class SettlementPostInteractionData { // add integrator fee if exists if (this.integratorFee?.ratio) { - fee.value |= 2n + bitMask = bitMask.setBit(1n, 1) data.push( { type: 'uint160', @@ -146,7 +173,7 @@ export class SettlementPostInteractionData { data.push({ type: 'uint32', - value: this.auctionStartTime + value: this.resolvingStartTime }) // whitelist data @@ -163,11 +190,45 @@ export class SettlementPostInteractionData { ) } + bitMask = bitMask.setMask( + new BitMask(3n, 8n), + BigInt(this.whitelist.length) + ) + + data.push({ + type: 'uint8', + value: bitMask.value + }) + return ethers.solidityPacked( data.map((d) => d.type), data.map((d) => d.value) ) } + + /** + * Check whether address allowed to execute order at the given time + * + * @param executor address of executor + * @param executionTime timestamp in sec at which order planning to execute + */ + public canExecuteAt(executor: Address, executionTime: bigint): boolean { + const addressHalf = executor.toString().slice(-20) + + let allowedFrom = this.resolvingStartTime + + for (const whitelist of this.whitelist) { + allowedFrom += whitelist.delay + + if (addressHalf === whitelist.addressHalf) { + return executionTime >= allowedFrom + } else if (executionTime < allowedFrom) { + return false + } + } + + return false + } } type WhitelistItem = { @@ -176,7 +237,8 @@ type WhitelistItem = { */ addressHalf: string /** - * Delay from auction start in seconds + * Delay from previous resolver in seconds + * For first resolver delay from `resolvingStartTime` */ delay: bigint } diff --git a/src/fusion-order/settlement-post-interaction-data/types.ts b/src/fusion-order/settlement-post-interaction-data/types.ts index 7103f0a..b52290e 100644 --- a/src/fusion-order/settlement-post-interaction-data/types.ts +++ b/src/fusion-order/settlement-post-interaction-data/types.ts @@ -3,16 +3,16 @@ import {Address} from '@1inch/limit-order-sdk' export type AuctionWhitelistItem = { address: Address /** - * Delay from auction start in seconds + * Timestamp in sec at which address can start resolving */ - delay: bigint + allowFrom: bigint } export type SettlementSuffixData = { whitelist: AuctionWhitelistItem[] integratorFee?: IntegratorFee - bankFee: bigint - auctionStartTime: bigint + bankFee?: bigint + resolvingStartTime: bigint } export type IntegratorFee = { diff --git a/src/utils/time.ts b/src/utils/time.ts new file mode 100644 index 0000000..fe4d268 --- /dev/null +++ b/src/utils/time.ts @@ -0,0 +1,3 @@ +export function now(): bigint { + return BigInt(Math.floor(Date.now() / 1000)) +} diff --git a/tsconfig.json b/tsconfig.json index 57a335f..ac915c7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "./dist", "rootDir": "./src", "lib": ["ES2021"], - "target": "ES2021" + "target": "ES2021", + "removeComments": false }, "include": ["./src"] }