diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b701a073..499cd1a95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## vNEXT - Migrate unit test files to Typescript & Hardhat: + - IexecOrderManagement (#101) - IexecMaintenance (#100) - IexecEscrowNative (#99) - IexecERC20 (#98) diff --git a/test/byContract/IexecOrderManagement/IexecOrderManagement.ts b/test/byContract/IexecOrderManagement/IexecOrderManagement.ts new file mode 100644 index 000000000..a98cd2b07 --- /dev/null +++ b/test/byContract/IexecOrderManagement/IexecOrderManagement.ts @@ -0,0 +1,348 @@ +// SPDX-FileCopyrightText: 2020-2024 IEXEC BLOCKCHAIN TECH +// SPDX-License-Identifier: Apache-2.0 + +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { ethers, expect } from 'hardhat'; +import { loadHardhatFixtureDeployment } from '../../../scripts/hardhat-fixture-deployer'; +import { + IexecInterfaceNative, + IexecInterfaceNative__factory, + IexecLibOrders_v5, +} from '../../../typechain'; +import { NULL } from '../../../utils/constants'; +import { buildOrders, createOrderOperation } from '../../../utils/createOrders'; +import { OrderOperationEnum, getIexecAccounts } from '../../../utils/poco-tools'; +import { IexecWrapper } from '../../utils/IexecWrapper'; + +const volume = 3; +const someSignature = ethers.utils.hexZeroPad('0x1', 65); // non empty signature + +describe('OrderManagement', async () => { + let proxyAddress: string; + let [ + iexecPoco, + iexecPocoAsAppProvider, + iexecPocoAsDatasetProvider, + iexecPocoAsScheduler, + iexecPocoAsRequester, + ]: IexecInterfaceNative[] = []; + let iexecWrapper: IexecWrapper; + let [anyone, appProvider, datasetProvider, scheduler, requester]: SignerWithAddress[] = []; + let appOrder: IexecLibOrders_v5.AppOrderStruct; + let datasetOrder: IexecLibOrders_v5.DatasetOrderStruct; + let workerpoolOrder: IexecLibOrders_v5.WorkerpoolOrderStruct; + let requestOrder: IexecLibOrders_v5.RequestOrderStruct; + let [appOrderHash, datasetOrderHash, workerpoolOrderHash, requestOrderHash]: string[] = []; + + beforeEach(async () => { + proxyAddress = await loadHardhatFixtureDeployment(); + await loadFixture(initFixture); + }); + + async function initFixture() { + const accounts = await getIexecAccounts(); + ({ appProvider, datasetProvider, scheduler, requester, anyone } = accounts); + iexecWrapper = new IexecWrapper(proxyAddress, accounts); + const { appAddress, datasetAddress, workerpoolAddress } = await iexecWrapper.createAssets(); + iexecPoco = IexecInterfaceNative__factory.connect(proxyAddress, anyone); + iexecPocoAsAppProvider = iexecPoco.connect(appProvider); + iexecPocoAsDatasetProvider = iexecPoco.connect(datasetProvider); + iexecPocoAsScheduler = iexecPoco.connect(scheduler); + iexecPocoAsRequester = iexecPoco.connect(requester); + const appPrice = 1000; + const datasetPrice = 1_000_000; + const workerpoolPrice = 1_000_000_000; + const ordersAssets = { + app: appAddress, + dataset: datasetAddress, + workerpool: workerpoolAddress, + }; + const ordersPrices = { + app: appPrice, + dataset: datasetPrice, + workerpool: workerpoolPrice, + }; + ({ appOrder, datasetOrder, workerpoolOrder, requestOrder } = buildOrders({ + assets: ordersAssets, + requester: requester.address, + prices: ordersPrices, + volume, + })); + appOrderHash = iexecWrapper.hashOrder(appOrder); + datasetOrderHash = iexecWrapper.hashOrder(datasetOrder); + workerpoolOrderHash = iexecWrapper.hashOrder(workerpoolOrder); + requestOrderHash = iexecWrapper.hashOrder(requestOrder); + } + + describe('Presign orders when operations are sent by owners', () => { + it('Should presign app order when operation is sent by app provider', async () => { + await expect( + iexecPocoAsAppProvider.manageAppOrder( + createOrderOperation(appOrder, OrderOperationEnum.SIGN), + ), + ) + .to.emit(iexecPoco, 'SignedAppOrder') + .withArgs(appOrderHash); + expect(await iexecPoco.viewPresigned(appOrderHash)).equal(appProvider.address); + expect(await iexecPoco.verifyPresignature(appProvider.address, appOrderHash)).is.true; + expect( + await iexecPoco.verifyPresignatureOrSignature( + appProvider.address, + appOrderHash, + NULL.SIGNATURE, + ), + ).is.true; + }); + it('Should presign dataset order when operation is sent by dataset provider', async () => { + await expect( + iexecPocoAsDatasetProvider.manageDatasetOrder( + createOrderOperation(datasetOrder, OrderOperationEnum.SIGN), + ), + ) + .to.emit(iexecPoco, 'SignedDatasetOrder') + .withArgs(datasetOrderHash); + expect(await iexecPoco.viewPresigned(datasetOrderHash)).equal(datasetProvider.address); + expect(await iexecPoco.verifyPresignature(datasetProvider.address, datasetOrderHash)).is + .true; + expect( + await iexecPoco.verifyPresignatureOrSignature( + datasetProvider.address, + datasetOrderHash, + NULL.SIGNATURE, + ), + ).is.true; + }); + it('Should presign workerpool order when operation is sent by scheduler', async () => { + await expect( + iexecPocoAsScheduler.manageWorkerpoolOrder( + createOrderOperation(workerpoolOrder, OrderOperationEnum.SIGN), + ), + ) + .to.emit(iexecPoco, 'SignedWorkerpoolOrder') + .withArgs(workerpoolOrderHash); + expect(await iexecPoco.viewPresigned(workerpoolOrderHash)).equal(scheduler.address); + expect(await iexecPoco.verifyPresignature(scheduler.address, workerpoolOrderHash)).is + .true; + expect( + await iexecPoco.verifyPresignatureOrSignature( + scheduler.address, + workerpoolOrderHash, + NULL.SIGNATURE, + ), + ).is.true; + }); + it('Should presign request order when operation is sent by requester', async () => { + await expect( + iexecPocoAsRequester.manageRequestOrder( + createOrderOperation(requestOrder, OrderOperationEnum.SIGN), + ), + ) + .to.emit(iexecPoco, 'SignedRequestOrder') + .withArgs(requestOrderHash); + expect(await iexecPoco.viewPresigned(requestOrderHash)).equal(requester.address); + expect(await iexecPoco.verifyPresignature(requester.address, requestOrderHash)).is.true; + expect( + await iexecPoco.verifyPresignatureOrSignature( + requester.address, + requestOrderHash, + NULL.SIGNATURE, + ), + ).is.true; + }); + }); + + describe('Presign orders when operations are signed by owners', () => { + it('Should presign app order when operation is signed by app provider', async () => { + const orderOperation = createOrderOperation(appOrder, OrderOperationEnum.SIGN); + await iexecWrapper.signOrderOperation(orderOperation, appProvider); + + await expect(iexecPoco.manageAppOrder(orderOperation)) + .to.emit(iexecPoco, 'SignedAppOrder') + .withArgs(appOrderHash); + expect(await iexecPoco.viewPresigned(appOrderHash)).equal(appProvider.address); + expect(await iexecPoco.verifyPresignature(appProvider.address, appOrderHash)).is.true; + expect( + await iexecPoco.verifyPresignatureOrSignature( + appProvider.address, + appOrderHash, + NULL.SIGNATURE, + ), + ).is.true; + }); + it('Should presign dataset order when operation is signed by dataset provider', async () => { + const orderOperation = createOrderOperation(datasetOrder, OrderOperationEnum.SIGN); + await iexecWrapper.signOrderOperation(orderOperation, datasetProvider); + + await expect(iexecPoco.manageDatasetOrder(orderOperation)) + .to.emit(iexecPoco, 'SignedDatasetOrder') + .withArgs(datasetOrderHash); + expect(await iexecPoco.viewPresigned(datasetOrderHash)).equal(datasetProvider.address); + expect(await iexecPoco.verifyPresignature(datasetProvider.address, datasetOrderHash)).is + .true; + expect( + await iexecPoco.verifyPresignatureOrSignature( + datasetProvider.address, + datasetOrderHash, + NULL.SIGNATURE, + ), + ).is.true; + }); + it('Should presign workerpool order when operation is signed by workerpool provider', async () => { + const orderOperation = createOrderOperation(workerpoolOrder, OrderOperationEnum.SIGN); + await iexecWrapper.signOrderOperation(orderOperation, scheduler); + + await expect(iexecPoco.manageWorkerpoolOrder(orderOperation)) + .to.emit(iexecPoco, 'SignedWorkerpoolOrder') + .withArgs(workerpoolOrderHash); + expect(await iexecPoco.viewPresigned(workerpoolOrderHash)).equal(scheduler.address); + expect(await iexecPoco.verifyPresignature(scheduler.address, workerpoolOrderHash)).is + .true; + expect( + await iexecPoco.verifyPresignatureOrSignature( + scheduler.address, + workerpoolOrderHash, + NULL.SIGNATURE, + ), + ).is.true; + }); + it('Should presign request order when operation is signed by requester', async () => { + const orderOperation = createOrderOperation(requestOrder, OrderOperationEnum.SIGN); + await iexecWrapper.signOrderOperation(orderOperation, requester); + + await expect(iexecPoco.manageRequestOrder(orderOperation)) + .to.emit(iexecPoco, 'SignedRequestOrder') + .withArgs(requestOrderHash); + expect(await iexecPoco.viewPresigned(requestOrderHash)).equal(requester.address); + expect(await iexecPoco.verifyPresignature(requester.address, requestOrderHash)).is.true; + expect( + await iexecPoco.verifyPresignatureOrSignature( + requester.address, + requestOrderHash, + NULL.SIGNATURE, + ), + ).is.true; + }); + }); + + describe('Close orders when operations are sent by owners', () => { + it('Should close app order when operation is sent by app provider', async () => { + await expect( + iexecPocoAsAppProvider.manageAppOrder( + createOrderOperation(appOrder, OrderOperationEnum.CLOSE), + ), + ) + .to.emit(iexecPoco, 'ClosedAppOrder') + .withArgs(appOrderHash); + expect(await iexecPoco.viewConsumed(appOrderHash)).equal(volume); + }); + it('Should close dataset order when operation is sent by dataset provider', async () => { + await expect( + iexecPocoAsDatasetProvider.manageDatasetOrder( + createOrderOperation(datasetOrder, OrderOperationEnum.CLOSE), + ), + ) + .to.emit(iexecPoco, 'ClosedDatasetOrder') + .withArgs(datasetOrderHash); + expect(await iexecPoco.viewConsumed(datasetOrderHash)).equal(volume); + }); + it('Should close workerpool order when operation is sent by scheduler', async () => { + await expect( + iexecPocoAsScheduler.manageWorkerpoolOrder( + createOrderOperation(workerpoolOrder, OrderOperationEnum.CLOSE), + ), + ) + .to.emit(iexecPoco, 'ClosedWorkerpoolOrder') + .withArgs(workerpoolOrderHash); + expect(await iexecPoco.viewConsumed(workerpoolOrderHash)).equal(volume); + }); + it('Should close request order when operation is sent by requester', async () => { + await expect( + iexecPocoAsRequester.manageRequestOrder( + createOrderOperation(requestOrder, OrderOperationEnum.CLOSE), + ), + ) + .to.emit(iexecPoco, 'ClosedRequestOrder') + .withArgs(requestOrderHash); + expect(await iexecPoco.viewConsumed(requestOrderHash)).equal(volume); + }); + }); + + describe('Close orders when operations are signed by owners', () => { + it('Should close app order when operation is signed by app provider', async () => { + const orderOperation = createOrderOperation(appOrder, OrderOperationEnum.CLOSE); + await iexecWrapper.signOrderOperation(orderOperation, appProvider); + + await expect(iexecPoco.manageAppOrder(orderOperation)) + .to.emit(iexecPoco, 'ClosedAppOrder') + .withArgs(appOrderHash); + expect(await iexecPoco.viewConsumed(appOrderHash)).equal(volume); + }); + it('Should close dataset order when operation is signed by dataset provider', async () => { + const orderOperation = createOrderOperation(datasetOrder, OrderOperationEnum.CLOSE); + await iexecWrapper.signOrderOperation(orderOperation, datasetProvider); + + await expect(iexecPoco.manageDatasetOrder(orderOperation)) + .to.emit(iexecPoco, 'ClosedDatasetOrder') + .withArgs(datasetOrderHash); + expect(await iexecPoco.viewConsumed(datasetOrderHash)).equal(volume); + }); + it('Should close workerpool order when operation is signed by scheduler', async () => { + const orderOperation = createOrderOperation(workerpoolOrder, OrderOperationEnum.CLOSE); + await iexecWrapper.signOrderOperation(orderOperation, scheduler); + + await expect(iexecPoco.manageWorkerpoolOrder(orderOperation)) + .to.emit(iexecPoco, 'ClosedWorkerpoolOrder') + .withArgs(workerpoolOrderHash); + expect(await iexecPoco.viewConsumed(workerpoolOrderHash)).equal(volume); + }); + it('Should close request order when operation is signed by requester', async () => { + const orderOperation = createOrderOperation(requestOrder, OrderOperationEnum.CLOSE); + await iexecWrapper.signOrderOperation(orderOperation, requester); + + await expect(iexecPoco.manageRequestOrder(orderOperation)) + .to.emit(iexecPoco, 'ClosedRequestOrder') + .withArgs(requestOrderHash); + expect(await iexecPoco.viewConsumed(requestOrderHash)).equal(volume); + }); + }); + + describe('Should not manage orders when invalid sender or signature', () => { + it('Should not manage app order when invalid sender or signature', async () => { + await expect( + iexecPoco.manageAppOrder({ + order: appOrder, + operation: OrderOperationEnum.SIGN, // any is fine + sign: someSignature, + }), + ).to.be.revertedWith('invalid-sender-or-signature'); + }); + it('Should not manage dataset order when invalid sender or signature', async () => { + await expect( + iexecPoco.manageDatasetOrder({ + order: datasetOrder, + operation: OrderOperationEnum.SIGN, // any is fine + sign: someSignature, + }), + ).to.be.revertedWith('invalid-sender-or-signature'); + }); + it('Should not manage workerpool order when invalid sender or signature', async () => { + await expect( + iexecPoco.manageWorkerpoolOrder({ + order: workerpoolOrder, + operation: OrderOperationEnum.SIGN, // any is fine + sign: someSignature, + }), + ).to.be.revertedWith('invalid-sender-or-signature'); + }); + it('Should not manage request order when invalid sender or signature', async () => { + await expect( + iexecPoco.manageRequestOrder({ + order: requestOrder, + operation: OrderOperationEnum.SIGN, // any is fine + sign: someSignature, + }), + ).to.be.revertedWith('invalid-sender-or-signature'); + }); + }); +}); diff --git a/test/byContract/IexecOrderManagement/close.js b/test/byContract/IexecOrderManagement/close.js.skip similarity index 99% rename from test/byContract/IexecOrderManagement/close.js rename to test/byContract/IexecOrderManagement/close.js.skip index 63f7e47d9..d6b223ddb 100644 --- a/test/byContract/IexecOrderManagement/close.js +++ b/test/byContract/IexecOrderManagement/close.js.skip @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: 2020-2024 IEXEC BLOCKCHAIN TECH // SPDX-License-Identifier: Apache-2.0 +// TODO: Remove this file replaced by IexecOrderManagement.ts + const loadTruffleFixtureDeployment = require('../../../scripts/truffle-fixture-deployer'); // Config var DEPLOYMENT = require('../../../config/config.json').chains.default; diff --git a/test/byContract/IexecOrderManagement/invalid.js b/test/byContract/IexecOrderManagement/invalid.js.skip similarity index 99% rename from test/byContract/IexecOrderManagement/invalid.js rename to test/byContract/IexecOrderManagement/invalid.js.skip index 33d886529..53e5eaf47 100644 --- a/test/byContract/IexecOrderManagement/invalid.js +++ b/test/byContract/IexecOrderManagement/invalid.js.skip @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: 2020-2024 IEXEC BLOCKCHAIN TECH // SPDX-License-Identifier: Apache-2.0 +// TODO: Remove this file replaced by IexecOrderManagement.ts + const loadTruffleFixtureDeployment = require('../../../scripts/truffle-fixture-deployer'); // Config var DEPLOYMENT = require('../../../config/config.json').chains.default; diff --git a/test/byContract/IexecOrderManagement/sign.js b/test/byContract/IexecOrderManagement/sign.js.skip similarity index 99% rename from test/byContract/IexecOrderManagement/sign.js rename to test/byContract/IexecOrderManagement/sign.js.skip index 86149411b..445a81075 100644 --- a/test/byContract/IexecOrderManagement/sign.js +++ b/test/byContract/IexecOrderManagement/sign.js.skip @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: 2020-2024 IEXEC BLOCKCHAIN TECH // SPDX-License-Identifier: Apache-2.0 +// TODO: Remove this file replaced by IexecOrderManagement.ts + const loadTruffleFixtureDeployment = require('../../../scripts/truffle-fixture-deployer'); // Config var DEPLOYMENT = require('../../../config/config.json').chains.default; diff --git a/test/utils/IexecWrapper.ts b/test/utils/IexecWrapper.ts index 4518c0e63..0f07278f0 100644 --- a/test/utils/IexecWrapper.ts +++ b/test/utils/IexecWrapper.ts @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2024 IEXEC BLOCKCHAIN TECH // SPDX-License-Identifier: Apache-2.0 +import { TypedDataDomain } from '@ethersproject/abstract-signer'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { BigNumber, ContractReceipt } from 'ethers'; import hre, { ethers } from 'hardhat'; @@ -18,7 +19,14 @@ import { WorkerpoolRegistry__factory, } from '../../typechain'; import { IexecPoco1__factory } from '../../typechain/factories/contracts/modules/interfaces/IexecPoco1.v8.sol'; -import { IexecOrders, Orders, hashOrder, signOrders } from '../../utils/createOrders'; +import { + IexecOrders, + OrderOperation, + Orders, + hashOrder, + signOrderOperation, + signOrders, +} from '../../utils/createOrders'; import { IexecAccounts, getDealId, getTaskId, setNextBlockTimestamp } from '../../utils/poco-tools'; import { extractEventsFromReceipt } from '../../utils/tools'; const DEPLOYMENT_CONFIG = config.chains.default; @@ -26,10 +34,17 @@ const DEPLOYMENT_CONFIG = config.chains.default; export class IexecWrapper { proxyAddress: string; accounts: IexecAccounts; + domain: TypedDataDomain; constructor(proxyAddress: string, accounts: IexecAccounts) { this.proxyAddress = proxyAddress; this.accounts = accounts; + this.domain = { + name: 'iExecODB', + version: '5.0.0', + chainId: hre.network.config.chainId, + verifyingContract: this.proxyAddress, + }; } /** @@ -89,6 +104,23 @@ export class IexecWrapper { .then((tx) => tx.wait()); } + /** + * Hash an order using current domain. + */ + hashOrder(order: Record) { + return hashOrder(this.domain, order); + } + + /** + * Sign an order operation using current domain. + */ + async signOrderOperation( + orderOperation: OrderOperation, + signer: SignerWithAddress, + ): Promise { + return signOrderOperation(this.domain, orderOperation, signer); + } + async signAndSponsorMatchOrders(orders: IexecOrders) { return this._signAndMatchOrders(orders, true); } @@ -104,13 +136,7 @@ export class IexecWrapper { * Otherwise the requester will be in charge of paying for the deal. */ private async _signAndMatchOrders(orders: IexecOrders, withSponsor: boolean) { - const domain = { - name: 'iExecODB', - version: '5.0.0', - chainId: hre.network.config.chainId, - verifyingContract: this.proxyAddress, - }; - await signOrders(domain, orders, { + await signOrders(this.domain, orders, { appOwner: this.accounts.appProvider, datasetOwner: this.accounts.datasetProvider, workerpoolOwner: this.accounts.scheduler, @@ -124,9 +150,9 @@ export class IexecWrapper { await IexecAccessors__factory.connect( this.proxyAddress, this.accounts.anyone, - ).viewConsumed(hashOrder(domain, requestOrder)) + ).viewConsumed(this.hashOrder(requestOrder)) ).toNumber(); - const dealId = getDealId(domain, requestOrder, taskIndex); + const dealId = getDealId(this.domain, requestOrder, taskIndex); const taskId = getTaskId(dealId, taskIndex); const volume = Number(requestOrder.volume); const taskPrice = diff --git a/utils/createOrders.ts b/utils/createOrders.ts index fb3b5df56..3c623596a 100644 --- a/utils/createOrders.ts +++ b/utils/createOrders.ts @@ -1,12 +1,14 @@ -// SPDX-FileCopyrightText: 2023 IEXEC BLOCKCHAIN TECH +// SPDX-FileCopyrightText: 2023-2024 IEXEC BLOCKCHAIN TECH // SPDX-License-Identifier: Apache-2.0 import { TypedDataDomain } from '@ethersproject/abstract-signer'; +import { BigNumber } from '@ethersproject/bignumber'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { ethers } from 'hardhat'; import { IexecLibOrders_v5 } from '../typechain'; -import constants from './constants'; +import constants, { NULL } from './constants'; import { utils } from './odb-tools'; +import { OrderOperationEnum } from './poco-tools'; export type Orders = [ IexecLibOrders_v5.AppOrderStruct, @@ -52,6 +54,12 @@ export interface IexecOrders { requester: IexecLibOrders_v5.RequestOrderStruct; } +export interface OrderOperation { + order: Record; + operation: BigNumber; + sign: string; +} + export function createEmptyAppOrder(): IexecLibOrders_v5.AppOrderStruct { return { app: constants.NULL.ADDRESS, @@ -117,6 +125,13 @@ export function createEmptyDatasetOrder(): IexecLibOrders_v5.DatasetOrderStruct }; } +/** + * Create an order operation from an existing order. + */ +export function createOrderOperation(order: OrderType, operation: OrderOperationEnum) { + return { order, operation: BigNumber.from(operation), sign: NULL.SIGNATURE }; +} + export function buildOrders(matchOrdersArgs: MatchOrdersArgs) { let requestOrder = createEmptyRequestOrder(); let appOrder = createEmptyAppOrder(); @@ -240,6 +255,23 @@ export async function signOrder( return utils.signStruct(getTypeOf(order), order, domain, signer); } +/** + * Sign an iExec EIP712 order operation for app, dataset, workerpool or request + * order operations. + */ +export async function signOrderOperation( + domain: TypedDataDomain, + orderOperation: OrderOperation, + signer: SignerWithAddress, +): Promise { + return utils.signStruct( + getTypeOf(orderOperation.order) + 'Operation', + orderOperation, + domain, + signer, + ); +} + /** * Get typed data hash of order: app, dataset, workerpool or request * @returns order hash diff --git a/utils/poco-tools.ts b/utils/poco-tools.ts index c160081d7..0788c83af 100644 --- a/utils/poco-tools.ts +++ b/utils/poco-tools.ts @@ -22,6 +22,11 @@ export enum TaskStatusEnum { FAILED, } +export enum OrderOperationEnum { + SIGN, + CLOSE, +} + export interface IexecAccounts { iexecAdmin: SignerWithAddress; requester: SignerWithAddress;