diff --git a/CHANGELOG.md b/CHANGELOG.md index d9a939994..e2181758f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ - finalize (#79, #117, #119) - reveal (#114, #118) - contribute (#108, #109, #110) - - IexecPoco1 (#107, #113) + - IexecPoco1 (#107, #113, #115) - Add `.test` suffix to unit test files (#106) - ENSIntegration (#105) - IexecOrderManagement (#101, #102, #103, #104) diff --git a/contracts/tools/testing/ERC1271Mock.sol b/contracts/tools/testing/ERC1271Mock.sol new file mode 100644 index 000000000..3dd5225cb --- /dev/null +++ b/contracts/tools/testing/ERC1271Mock.sol @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2024 IEXEC BLOCKCHAIN TECH +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-v5/interfaces/IERC1271.sol"; + +// Note: section that are not covered by tests are commented. +// TODO: uncomment when adding signature verification tests. + +contract ERC1271Mock is IERC1271 { + // bool public shouldValidateSignature; + + // function setShouldValidateSignature(bool value) external { + // shouldValidateSignature = value; + // } + + function isValidSignature( + bytes32, + bytes memory + ) external view override returns (bytes4 magicValue) { + // if (shouldValidateSignature) { + // magicValue = IERC1271.isValidSignature.selector; + // } + } +} diff --git a/contracts/tools/testing/OwnableMock.sol b/contracts/tools/testing/OwnableMock.sol new file mode 100644 index 000000000..7e396ff65 --- /dev/null +++ b/contracts/tools/testing/OwnableMock.sol @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2024 IEXEC BLOCKCHAIN TECH +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-v5/access/Ownable.sol"; + +contract OwnableMock is Ownable { + constructor() Ownable(msg.sender) {} +} diff --git a/test/byContract/IexecPoco/00_matchorders.js b/test/byContract/IexecPoco/00_matchorders.js.skip similarity index 100% rename from test/byContract/IexecPoco/00_matchorders.js rename to test/byContract/IexecPoco/00_matchorders.js.skip diff --git a/test/byContract/IexecPoco/IexecPoco1.test.ts b/test/byContract/IexecPoco/IexecPoco1.test.ts index 5aa12a4df..6bd39f863 100644 --- a/test/byContract/IexecPoco/IexecPoco1.test.ts +++ b/test/byContract/IexecPoco/IexecPoco1.test.ts @@ -7,18 +7,25 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { ethers, expect } from 'hardhat'; import { loadHardhatFixtureDeployment } from '../../../scripts/hardhat-fixture-deployer'; import { + ERC1271Mock, + ERC1271Mock__factory, + IERC721__factory, IexecInterfaceNative, IexecInterfaceNative__factory, IexecPocoAccessors__factory, - TestClient, - TestClient__factory, + OwnableMock, + OwnableMock__factory, } from '../../../typechain'; +import { IexecPoco1 } from '../../../typechain/contracts/modules/interfaces/IexecPoco1.v8.sol'; +import { IexecPoco1__factory } from '../../../typechain/factories/contracts/modules/interfaces/IexecPoco1.v8.sol'; import { IexecOrders, OrdersActors, OrdersAssets, OrdersPrices, buildOrders, + hashOrder, + signOrder, signOrders, } from '../../../utils/createOrders'; import { getDealId, getIexecAccounts, setNextBlockTimestamp } from '../../../utils/poco-tools'; @@ -31,22 +38,22 @@ import { IexecWrapper } from '../../utils/IexecWrapper'; const appPrice = 1000; const datasetPrice = 1_000_000; const workerpoolPrice = 1_000_000_000; -const trust = 3; -const category = 2; const standardDealTag = '0x0000000000000000000000000000000000000000000000000000000000000000'; const teeDealTag = '0x0000000000000000000000000000000000000000000000000000000000000001'; -const callback = ethers.Wallet.createRandom().address; -const params = ''; -const volume = 321; -const taskIndex = 0; +const volume = 1; +const botVolume = 321; + +/** + * Note: TEE is the default in tests. + */ describe('IexecPoco1', () => { let proxyAddress: string; let [iexecPoco, iexecPocoAsRequester]: IexecInterfaceNative[] = []; + let iexecPocoAsSponsor: IexecPoco1; // Sponsor function not available yet in IexecInterfaceNative. let iexecWrapper: IexecWrapper; let [appAddress, datasetAddress, workerpoolAddress]: string[] = []; let [ - iexecAdmin, requester, sponsor, beneficiary, @@ -59,8 +66,9 @@ describe('IexecPoco1', () => { let ordersAssets: OrdersAssets; let ordersPrices: OrdersPrices; let orders: IexecOrders; - let randomAddress: string; - let randomContract: TestClient; + let [randomAddress, randomSignature]: string[] = []; + let randomContract: OwnableMock; + let erc1271MockContract: ERC1271Mock; beforeEach('Deploy', async () => { // Deploy all contracts @@ -71,20 +79,13 @@ describe('IexecPoco1', () => { async function initFixture() { const accounts = await getIexecAccounts(); - ({ - iexecAdmin, - requester, - sponsor, - beneficiary, - appProvider, - datasetProvider, - scheduler, - anyone, - } = accounts); + ({ requester, sponsor, beneficiary, appProvider, datasetProvider, scheduler, anyone } = + accounts); iexecWrapper = new IexecWrapper(proxyAddress, accounts); ({ appAddress, datasetAddress, workerpoolAddress } = await iexecWrapper.createAssets()); iexecPoco = IexecInterfaceNative__factory.connect(proxyAddress, anyone); iexecPocoAsRequester = iexecPoco.connect(requester); + iexecPocoAsSponsor = IexecPoco1__factory.connect(proxyAddress, sponsor); ordersActors = { appOwner: appProvider, datasetOwner: datasetProvider, @@ -105,54 +106,47 @@ describe('IexecPoco1', () => { assets: ordersAssets, prices: ordersPrices, requester: requester.address, - beneficiary: beneficiary.address, tag: teeDealTag, volume: volume, - callback: callback, - trust: trust, - category: category, - params: params, })); - randomAddress = ethers.Wallet.createRandom().address; - randomContract = await new TestClient__factory() + const randomWallet = ethers.Wallet.createRandom(); + randomAddress = randomWallet.address; + randomSignature = await randomWallet.signMessage('random'); + randomContract = await new OwnableMock__factory() + .connect(anyone) + .deploy() + .then((contract) => contract.deployed()); + erc1271MockContract = await new ERC1271Mock__factory() .connect(anyone) .deploy() .then((contract) => contract.deployed()); - // TODO check why this is done in 00_matchorders.js - // await Workerpool__factory.connect(workerpoolAddress, scheduler) - // .changePolicy(35, 5) - // .then((tx) => tx.wait()); } - // TODO - describe('Verify signature', () => {}); - describe('Verify presignature', () => {}); - describe('Verify presignature or signature', () => {}); - describe('Match orders', () => { - it('[TEE] Should match orders with all assets, callback, and BoT', async () => { - // Recreate orders here instead of using the default ones created - // in beforeEach to make this test as explicit as possible. - const { orders } = buildOrders({ + it('Should match orders with: all assets, beneficiary, BoT, callback, replication', async () => { + const trust = 3; + const category = 2; + const params = ''; + // Use orders with full configuration. + const { orders: fullConfigOrders } = buildOrders({ assets: ordersAssets, prices: ordersPrices, requester: requester.address, beneficiary: beneficiary.address, tag: teeDealTag, - volume: volume, - callback: callback, + volume: botVolume, + callback: randomAddress, trust: trust, category: category, params: params, }); - expect(await iexecPoco.balanceOf(proxyAddress)).to.equal(0); // Compute prices, stakes, rewards, ... const dealPrice = (appPrice + datasetPrice + workerpoolPrice) * // task price - volume; + botVolume; const schedulerStake = await iexecWrapper.computeSchedulerDealStake( workerpoolPrice, - volume, + botVolume, ); const workerStakePerTask = await iexecWrapper.computeWorkerTaskStake( workerpoolAddress, @@ -163,28 +157,26 @@ describe('IexecPoco1', () => { // Deposit required amounts. await iexecWrapper.depositInIexecAccount(requester, dealPrice); await iexecWrapper.depositInIexecAccount(scheduler, schedulerStake); - // Check balances and frozen before. - expect(await iexecPoco.balanceOf(requester.address)).to.equal(dealPrice); - expect(await iexecPoco.frozenOf(requester.address)).to.equal(0); - expect(await iexecPoco.balanceOf(scheduler.address)).to.equal(schedulerStake); - expect(await iexecPoco.frozenOf(scheduler.address)).to.equal(0); + // Save frozen balances before match. + const requesterFrozenBefore = (await iexecPoco.frozenOf(requester.address)).toNumber(); + const schedulerFrozenBefore = (await iexecPoco.frozenOf(scheduler.address)).toNumber(); // Sign and match orders. const startTime = await setNextBlockTimestamp(); - await signOrders(iexecWrapper.getDomain(), orders, ordersActors); + await signOrders(iexecWrapper.getDomain(), fullConfigOrders, ordersActors); const { appOrderHash, datasetOrderHash, workerpoolOrderHash, requestOrderHash } = - iexecWrapper.hashOrders(orders); - const dealId = getDealId(iexecWrapper.getDomain(), orders.requester, taskIndex); + iexecWrapper.hashOrders(fullConfigOrders); + const dealId = getDealId(iexecWrapper.getDomain(), fullConfigOrders.requester); expect( await IexecPocoAccessors__factory.connect(proxyAddress, anyone).computeDealVolume( - ...orders.toArray(), + ...fullConfigOrders.toArray(), ), - ).to.equal(volume); + ).to.equal(botVolume); - expect(await iexecPocoAsRequester.callStatic.matchOrders(...orders.toArray())).to.equal( + expect(await iexecPoco.callStatic.matchOrders(...fullConfigOrders.toArray())).to.equal( dealId, ); - const tx = iexecPocoAsRequester.matchOrders(...orders.toArray()); - // Check balances and frozen after. + const tx = iexecPocoAsRequester.matchOrders(...fullConfigOrders.toArray()); + // Check balances and frozen. await expect(tx).to.changeTokenBalances( iexecPoco, [iexecPoco, requester, scheduler], @@ -192,10 +184,16 @@ describe('IexecPoco1', () => { ); // TODO use predicate `(change) => boolean` when migrating to a recent version of Hardhat. // See https://github.com/NomicFoundation/hardhat/blob/main/packages/hardhat-chai-matchers/src/internal/changeTokenBalance.ts#L42 - expect(await iexecPoco.frozenOf(requester.address)).to.equal(dealPrice); - expect(await iexecPoco.frozenOf(scheduler.address)).to.equal(schedulerStake); + expect(await iexecPoco.frozenOf(requester.address)).to.equal( + requesterFrozenBefore + dealPrice, + ); + expect(await iexecPoco.frozenOf(scheduler.address)).to.equal( + schedulerFrozenBefore + schedulerStake, + ); // Check events. await expect(tx) + .to.emit(iexecPoco, 'SchedulerNotice') + .withArgs(workerpoolAddress, dealId) .to.emit(iexecPoco, 'OrdersMatched') .withArgs( dealId, @@ -203,67 +201,130 @@ describe('IexecPoco1', () => { datasetOrderHash, workerpoolOrderHash, requestOrderHash, - volume, + botVolume, ); // Check deal const deal = await iexecPoco.viewDeal(dealId); expect(deal.app.pointer).to.equal(appAddress); expect(deal.app.owner).to.equal(appProvider.address); - expect(deal.app.price.toNumber()).to.equal(appPrice); + expect(deal.app.price).to.equal(appPrice); expect(deal.dataset.pointer).to.equal(datasetAddress); expect(deal.dataset.owner).to.equal(datasetProvider.address); - expect(deal.dataset.price.toNumber()).to.equal(datasetPrice); + expect(deal.dataset.price).to.equal(datasetPrice); expect(deal.workerpool.pointer).to.equal(workerpoolAddress); expect(deal.workerpool.owner).to.equal(scheduler.address); - expect(deal.workerpool.price.toNumber()).to.equal(workerpoolPrice); - expect(deal.trust.toNumber()).to.equal(trust); - expect(deal.category.toNumber()).to.equal(category); + expect(deal.workerpool.price).to.equal(workerpoolPrice); + expect(deal.trust).to.equal(trust); + expect(deal.category).to.equal(category); expect(deal.tag).to.equal(teeDealTag); expect(deal.requester).to.equal(requester.address); expect(deal.beneficiary).to.equal(beneficiary.address); - expect(deal.callback).to.equal(callback); + expect(deal.callback).to.equal(randomAddress); expect(deal.params).to.equal(params); - expect(deal.startTime.toNumber()).to.equal(startTime); - expect(deal.botFirst.toNumber()).to.equal(0); - expect(deal.botSize.toNumber()).to.equal(volume); - expect(deal.workerStake.toNumber()).to.equal(workerStakePerTask); - expect(deal.schedulerRewardRatio.toNumber()).to.equal(schedulerRewardByTask); + expect(deal.startTime).to.equal(startTime); + expect(deal.botFirst).to.equal(0); + expect(deal.botSize).to.equal(botVolume); + expect(deal.workerStake).to.equal(workerStakePerTask); + expect(deal.schedulerRewardRatio).to.equal(schedulerRewardByTask); expect(deal.sponsor).to.equal(requester.address); }); - it('[TEE] Should match orders without callback', async () => { - orders.requester.callback = AddressZero; - await depositForRequesterAndSchedulerWithDefaultPrices(); + it('[Standard] Should match orders with: all assets, beneficiary, BoT, callback, replication', async () => { + const trust = 3; + const category = 2; + const params = ''; + // Use orders with full configuration. + const { orders: standardOrders } = buildOrders({ + assets: ordersAssets, + prices: ordersPrices, + requester: requester.address, + beneficiary: beneficiary.address, + tag: standardDealTag, + volume: botVolume, + callback: randomAddress, + trust: trust, + category: category, + params: params, + }); + await depositForRequesterAndSchedulerWithDefaultPrices(botVolume); + // Sign and match orders. + const startTime = await setNextBlockTimestamp(); + await signOrders(iexecWrapper.getDomain(), standardOrders, ordersActors); + const dealId = getDealId(iexecWrapper.getDomain(), standardOrders.requester); + await expect(iexecPocoAsRequester.matchOrders(...standardOrders.toArray())).to.emit( + iexecPoco, + 'OrdersMatched', + ); + // Check deal + const deal = await iexecPoco.viewDeal(dealId); + expect(deal.app.pointer).to.equal(appAddress); + expect(deal.app.owner).to.equal(appProvider.address); + expect(deal.app.price).to.equal(appPrice); + expect(deal.dataset.pointer).to.equal(datasetAddress); + expect(deal.dataset.owner).to.equal(datasetProvider.address); + expect(deal.dataset.price).to.equal(datasetPrice); + expect(deal.workerpool.pointer).to.equal(workerpoolAddress); + expect(deal.workerpool.owner).to.equal(scheduler.address); + expect(deal.workerpool.price).to.equal(workerpoolPrice); + expect(deal.trust).to.equal(trust); + expect(deal.category).to.equal(category); + expect(deal.tag).to.equal(standardDealTag); + expect(deal.requester).to.equal(requester.address); + expect(deal.beneficiary).to.equal(beneficiary.address); + expect(deal.callback).to.equal(randomAddress); + expect(deal.params).to.equal(params); + expect(deal.startTime).to.equal(startTime); + expect(deal.botFirst).to.equal(0); + expect(deal.botSize).to.equal(botVolume); + expect(deal.workerStake).to.equal( + await iexecWrapper.computeWorkerTaskStake(workerpoolAddress, workerpoolPrice), + ); + expect(deal.schedulerRewardRatio).to.equal( + await iexecWrapper.getSchedulerTaskRewardRatio(workerpoolAddress), + ); + expect(deal.sponsor).to.equal(requester.address); + }); + + it('Should match orders without: beneficiary, BoT, callback, replication', async () => { + await depositForRequesterAndSchedulerWithDefaultPrices(volume); // Sign and match orders. await signOrders(iexecWrapper.getDomain(), orders, ordersActors); - const dealId = getDealId(iexecWrapper.getDomain(), orders.requester, taskIndex); + const dealId = getDealId(iexecWrapper.getDomain(), orders.requester); await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.emit( iexecPoco, 'OrdersMatched', ); // Check deal const deal = await iexecPoco.viewDeal(dealId); + expect(deal.beneficiary).to.equal(AddressZero); + expect(deal.botSize).to.equal(1); expect(deal.callback).to.equal(AddressZero); + expect(deal.trust).to.equal(1); }); - it('[TEE] Should match orders without dataset', async () => { + it('Should match orders without: dataset', async () => { orders.dataset.dataset = AddressZero; orders.requester.dataset = AddressZero; // Set dataset volume lower than other assets to make sure // it does not impact final volume computation. - orders.dataset.volume = volume - 1; + orders.dataset.volume = botVolume - 1; + orders.app.volume = botVolume; + orders.workerpool.volume = botVolume; + orders.requester.volume = botVolume; // Compute prices, stakes, rewards, ... - const dealPrice = (appPrice + workerpoolPrice) * volume; // no dataset price + const dealPrice = (appPrice + workerpoolPrice) * botVolume; // no dataset price const schedulerStake = await iexecWrapper.computeSchedulerDealStake( workerpoolPrice, - volume, + botVolume, ); // Deposit required amounts. await iexecWrapper.depositInIexecAccount(requester, dealPrice); await iexecWrapper.depositInIexecAccount(scheduler, schedulerStake); + // Save frozen balances before match. + const requesterFrozenBefore = (await iexecPoco.frozenOf(requester.address)).toNumber(); // Sign and match orders. await signOrders(iexecWrapper.getDomain(), orders, ordersActors); - const dealId = getDealId(iexecWrapper.getDomain(), orders.requester, taskIndex); + const dealId = getDealId(iexecWrapper.getDomain(), orders.requester); const tx = iexecPocoAsRequester.matchOrders(...orders.toArray()); // Check balances and frozen. // Dataset price shouldn't be included. @@ -272,28 +333,95 @@ describe('IexecPoco1', () => { [iexecPoco, requester, scheduler], [dealPrice + schedulerStake, -dealPrice, -schedulerStake], ); - expect(await iexecPoco.frozenOf(requester.address)).to.equal(dealPrice); + expect(await iexecPoco.frozenOf(requester.address)).to.equal( + requesterFrozenBefore + dealPrice, + ); // Check events. await expect(tx).to.emit(iexecPoco, 'OrdersMatched'); // Check deal const deal = await iexecPoco.viewDeal(dealId); expect(deal.dataset.pointer).to.equal(AddressZero); expect(deal.dataset.owner).to.equal(AddressZero); - expect(deal.dataset.price.toNumber()).to.equal(0); + expect(deal.dataset.price).to.equal(0); // BoT size should not be impacted even if the dataset order is the order with the lowest volume - expect(deal.botSize.toNumber()).to.equal(volume); + expect(deal.botSize).to.equal(botVolume); }); - it('[TODO][TEE] Should match orders with restrictions', async () => {}); + it(`Should match orders with full restrictions in all orders`, async () => { + orders.app.datasetrestrict = orders.dataset.dataset; + orders.app.workerpoolrestrict = orders.workerpool.workerpool; + orders.app.requesterrestrict = orders.requester.requester; + + orders.dataset.apprestrict = orders.app.app; + orders.dataset.workerpoolrestrict = orders.workerpool.workerpool; + orders.dataset.requesterrestrict = orders.requester.requester; - it('[TEE] Should fail when categories are different', async () => { - orders.requester.category = category + 1; // Valid but different category. + orders.workerpool.apprestrict = orders.app.app; + orders.workerpool.datasetrestrict = orders.dataset.dataset; + orders.workerpool.requesterrestrict = orders.requester.requester; + + // requestOrder.workerpool is a restriction. + orders.requester.workerpool = orders.workerpool.workerpool; + + await depositForRequesterAndSchedulerWithDefaultPrices(volume); + // Sign and match orders. + await signOrders(iexecWrapper.getDomain(), orders, ordersActors); + await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.emit( + iexecPoco, + 'OrdersMatched', + ); + }); + + /** + * Successful match orders with partial restrictions. + * Note: Workerpool is the only restriction in request order and it is + * tested elsewhere. + */ + ['app', 'dataset', 'workerpool'].forEach((orderName) => { + ['app', 'dataset', 'workerpool', 'requester'].forEach((assetName) => { + // Filter irrelevant cases (e.g. app - app). + if (orderName.includes(assetName)) { + return; + } + it(`Should match orders with ${assetName} restriction in ${orderName} order`, async () => { + // e.g. orders.app.datasetrestrict = orders.dataset.dataset + orders[orderName][assetName + 'restrict'] = orders[assetName][assetName]; + await depositForRequesterAndSchedulerWithDefaultPrices(volume); + // Sign and match orders. + await signOrders(iexecWrapper.getDomain(), orders, ordersActors); + await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.emit( + iexecPoco, + 'OrdersMatched', + ); + }); + }); + }); + + it(`Should match orders with any workerpool when request order has no workerpool restriction`, async () => { + orders.requester.workerpool = AddressZero; // No restriction. + await depositForRequesterAndSchedulerWithDefaultPrices(volume); + // Sign and match orders. + await signOrders(iexecWrapper.getDomain(), orders, ordersActors); + await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.emit( + iexecPoco, + 'OrdersMatched', + ); + }); + + // TODO add success tests for: + // - identity groups + // - pre-signatures + // - low orders volumes + // - multiple matches of the same order + + it('Should fail when categories are different', async () => { + orders.requester.category = Number(orders.workerpool.category) + 1; // Valid but different category. await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.be.revertedWith( 'iExecV5-matchOrders-0x00', ); }); - it('[TEE] Should fail when category is unknown', async () => { + it('Should fail when category is unknown', async () => { const lastCategoryIndex = (await iexecPoco.countCategory()).toNumber() - 1; orders.requester.category = lastCategoryIndex + 1; orders.workerpool.category = lastCategoryIndex + 1; @@ -302,35 +430,35 @@ describe('IexecPoco1', () => { ); }); - it('[TEE] Should fail when requested trust is above workerpool trust', async () => { + it('Should fail when requested trust is above workerpool trust', async () => { orders.requester.trust = Number(orders.workerpool.trust) + 1; await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.be.revertedWith( 'iExecV5-matchOrders-0x02', ); }); - it('[TEE] Should fail when app max price is less than app price', async () => { + it('Should fail when app max price is less than app price', async () => { orders.requester.appmaxprice = Number(orders.app.appprice) - 1; await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.be.revertedWith( 'iExecV5-matchOrders-0x03', ); }); - it('[TEE] Should fail when dataset max price is less than dataset price', async () => { + it('Should fail when dataset max price is less than dataset price', async () => { orders.requester.datasetmaxprice = Number(orders.dataset.datasetprice) - 1; await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.be.revertedWith( 'iExecV5-matchOrders-0x04', ); }); - it('[TEE] Should fail when workerpool max price is less than workerpool price', async () => { + it('Should fail when workerpool max price is less than workerpool price', async () => { orders.requester.workerpoolmaxprice = Number(orders.workerpool.workerpoolprice) - 1; await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.be.revertedWith( 'iExecV5-matchOrders-0x05', ); }); - it('[TEE] Should fail when workerpool tag does not satisfy app, dataset and request requirements', async () => { + it('Should fail when workerpool tag does not satisfy app, dataset and request requirements', async () => { orders.app.tag = '0x0000000000000000000000000000000000000000000000000000000000000001'; // 0b0001 orders.dataset.tag = '0x0000000000000000000000000000000000000000000000000000000000000002'; // 0b0010 @@ -346,7 +474,7 @@ describe('IexecPoco1', () => { ); }); - it('[TEE] Should fail when the last bit of app tag does not satisfy dataset or request requirements', async () => { + it('Should fail when the last bit of app tag does not satisfy dataset or request requirements', async () => { // The last bit of dataset and request tag is 1, but app tag does not set it orders.app.tag = '0x0000000000000000000000000000000000000000000000000000000000000002'; // 0b0010 orders.dataset.tag = @@ -362,21 +490,21 @@ describe('IexecPoco1', () => { ); }); - it('[TEE] Should fail when apps are different', async () => { + it('Should fail when apps are different', async () => { orders.requester.app = randomAddress; await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.be.revertedWith( 'iExecV5-matchOrders-0x10', ); }); - it('[TEE] Should fail when datasets are different', async () => { + it('Should fail when datasets are different', async () => { orders.requester.dataset = randomAddress; await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.be.revertedWith( 'iExecV5-matchOrders-0x11', ); }); - it('[TEE] Should fail when request order workerpool mismatches workerpool order workerpool (EOA, SC)', async () => { + it('Should fail when request order mismatches workerpool restriction (EOA, SC)', async () => { orders.requester.workerpool = randomAddress; await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.be.revertedWith( 'iExecV5-matchOrders-0x12', @@ -388,8 +516,10 @@ describe('IexecPoco1', () => { }); /** - * Dynamically generated tests for all different restrictions in orders - * (requesterrestrict, apprestrict, workerpoolrestrict, datasetrestrict). + * Failed match orders because of restriction mismatch (apprestrict, + * datasetrestrict, workerpoolrestrict, requesterrestrict). + * Note: Workerpool is the only restriction in request order and it is + * tested elsewhere. */ const revertMessages: { [key: string]: { [key: string]: string } } = { app: { @@ -408,22 +538,21 @@ describe('IexecPoco1', () => { requester: 'iExecV5-matchOrders-0x1b', }, }; - ['app', 'workerpool', 'dataset'].forEach((orderName) => { - // No request order - ['requester', 'app', 'workerpool', 'dataset'].forEach((assetName) => { - // Filter irrelevant cases. E.g. no need to change the app address in the app order. + ['app', 'dataset', 'workerpool'].forEach((orderName) => { + ['app', 'dataset', 'workerpool', 'requester'].forEach((assetName) => { + // Filter irrelevant cases (e.g. app - app). if (orderName.includes(assetName)) { return; } - it(`[TEE] Should fail when ${orderName} order mismatch ${assetName} restriction (EOA, SC)`, async function () { + it(`Should fail when ${orderName} order mismatches ${assetName} restriction (EOA, SC)`, async () => { const message = revertMessages[orderName][assetName]; // EOA - orders[orderName][assetName + 'restrict'] = randomAddress; // e.g. orders['app']['apprestrict'] = 0xEOA + orders[orderName][assetName + 'restrict'] = randomAddress; // e.g. orders.app.datasetrestrict = 0xEOA await expect(iexecPoco.matchOrders(...orders.toArray())).to.be.revertedWith( message, ); // SC - orders[orderName][assetName + 'restrict'] = randomContract.address; // e.g. orders['app']['apprestrict'] = 0xSC + orders[orderName][assetName + 'restrict'] = randomContract.address; // e.g. orders.app.datasetrestrict = 0xSC await expect(iexecPoco.matchOrders(...orders.toArray())).to.be.revertedWith( message, ); @@ -431,15 +560,272 @@ describe('IexecPoco1', () => { }); }); - it('[TODO] Should match orders with replication', () => {}); + it('Should fail when app is not registered', async () => { + orders.app.app = randomContract.address; // Must be an Ownable contract. + orders.requester.app = randomContract.address; + await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.be.revertedWith( + 'iExecV5-matchOrders-0x20', + ); + }); + + it('Should fail when invalid app order signature from EOA', async () => { + await signOrders(iexecWrapper.getDomain(), orders, ordersActors); + orders.app.sign = randomSignature; // Override signature. + await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.be.revertedWith( + 'iExecV5-matchOrders-0x21', + ); + }); + + it('Should fail when invalid app order signature from SC', async () => { + await signOrders(iexecWrapper.getDomain(), orders, ordersActors); + orders.app.sign = randomSignature; // Override signature. + // Transfer ownership of the app to the ERC1271 contract. + await IERC721__factory.connect(await iexecPoco.appregistry(), appProvider) + .transferFrom( + appProvider.address, + erc1271MockContract.address, + appAddress, // tokenId + ) + .then((tx) => tx.wait()); + // Make sure the test does not fail because of another reason. + const signerAddress = ethers.utils.verifyMessage( + hashOrder(iexecWrapper.getDomain(), orders.app), + orders.app.sign as any, + ); + expect(signerAddress).to.not.equal(erc1271MockContract.address); // owner of app. + // Match orders. + await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.be.revertedWith( + 'iExecV5-matchOrders-0x21', + ); + }); + + it('Should fail when dataset is not registered', async () => { + orders.dataset.dataset = randomContract.address; // Must be an Ownable contract. + orders.requester.dataset = randomContract.address; + await signOrder(iexecWrapper.getDomain(), orders.app, appProvider); + await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.be.revertedWith( + 'iExecV5-matchOrders-0x30', + ); + }); + + it('Should fail when invalid dataset order signature from EOA', async () => { + await signOrders(iexecWrapper.getDomain(), orders, ordersActors); + orders.dataset.sign = randomSignature; // Override signature. + await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.be.revertedWith( + 'iExecV5-matchOrders-0x31', + ); + }); + + it('Should fail when invalid dataset order signature from SC', async () => { + await signOrders(iexecWrapper.getDomain(), orders, ordersActors); + orders.dataset.sign = randomSignature; // Override signature. + // Transfer ownership of the dataset to the ERC1271 contract. + await IERC721__factory.connect(await iexecPoco.datasetregistry(), datasetProvider) + .transferFrom( + datasetProvider.address, + erc1271MockContract.address, + datasetAddress, // tokenId + ) + .then((tx) => tx.wait()); + // Make sure the test does not fail because of another reason. + const signerAddress = ethers.utils.verifyMessage( + hashOrder(iexecWrapper.getDomain(), orders.dataset), + orders.dataset.sign as any, + ); + expect(signerAddress).to.not.equal(erc1271MockContract.address); // owner of dataset. + // Match orders. + await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.be.revertedWith( + 'iExecV5-matchOrders-0x31', + ); + }); + + it('Should fail when workerpool is not registered', async () => { + orders.workerpool.workerpool = randomContract.address; // Must be an Ownable contract. + orders.requester.workerpool = randomContract.address; + await signOrder(iexecWrapper.getDomain(), orders.app, appProvider); + await signOrder(iexecWrapper.getDomain(), orders.dataset, datasetProvider); + await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.be.revertedWith( + 'iExecV5-matchOrders-0x40', + ); + }); + + it('Should fail when invalid workerpool order signature from EOA', async () => { + await signOrders(iexecWrapper.getDomain(), orders, ordersActors); + orders.workerpool.sign = randomSignature; // Override signature. + await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.be.revertedWith( + 'iExecV5-matchOrders-0x41', + ); + }); + + it('Should fail when invalid workerpool order signature from SC', async () => { + await signOrders(iexecWrapper.getDomain(), orders, ordersActors); + orders.workerpool.sign = randomSignature; // Override signature. + // Transfer ownership of the workerpool to the ERC1271 contract. + await IERC721__factory.connect(await iexecPoco.workerpoolregistry(), scheduler) + .transferFrom( + scheduler.address, + erc1271MockContract.address, + workerpoolAddress, // tokenId + ) + .then((tx) => tx.wait()); + // Make sure the test does not fail because of another reason. + const signerAddress = ethers.utils.verifyMessage( + hashOrder(iexecWrapper.getDomain(), orders.workerpool), + orders.workerpool.sign as any, + ); + expect(signerAddress).to.not.equal(erc1271MockContract.address); // owner of workerpool. + // Match orders. + await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.be.revertedWith( + 'iExecV5-matchOrders-0x41', + ); + }); + + it('Should fail when invalid request order signature from EOA', async () => { + await signOrders(iexecWrapper.getDomain(), orders, ordersActors); + orders.requester.sign = randomSignature; // Override signature. + await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.be.revertedWith( + 'iExecV5-matchOrders-0x50', + ); + }); + + it('Should fail when invalid request order signature from SC', async () => { + await signOrders(iexecWrapper.getDomain(), orders, ordersActors); + orders.requester.sign = randomSignature; // Override signature. + // Set the smart contract as the requester. + orders.requester.requester = erc1271MockContract.address; + // Make sure the test does not fail because of another reason. + const signerAddress = ethers.utils.verifyMessage( + hashOrder(iexecWrapper.getDomain(), orders.requester), + orders.requester.sign as any, + ); + expect(signerAddress).to.not.equal(erc1271MockContract.address); // Requester. + // Match orders. + await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.be.revertedWith( + 'iExecV5-matchOrders-0x50', + ); + }); + + it('Should fail if one or more orders are consumed', async () => { + orders.app.volume = 0; + // TODO Set order as consumed directly in storage using the following code. + // Needs more debugging. + // + // const appOrderHash = iexecWrapper.hashOrder(orders.app); + // const appOrderConsumedSlotIndex = ethers.utils.keccak256( + // ethers.utils.concat([ + // appOrderHash, // key in the mapping. + // '0x12', // m_consumed mapping index. + // ]) + // ); + // // Set order as fully consumed. + // await setStorageAt( + // iexecPoco.address, + // appOrderConsumedSlotIndex, + // ethers.utils.hexlify(Number(orders.app.volume)), + // ); + await depositForRequesterAndSchedulerWithDefaultPrices(botVolume); + await signOrders(iexecWrapper.getDomain(), orders, ordersActors); + await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.be.revertedWith( + 'iExecV5-matchOrders-0x60', + ); + }); + + it('Should fail when requester has insufficient balance', async () => { + const dealPrice = (appPrice + datasetPrice + workerpoolPrice) * volume; + const schedulerStake = await iexecWrapper.computeSchedulerDealStake( + workerpoolPrice, + volume, + ); + // Deposit less than deal price in the requester's account. + await iexecWrapper.depositInIexecAccount(requester, dealPrice - 1); + await iexecWrapper.depositInIexecAccount(scheduler, schedulerStake); + expect(await iexecPoco.balanceOf(requester.address)).to.be.lessThan(dealPrice); + expect(await iexecPoco.balanceOf(scheduler.address)).to.equal(schedulerStake); + await signOrders(iexecWrapper.getDomain(), orders, ordersActors); + await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.be.revertedWith( + 'IexecEscrow: Transfer amount exceeds balance', + ); + }); + + it('Should fail when scheduler has insufficient balance', async () => { + const dealPrice = (appPrice + datasetPrice + workerpoolPrice) * volume; + const schedulerStake = await iexecWrapper.computeSchedulerDealStake( + workerpoolPrice, + volume, + ); + await iexecWrapper.depositInIexecAccount(requester, dealPrice); + // Deposit less than stake value in the scheduler's account. + await iexecWrapper.depositInIexecAccount(scheduler, schedulerStake - 1); + expect(await iexecPoco.balanceOf(requester.address)).to.equal(dealPrice); + expect(await iexecPoco.balanceOf(scheduler.address)).to.be.lessThan(schedulerStake); + await signOrders(iexecWrapper.getDomain(), orders, ordersActors); + await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.be.revertedWith( + 'IexecEscrow: Transfer amount exceeds balance', + ); + }); + }); + + describe('Sponsor match orders', () => { + it('Should sponsor match orders', async () => { + // Compute prices, stakes, rewards, ... + const dealPrice = + (appPrice + datasetPrice + workerpoolPrice) * // task price + volume; + const schedulerStake = await iexecWrapper.computeSchedulerDealStake( + workerpoolPrice, + volume, + ); + // Deposit required amounts. + await iexecWrapper.depositInIexecAccount(sponsor, dealPrice); + await iexecWrapper.depositInIexecAccount(scheduler, schedulerStake); + // Save frozen balances before match. + const sponsorFrozenBefore = (await iexecPoco.frozenOf(sponsor.address)).toNumber(); + // Sign and match orders. + await signOrders(iexecWrapper.getDomain(), orders, ordersActors); + const dealId = getDealId(iexecWrapper.getDomain(), orders.requester); + const tx = iexecPocoAsSponsor.sponsorMatchOrders(...orders.toArray()); + // Check balances and frozen. + await expect(tx).to.changeTokenBalances( + iexecPoco, + [iexecPoco, sponsor, scheduler, requester], + [dealPrice + schedulerStake, -dealPrice, -schedulerStake, 0], + ); + expect(await iexecPoco.frozenOf(requester.address)).to.equal(0); + expect(await iexecPoco.frozenOf(sponsor.address)).to.equal( + sponsorFrozenBefore + dealPrice, + ); + // Check events. + await expect(tx).to.emit(iexecPoco, 'OrdersMatched'); + // Check deal + const deal = await iexecPoco.viewDeal(dealId); + expect(deal.sponsor).to.equal(sponsor.address); + }); + + it('Should fail when sponsor has insufficient balance', async () => { + // Compute prices, stakes, rewards, ... + const dealPrice = + (appPrice + datasetPrice + workerpoolPrice) * // task price + volume; + const schedulerStake = await iexecWrapper.computeSchedulerDealStake( + workerpoolPrice, + volume, + ); + // Deposit less than deal price in the sponsor's account. + await iexecWrapper.depositInIexecAccount(sponsor, dealPrice - 1); + await iexecWrapper.depositInIexecAccount(scheduler, schedulerStake); + // Sign and match orders. + await signOrders(iexecWrapper.getDomain(), orders, ordersActors); + await expect( + iexecPocoAsSponsor.sponsorMatchOrders(...orders.toArray()), + ).to.be.revertedWith('IexecEscrow: Transfer amount exceeds balance'); + }); }); - describe('[TODO] Sponsor match orders', () => {}); /** * Helper function to deposit requester and scheduler stakes with * default prices for tests that do not rely on price changes. */ - async function depositForRequesterAndSchedulerWithDefaultPrices() { + async function depositForRequesterAndSchedulerWithDefaultPrices(volume: number) { const dealPrice = (appPrice + datasetPrice + workerpoolPrice) * volume; const schedulerStake = await iexecWrapper.computeSchedulerDealStake( workerpoolPrice, diff --git a/utils/poco-tools.ts b/utils/poco-tools.ts index 5af2c3998..ab5cd56f2 100644 --- a/utils/poco-tools.ts +++ b/utils/poco-tools.ts @@ -76,7 +76,7 @@ export async function getIexecAccounts(): Promise { export function getDealId( domain: TypedDataDomain, requestOrder: IexecLibOrders_v5.RequestOrderStruct, - taskIndex: number, + taskIndex: number = 0, ): string { return ethers.utils.solidityKeccak256( ['bytes32', 'uint256'],