diff --git a/CHANGELOG.md b/CHANGELOG.md index 640c14648..49f837ac7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## vNEXT - Migrate proxy to Diamond pattern (ERC-2535): + - Add Diamond contract unit tests (#224) - Fix `fallback` and `receive` (#223) - Migrate contracts (#222) - Add Github Action CI in order to publish NPM package diff --git a/contracts/Diamond.sol b/contracts/Diamond.sol index 7ac06c8c5..45a5299f9 100644 --- a/contracts/Diamond.sol +++ b/contracts/Diamond.sol @@ -1,4 +1,6 @@ -// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: 2025 IEXEC BLOCKCHAIN TECH +// SPDX-License-Identifier: Apache-2.0 + pragma solidity ^0.8.0; //*************************************************************************************\ @@ -8,8 +10,9 @@ pragma solidity ^0.8.0; //* Implementation of a diamond. //*************************************************************************************/ -// Diamond proxy implementation adapted from Mudgen's to re-direct +// Diamond proxy implementation adapted from Mudgen's to redirect // `receive` and `fallback` calls to the implementations in facets. +// See diff at: https://github.com/iExecBlockchainComputing/PoCo/pull/223/commits/0562f982 import { LibDiamond } from "@mudgen/diamond-1/contracts/libraries/LibDiamond.sol"; import { IDiamondCut } from "@mudgen/diamond-1/contracts/interfaces/IDiamondCut.sol"; diff --git a/contracts/tools/Migrations.sol b/contracts/tools/Migrations.sol deleted file mode 100644 index 5b8af327e..000000000 --- a/contracts/tools/Migrations.sol +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -/****************************************************************************** - * Copyright 2020 IEXEC BLOCKCHAIN TECH * - * * - * Licensed under the Apache License, Version 2.0 (the "License"); * - * you may not use this file except in compliance with the License. * - * You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an "AS IS" BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - ******************************************************************************/ - -pragma solidity ^0.6.0; - -import "@openzeppelin/contracts/access/Ownable.sol"; - - -contract Migrations is Ownable -{ - uint256 public lastCompletedMigration; - - constructor() - public - { - } - - function setCompleted(uint completed) public onlyOwner - { - lastCompletedMigration = completed; - } - - function upgrade(address newAddress) public onlyOwner - { - Migrations upgraded = Migrations(newAddress); - upgraded.setCompleted(lastCompletedMigration); - } -} diff --git a/deploy/0_deploy.ts b/deploy/0_deploy.ts index be7583351..d6b0ddcfe 100644 --- a/deploy/0_deploy.ts +++ b/deploy/0_deploy.ts @@ -42,6 +42,7 @@ import { FactoryDeployer } from '../utils/FactoryDeployer'; import config from '../utils/config'; import { getFunctionSelectors, linkContractToProxy } from '../utils/proxy-tools'; import { DiamondArgsStruct } from '../typechain/@mudgen/diamond-1/contracts/Diamond'; +import { getLibDiamondConfigOrEmpty } from '../utils/tools'; let factoryDeployer: FactoryDeployer; @@ -72,7 +73,7 @@ export default async function deploy() { throw new Error('Failed to prepare transferOwnership data'); }); const erc1538ProxyAddress = await deployDiamondProxyWithDefaultFacets( - owner.address, + owner, // transferOwnershipCall, //TODO ); const erc1538 = DiamondCutFacet__factory.connect(erc1538ProxyAddress, owner); @@ -248,18 +249,12 @@ async function getOrDeployRlc(token: string, owner: SignerWithAddress) { * @returns The address of the deployed Diamond proxy contract. */ async function deployDiamondProxyWithDefaultFacets( - ownerAddress: string, + owner: SignerWithAddress, // transferOwnershipCall: string, // TODO ): Promise { const initAddress = await factoryDeployer.deployContract(new DiamondInit__factory()); const initCalldata = DiamondInit__factory.createInterface().encodeFunctionData('init'); - // Deploy LibDiamond and link it to fix coverage task issue. - const libDiamondAddress = - (hre as any).__SOLIDITY_COVERAGE_RUNNING && - (await factoryDeployer.deployContract(new LibDiamond__factory())); - const libDiamondConfig = (hre as any).__SOLIDITY_COVERAGE_RUNNING && { - ['@mudgen/diamond-1/contracts/libraries/LibDiamond.sol:LibDiamond']: libDiamondAddress, - }; + const libDiamondConfig = await getLibDiamondConfigOrEmpty(owner); // Deploy required proxy facets. const facetFactories = [ new DiamondCutFacet__factory(libDiamondConfig), @@ -278,7 +273,7 @@ async function deployDiamondProxyWithDefaultFacets( } // Set diamond constructor arguments const diamondArgs: DiamondArgsStruct = { - owner: ownerAddress, + owner: owner.address, init: initAddress, initCalldata: initCalldata, }; diff --git a/test/byContract/Diamond.test.ts b/test/byContract/Diamond.test.ts new file mode 100644 index 000000000..fa0632c94 --- /dev/null +++ b/test/byContract/Diamond.test.ts @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2025 IEXEC BLOCKCHAIN TECH +// SPDX-License-Identifier: Apache-2.0 + +import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; +import { getStorageAt } from '@nomicfoundation/hardhat-network-helpers'; +import { expect } from 'chai'; +import { ZeroAddress } from 'ethers'; +import { ethers } from 'hardhat'; +import { FacetCutAction } from 'hardhat-deploy/dist/types'; +import { Diamond__factory, DiamondLoupeFacet__factory, IDiamond } from '../../typechain'; +import { DiamondArgsStruct } from '../../typechain/contracts/Diamond'; +import { getFunctionSelectors } from '../../utils/proxy-tools'; +import { getLibDiamondConfigOrEmpty } from '../../utils/tools'; + +const DIAMOND_STORAGE_POSITION = ethers.id('diamond.standard.diamond.storage'); + +describe('Diamond', async () => { + let deployer: SignerWithAddress; + let owner: SignerWithAddress; + + beforeEach('Deploy', async () => { + [deployer, owner] = await ethers.getSigners(); + }); + + describe('Deployment', () => { + it('Should set owner at deployment', async () => { + const diamond = await _deployDiamond([]); // No facets + const diamondAddress = await diamond.getAddress(); + // Check the owner. + const ownerSlotPosition = ethers.toBeHex(BigInt(DIAMOND_STORAGE_POSITION) + 3n); + const actualOwnerAddress = await getStorageAt(diamondAddress, ownerSlotPosition); + const expectedOwnerAddress = ethers.zeroPadValue(owner.address, 32); // Padded to 32 bytes. + expect(actualOwnerAddress).to.equal(expectedOwnerAddress); + // Check `DiamondCut` event with empty facet cuts. + await expect(diamond.deploymentTransaction()) + .to.emit(diamond, 'DiamondCut') + .withArgs([], ZeroAddress, '0x'); + }); + + it('Should apply diamond cuts at deployment', async () => { + // Deploy any facet. + const facet = await new DiamondLoupeFacet__factory() + .connect(deployer) + .deploy() + .then((tx) => tx.waitForDeployment()); + const facetCuts = [ + { + facetAddress: await facet.getAddress(), + action: FacetCutAction.Add, + functionSelectors: getFunctionSelectors(new DiamondLoupeFacet__factory()), + }, + ]; + const diamond = await _deployDiamond(facetCuts); + await expect(diamond.deploymentTransaction()) + .to.emit(diamond, 'DiamondCut') + .withArgs( + [Object.values(facetCuts[0])], // Convert object to array for deep comparison. + ZeroAddress, + '0x', + ); + }); + }); + + describe('Delegatecall', () => { + it.skip('[TODO] Should delegate `fallback` call', async () => {}); + + it.skip('[TODO] Should delegate `receive` call', async () => {}); + + it.skip('[TODO] Should delegate any function call', async () => {}); + + it('Should revert when function not found', async () => { + const diamond = await _deployDiamond([]); + const randomData = ethers.id('0xrandom'); + const tx = owner.sendTransaction({ + to: await diamond.getAddress(), + value: 0, + data: randomData, + }); + await expect(tx) + .to.be.revertedWithCustomError(diamond, 'FunctionNotFound') + .withArgs(randomData.slice(0, 10)); // First 4 bytes. + }); + }); + + async function _deployDiamond(facetCuts: IDiamond.FacetCutStruct[]) { + const libDiamondConfig = await getLibDiamondConfigOrEmpty(deployer); + return await new Diamond__factory(libDiamondConfig) + .connect(deployer) + .deploy(facetCuts, { + owner: owner.address, + init: ZeroAddress, + initCalldata: '0x', + } as DiamondArgsStruct) + .then((tx) => tx.waitForDeployment()); + } +}); diff --git a/utils/tools.ts b/utils/tools.ts index 937ccced6..3d3ba3eef 100644 --- a/utils/tools.ts +++ b/utils/tools.ts @@ -1,8 +1,10 @@ // SPDX-FileCopyrightText: 2020-2025 IEXEC BLOCKCHAIN TECH // SPDX-License-Identifier: Apache-2.0 +import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; import { Signature } from 'ethers'; -import { ethers } from 'hardhat'; +import hre, { ethers } from 'hardhat'; +import { LibDiamond__factory } from '../typechain'; export function compactSignature(signature: string): string { return Signature.from(signature).compactSerialized; @@ -19,3 +21,27 @@ export function minBigInt(a: bigint, b: bigint) { export function maxBigInt(a: bigint, b: bigint) { return a > b ? a : b; } + +/** + * Deploys the `LibDiamond` library if running coverage task and returns + * the linking configuration. Returns an empty config if not running coverage + * task. + * This fixes an issue with the coverage task that requires the library to be + * deployed and linked to contracts where it is used. + * @param deployer Signer to deploy the library. + * @returns The library configuration or an empty object. + */ +export async function getLibDiamondConfigOrEmpty(deployer: SignerWithAddress): Promise { + // No need to deploy the library if not running coverage task. + if (!(hre as any).__SOLIDITY_COVERAGE_RUNNING) { + return {}; + } + const libDiamondAddress = await new LibDiamond__factory() + .connect(deployer) + .deploy() + .then((contract) => contract.waitForDeployment()) + .then((contract) => contract.getAddress()); + return { + ['@mudgen/diamond-1/contracts/libraries/LibDiamond.sol:LibDiamond']: libDiamondAddress, + }; +}