diff --git a/contracts/facets/IexecPoco1Facet.sol b/contracts/facets/IexecPoco1Facet.sol index 8d8d9e9a2..d3281937d 100644 --- a/contracts/facets/IexecPoco1Facet.sol +++ b/contracts/facets/IexecPoco1Facet.sol @@ -26,13 +26,7 @@ struct Matching { bool hasDataset; } -contract IexecPoco1Facet is - IexecPoco1, - FacetBase, - IexecEscrow, - SignatureVerifier, - IexecPocoCommon -{ +contract IexecPoco1Facet is IexecPoco1, FacetBase, IexecEscrow, SignatureVerifier, IexecPocoCommon { using Math for uint256; using IexecLibOrders_v5 for IexecLibOrders_v5.AppOrder; using IexecLibOrders_v5 for IexecLibOrders_v5.DatasetOrder; @@ -65,6 +59,65 @@ contract IexecPoco1Facet is return _verifySignatureOrPresignature(_identity, _hash, _signature); } + /** + * @notice Public view function to check if a dataset order is compatible with a deal. + * This function performs all the necessary checks to verify dataset order compatibility with a deal. + * + * @dev This function is mainly consumed by offchain clients. It should be carefully inspected if used inside on-chain code. + * This function should not be used in matchOrders as it does not check the same requirements. + * + * @param datasetOrder The dataset order to verify + * @param dealid The deal ID to check against + * @return true if the dataset order is compatible with the deal, false otherwise + */ + function isDatasetCompatibleWithDeal( + IexecLibOrders_v5.DatasetOrder calldata datasetOrder, + bytes32 dealid + ) external view override returns (bool) { + PocoStorageLib.PocoStorage storage $ = PocoStorageLib.getPocoStorage(); + // Check if deal exists + IexecLibCore_v5.Deal storage deal = $.m_deals[dealid]; + if (deal.requester == address(0)) { + return false; + } + // The specified deal should not have a dataset. + if (deal.dataset.pointer != address(0)) { + return false; + } + // Check dataset order owner signature (including presign and EIP1271) + bytes32 datasetOrderHash = _toTypedDataHash(datasetOrder.hash()); + address datasetOwner = IERC5313(datasetOrder.dataset).owner(); + if (!_verifySignatureOrPresignature(datasetOwner, datasetOrderHash, datasetOrder.sign)) { + return false; + } + // Check if dataset order is not fully consumed + if ($.m_consumed[datasetOrderHash] >= datasetOrder.volume) { + return false; + } + // Check if deal app is allowed by dataset order apprestrict (including whitelist) + if (!_isAccountAuthorizedByRestriction(datasetOrder.apprestrict, deal.app.pointer)) { + return false; + } + // Check if deal workerpool is allowed by dataset order workerpoolrestrict (including whitelist) + if ( + !_isAccountAuthorizedByRestriction( + datasetOrder.workerpoolrestrict, + deal.workerpool.pointer + ) + ) { + return false; + } + // Check if deal requester is allowed by dataset order requesterrestrict (including whitelist) + if (!_isAccountAuthorizedByRestriction(datasetOrder.requesterrestrict, deal.requester)) { + return false; + } + // Check if deal tag fulfills all the tag bits of the dataset order + if ((deal.tag & datasetOrder.tag) != datasetOrder.tag) { + return false; + } + return true; + } + /*************************************************************************** * ODB order matching * ***************************************************************************/ diff --git a/contracts/interfaces/IexecPoco1.sol b/contracts/interfaces/IexecPoco1.sol index b68dd67c2..0e2eaca7a 100644 --- a/contracts/interfaces/IexecPoco1.sol +++ b/contracts/interfaces/IexecPoco1.sol @@ -46,4 +46,9 @@ interface IexecPoco1 { IexecLibOrders_v5.WorkerpoolOrder calldata, IexecLibOrders_v5.RequestOrder calldata ) external returns (bytes32); + + function isDatasetCompatibleWithDeal( + IexecLibOrders_v5.DatasetOrder calldata datasetOrder, + bytes32 dealid + ) external view returns (bool); } diff --git a/contracts/interfaces/IexecPoco1.v8.sol b/contracts/interfaces/IexecPoco1.v8.sol index deb90b8e3..bde6885cd 100644 --- a/contracts/interfaces/IexecPoco1.v8.sol +++ b/contracts/interfaces/IexecPoco1.v8.sol @@ -40,4 +40,9 @@ interface IexecPoco1 { IexecLibOrders_v5.WorkerpoolOrder calldata, IexecLibOrders_v5.RequestOrder calldata ) external returns (bytes32); + + function isDatasetCompatibleWithDeal( + IexecLibOrders_v5.DatasetOrder calldata datasetOrder, + bytes32 dealid + ) external view returns (bool); } diff --git a/scripts/upgrades/accessors/deploy-and-update-accessor-facet.ts b/scripts/upgrades/deploy-and-update-some-facet.ts similarity index 65% rename from scripts/upgrades/accessors/deploy-and-update-accessor-facet.ts rename to scripts/upgrades/deploy-and-update-some-facet.ts index f94e2c435..ef1b181c5 100644 --- a/scripts/upgrades/accessors/deploy-and-update-accessor-facet.ts +++ b/scripts/upgrades/deploy-and-update-some-facet.ts @@ -4,20 +4,21 @@ import { ZeroAddress } from 'ethers'; import { ethers } from 'hardhat'; import { FacetCutAction } from 'hardhat-deploy/dist/types'; -import type { IDiamond } from '../../../typechain'; +import type { IDiamond } from '../../typechain'; import { DiamondCutFacet__factory, DiamondLoupeFacet__factory, + IexecPoco1Facet__factory, IexecPocoAccessorsFacet__factory, -} from '../../../typechain'; -import { Ownable__factory } from '../../../typechain/factories/rlc-faucet-contract/contracts'; -import { FactoryDeployer } from '../../../utils/FactoryDeployer'; -import config from '../../../utils/config'; -import { linkContractToProxy } from '../../../utils/proxy-tools'; -import { printFunctions } from '../upgrade-helper'; +} from '../../typechain'; +import { Ownable__factory } from '../../typechain/factories/rlc-faucet-contract/contracts'; +import { FactoryDeployer } from '../../utils/FactoryDeployer'; +import config from '../../utils/config'; +import { linkContractToProxy } from '../../utils/proxy-tools'; +import { printFunctions } from './upgrade-helper'; (async () => { - console.log('Deploying and updating IexecPocoAccessorsFacet...'); + console.log('Deploying and updating IexecPocoAccessorsFacet & IexecPoco1Facet...'); const [account] = await ethers.getSigners(); const chainId = (await ethers.provider.getNetwork()).chainId; @@ -47,18 +48,25 @@ import { printFunctions } from '../upgrade-helper'; proxyOwnerSigner, ); - console.log('\n=== Step 1: Deploying new IexecPocoAccessorsFacet ==='); + console.log('\n=== Step 1: Deploying all new facets ==='); const factoryDeployer = new FactoryDeployer(account, chainId); const iexecLibOrders = { ['contracts/libs/IexecLibOrders_v5.sol:IexecLibOrders_v5']: deploymentOptions.IexecLibOrders_v5, }; - const newFacetFactory = new IexecPocoAccessorsFacet__factory(iexecLibOrders); - const newFacetAddress = await factoryDeployer.deployContract(newFacetFactory); + console.log('Deploying new IexecPocoAccessorsFacet...'); + const iexecPocoAccessorsFacetFactory = new IexecPocoAccessorsFacet__factory(iexecLibOrders); + const iexecPocoAccessorsFacet = await factoryDeployer.deployContract( + new IexecPocoAccessorsFacet__factory(iexecLibOrders), + ); + + console.log('Deploying new IexecPoco1Facet...'); + const newIexecPoco1FacetFactory = new IexecPoco1Facet__factory(iexecLibOrders); + const newIexecPoco1Facet = await factoryDeployer.deployContract(newIexecPoco1FacetFactory); console.log( - '\n=== Step 2: Remove old facets (remove all functions of old accessors facets) ===', + '\n=== Step 2: Remove old facets (IexecAccessorsFacet & IexecPocoAccessorsFacet & IexecPoco1Facet) ===', ); const diamondLoupe = DiamondLoupeFacet__factory.connect(diamondProxyAddress, account); @@ -99,16 +107,17 @@ import { printFunctions } from '../upgrade-helper'; }); } - const oldAccessorFacets = [ + const oldFacets = [ '0xEa232be31ab0112916505Aeb7A2a94b5571DCc6b', //IexecAccessorsFacet '0xeb40697b275413241d9b31dE568C98B3EA12FFF0', //IexecPocoAccessorsFacet + '0x46b555fE117DFd8D4eAC2470FA2d739c6c3a0152', //IexecPoco1Facet ]; - // Remove ALL functions from the old accessor facets using diamondLoupe.facetFunctionSelectors() except of constant founctions - for (const facetAddress of oldAccessorFacets) { + // Remove ALL functions from the old facets using diamondLoupe.facetFunctionSelectors() except of constant founctions + for (const facetAddress of oldFacets) { const selectors = await diamondLoupe.facetFunctionSelectors(facetAddress); if (selectors.length > 0) { console.log( - `Removing old accessor facet ${facetAddress} with ${selectors.length} functions - will remove ALL`, + `Removing old facet ${facetAddress} with ${selectors.length} functions - will remove ALL`, ); removalCuts.push({ facetAddress: ZeroAddress, @@ -131,12 +140,23 @@ import { printFunctions } from '../upgrade-helper'; console.log('Diamond functions after removing old facets:'); await printFunctions(diamondProxyAddress); } - console.log('\n=== Step 3: Updating diamond proxy with new facet ==='); - await linkContractToProxy(diamondProxyAsOwner, newFacetAddress, newFacetFactory); - console.log('New functions added successfully'); + console.log('\n=== Step 3: Updating diamond proxy with all new facets ==='); + console.log('Adding new IexecPocoAccessorsFacet...'); + await linkContractToProxy( + diamondProxyAsOwner, + iexecPocoAccessorsFacet, + iexecPocoAccessorsFacetFactory, + ); + console.log('New IexecPocoAccessorsFacet added successfully'); + + console.log('Adding new IexecPoco1Facet ...'); + await linkContractToProxy(diamondProxyAsOwner, newIexecPoco1Facet, newIexecPoco1FacetFactory); + console.log('New IexecPoco1Facet with isDatasetCompatibleWithDeal added successfully'); - console.log('Diamond functions after adding new facet:'); + console.log('Diamond functions after adding new facets:'); await printFunctions(diamondProxyAddress); console.log('\nUpgrade completed successfully!'); + console.log(`New IexecPocoAccessorsFacet deployed at: ${iexecPocoAccessorsFacet}`); + console.log(`New IexecPoco1Facet deployed at: ${newIexecPoco1Facet}`); })(); diff --git a/test/byContract/IexecPoco/IexecPoco1.test.ts b/test/byContract/IexecPoco/IexecPoco1.test.ts index bc28ef941..8fe235542 100644 --- a/test/byContract/IexecPoco/IexecPoco1.test.ts +++ b/test/byContract/IexecPoco/IexecPoco1.test.ts @@ -1075,6 +1075,203 @@ describe('IexecPoco1', () => { }); }); + describe('isDatasetCompatibleWithDeal', () => { + let dealIdWithoutDataset: string; + let compatibleDatasetOrder: IexecLibOrders_v5.DatasetOrderStruct; + + beforeEach('Create a deal without dataset and dataset orders', async () => { + // Create a deal without dataset + const ordersWithoutDataset = buildOrders({ + assets: { ...ordersAssets, dataset: ZeroAddress }, + prices: ordersPrices, + requester: requester.address, + tag: teeDealTag, + volume: volume, + }); + await depositForRequesterAndSchedulerWithDefaultPrices(volume); + await signOrders(iexecWrapper.getDomain(), ordersWithoutDataset, ordersActors); + dealIdWithoutDataset = getDealId( + iexecWrapper.getDomain(), + ordersWithoutDataset.requester, + ); + await iexecPocoAsRequester.matchOrders(...ordersWithoutDataset.toArray()); + + // Create a compatible dataset order (same restrictions as the deal) + compatibleDatasetOrder = { + dataset: datasetAddress, + datasetprice: datasetPrice, + volume: volume, + tag: teeDealTag, + apprestrict: ordersWithoutDataset.app.app, + workerpoolrestrict: ordersWithoutDataset.workerpool.workerpool, + requesterrestrict: ordersWithoutDataset.requester.requester, + salt: ethers.id('compatible-salt'), + sign: '0x', + }; + await signOrder(iexecWrapper.getDomain(), compatibleDatasetOrder, datasetProvider); + }); + + it('Should return true for compatible dataset order', async () => { + expect( + await iexecPoco.isDatasetCompatibleWithDeal( + compatibleDatasetOrder, + dealIdWithoutDataset, + ), + ).to.be.true; + }); + + it('Should return false for non-existent deal', async () => { + const nonExistentDealId = ethers.id('non-existent-deal'); + expect( + await iexecPoco.isDatasetCompatibleWithDeal( + compatibleDatasetOrder, + nonExistentDealId, + ), + ).to.be.false; + }); + + it('Should return false for deal with a dataset', async () => { + // Use the original orders that include a dataset to create a deal with dataset + const ordersWithDataset = buildOrders({ + assets: ordersAssets, // This includes the dataset + prices: ordersPrices, + requester: requester.address, + tag: teeDealTag, + volume: volume, + }); + + // Use fresh salts to avoid order consumption conflicts + ordersWithDataset.app.salt = ethers.id('fresh-app-salt'); + ordersWithDataset.dataset.salt = ethers.keccak256( + ethers.toUtf8Bytes('fresh-dataset-salt'), + ); + ordersWithDataset.workerpool.salt = ethers.keccak256( + ethers.toUtf8Bytes('fresh-workerpool-salt'), + ); + ordersWithDataset.requester.salt = ethers.keccak256( + ethers.toUtf8Bytes('fresh-requester-salt'), + ); + + await depositForRequesterAndSchedulerWithDefaultPrices(volume); + await signOrders(iexecWrapper.getDomain(), ordersWithDataset, ordersActors); + const dealIdWithDataset = getDealId( + iexecWrapper.getDomain(), + ordersWithDataset.requester, + ); + await iexecPocoAsRequester.matchOrders(...ordersWithDataset.toArray()); + + expect( + await iexecPoco.isDatasetCompatibleWithDeal( + compatibleDatasetOrder, + dealIdWithDataset, + ), + ).to.be.false; + }); + + it('Should return false for dataset order with invalid signature', async () => { + // Create dataset order with invalid signature + const invalidSignatureDatasetOrder = { + ...compatibleDatasetOrder, + sign: randomSignature, // Invalid signature + }; + + expect( + await iexecPoco.isDatasetCompatibleWithDeal( + invalidSignatureDatasetOrder, + dealIdWithoutDataset, + ), + ).to.be.false; + }); + + it('Should return false for fully consumed dataset order', async () => { + // Create dataset order with volume 0 (fully consumed) + const consumedDatasetOrder = { + ...compatibleDatasetOrder, + volume: 0n, + }; + await signOrder(iexecWrapper.getDomain(), consumedDatasetOrder, datasetProvider); + + expect( + await iexecPoco.isDatasetCompatibleWithDeal( + consumedDatasetOrder, + dealIdWithoutDataset, + ), + ).to.be.false; + }); + + it('Should return false for dataset order with incompatible app restriction', async () => { + // Create dataset order with incompatible app restriction + const incompatibleAppDatasetOrder = { + ...compatibleDatasetOrder, + apprestrict: randomAddress, // Different app restriction + }; + await signOrder(iexecWrapper.getDomain(), incompatibleAppDatasetOrder, datasetProvider); + + expect( + await iexecPoco.isDatasetCompatibleWithDeal( + incompatibleAppDatasetOrder, + dealIdWithoutDataset, + ), + ).to.be.false; + }); + + it('Should return false for dataset order with incompatible workerpool restriction', async () => { + // Create dataset order with incompatible workerpool restriction + const incompatibleWorkerpoolDatasetOrder = { + ...compatibleDatasetOrder, + workerpoolrestrict: randomAddress, // Different workerpool restriction + }; + await signOrder( + iexecWrapper.getDomain(), + incompatibleWorkerpoolDatasetOrder, + datasetProvider, + ); + + expect( + await iexecPoco.isDatasetCompatibleWithDeal( + incompatibleWorkerpoolDatasetOrder, + dealIdWithoutDataset, + ), + ).to.be.false; + }); + + it('Should return false for dataset order with incompatible requester restriction', async () => { + // Create dataset order with incompatible requester restriction + const incompatibleRequesterDatasetOrder = { + ...compatibleDatasetOrder, + requesterrestrict: randomAddress, // Different requester restriction + }; + await signOrder( + iexecWrapper.getDomain(), + incompatibleRequesterDatasetOrder, + datasetProvider, + ); + + expect( + await iexecPoco.isDatasetCompatibleWithDeal( + incompatibleRequesterDatasetOrder, + dealIdWithoutDataset, + ), + ).to.be.false; + }); + + it('Should return false for dataset order with incompatible tag', async () => { + // Create dataset order with incompatible tag + const incompatibleTagDatasetOrder = { + ...compatibleDatasetOrder, + tag: '0x0000000000000000000000000000000000000000000000000000000000000002', // Different tag + }; + await signOrder(iexecWrapper.getDomain(), incompatibleTagDatasetOrder, datasetProvider); + + expect( + await iexecPoco.isDatasetCompatibleWithDeal( + incompatibleTagDatasetOrder, + dealIdWithoutDataset, + ), + ).to.be.false; + }); + }); + /** * Helper function to deposit requester and scheduler stakes with * default prices for tests that do not rely on custom prices.