diff --git a/contracts/factory/ContractsDeployer.sol b/contracts/factory/ContractsDeployer.sol new file mode 100644 index 00000000..9724cd3b --- /dev/null +++ b/contracts/factory/ContractsDeployer.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-3.0 +// +// :+#####%%%%%%%%%%%%%%+ +// .-*@@@%+.:+%@@@@@%%#***%@@%= +// :=*%@@@#=. :#@@% *@@@%= +// .-+*%@%*-.:+%@@@@@@+. -*+: .=#. :%@@@%- +// :=*@@@@%%@@@@@@@@@%@@@- .=#@@@%@%= =@@@@#. +// -=+#%@@%#*=:. :%@@@@%. -*@@#*@@@@@@@#=:- *@@@@+ +// =@@%=:. :=: *@@@@@%#- =%*%@@@@#+-. =+ :%@@@%- +// -@@%. .+@@@ =+=-. @@#- +@@@%- =@@@@%: +// :@@@. .+@@#%: : .=*=-::.-%@@@+*@@= +@@@@#. +// %@@: +@%%* =%@@@@@@@@@@@#. .*@%- +@@@@*. +// #@@= .+@@@@%:=*@@@@@- :%@%: .*@@@@+ +// *@@* +@@@#-@@%-:%@@* +@@#. :%@@@@- +// -@@% .:-=++*##%%%@@@@@@@@@@@@*. :@+.@@@%: .#@@+ =@@@@#: +// .@@@*-+*#%%%@@@@@@@@@@@@@@@@%%#**@@%@@@. *@=*@@# :#@%= .#@@@@#- +// -%@@@@@@@@@@@@@@@*+==-:-@@@= *@# .#@*-=*@@@@%= -%@@@* =@@@@@%- +// -+%@@@#. %@%%= -@@:+@: -@@* *@@*-:: -%@@%=. .*@@@@@# +// *@@@* +@* *@@##@@- #@*@@+ -@@= . :+@@@#: .-+@@@%+- +// +@@@%*@@:..=@@@@* .@@@* .#@#. .=+- .=%@@@*. :+#@@@@*=: +// =@@@@%@@@@@@@@@@@@@@@@@@@@@@%- :+#*. :*@@@%=. .=#@@@@%+: +// .%@@= ..... .=#@@+. .#@@@*: -*%@@@@%+. +// +@@#+===---:::... .=%@@*- +@@@+. -*@@@@@%+. +// -@@@@@@@@@@@@@@@@@@@@@@%@@@@= -@@@+ -#@@@@@#=. +// ..:::---===+++***###%%%@@@#- .#@@+ -*@@@@@#=. +// @@@@@@+. +@@*. .+@@@@@%=. +// -@@@@@= =@@%: -#@@@@%+. +// +@@@@@. =@@@= .+@@@@@*: +// #@@@@#:%@@#. :*@@@@#- +// @@@@@%@@@= :#@@@@+. +// :@@@@@@@#.:#@@@%- +// +@@@@@@-.*@@@*: +// #@@@@#.=@@@+. +// @@@@+-%@%= +// :@@@#%@%= +// +@@@@%- +// :#%%= +// +/** + * NOTICE + * + * The T-REX software is licensed under a proprietary license or the GPL v.3. + * If you choose to receive it under the GPL v.3 license, the following applies: + * T-REX is a suite of smart contracts implementing the ERC-3643 standard and + * developed by Tokeny to manage and transfer financial assets on EVM blockchains + * + * Copyright (C) 2023, Tokeny sàrl. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +pragma solidity 0.8.17; + +import "../roles/AgentRole.sol"; + +/// @notice Error thrown when a contract with the same name has already been deployed. +/// @param addr The address of the previously deployed contract. +error ContractDeployedAlready(address addr); + +contract ContractsDeployer is AgentRole { + + /// @notice Maps a human-readable name to the address of a deployed contract. + /// @dev Used to retrieve contract addresses deployed by this deployer. + mapping(string => address) private _deployedContracts; + + /// @notice Emitted when a contract is deployed. + /// @param name The human-readable name of the deployed contract. + /// @param contractAddress The address of the deployed contract. + event ContractDeployed(string name, address contractAddress); + + + /** + * @dev Deploys a contract using the create2 opcode, ensuring deterministic address generation. + * @param name A human-readable name for the contract, used for referencing in the deployedContracts mapping. + * @param bytecode The bytecode of the contract to be deployed. + * @return addr The address of the deployed contract. + * @notice The function will revert with `ContractDeployedAlready` if a contract with the same name has been deployed. + */ + function deployContract(string memory name, bytes memory bytecode) external onlyAgent returns (address) { + bytes32 salt = keccak256(bytecode); + if (_deployedContracts[name] != address(0)) { + revert ContractDeployedAlready(_deployedContracts[name]); + } + + address addr; + // solhint-disable-next-line no-inline-assembly + assembly { + let encoded_data := add(0x20, bytecode) // Load initialization code. + let encoded_size := mload(bytecode) // Load init code's length. + addr := create2(0, encoded_data, encoded_size, salt) + if iszero(extcodesize(addr)) { + revert(0, 0) + } + } + _deployedContracts[name] = addr; + emit ContractDeployed(name, addr); + return addr; + } + + /** + * @dev Transfers the ownership of a contract to a new owner. + * @param _contract The address of the contract whose ownership is to be transferred. + * @param _owner The address of the new owner. + * @notice This function can only be called by an agent. + */ + function recoverContractOwnership(address _contract, address _owner) external onlyAgent { + Ownable(_contract).transferOwnership(_owner); + } + + /** + * @dev Retrieves the address of a deployed contract by its name. + * @param name The name of the contract. + * @return The address of the deployed contract. + */ + function getContract(string calldata name) external view returns (address) { + return _deployedContracts[name]; + } +} diff --git a/index.d.ts b/index.d.ts index 65d49032..190fedbf 100644 --- a/index.d.ts +++ b/index.d.ts @@ -6,7 +6,7 @@ type ContractJSON = { bytecode: string; deployedBytecode: string; linkReferences: any; -} +}; export namespace contracts { // Token @@ -48,6 +48,9 @@ export namespace contracts { export const TREXFactory: ContractJSON; // gateway export const TREXGateway: ContractJSON; + // contractsDeployer + + export const ContractsDeployer: ContractJSON; // DVD export const DVDTransferManager: ContractJSON; // DVA diff --git a/index.js b/index.js index 4a2d8819..fac7cd06 100644 --- a/index.js +++ b/index.js @@ -48,6 +48,8 @@ const TREXFactory = require('./artifacts/contracts/factory/TREXFactory.sol/TREXF // gateway const ITREXGateway = require('./artifacts/contracts/factory/ITREXGateway.sol/ITREXGateway.json'); const TREXGateway = require('./artifacts/contracts/factory/TREXGateway.sol/TREXGateway.json'); +// contractsDeployer +const ContractsDeployer = require('./artifacts/contracts/factory/ContractsDeployer.sol/ContractsDeployer.json'); // DVD const DVDTransferManager = require('./artifacts/contracts/DVD/DVDTransferManager.sol/DVDTransferManager.json'); // DVA @@ -112,6 +114,8 @@ module.exports = { TREXFactory, // gateway TREXGateway, + // contractsDeployer + ContractsDeployer, // DVD DVDTransferManager, // DVA diff --git a/package-lock.json b/package-lock.json index ba8037dd..bd7abd63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,13 +6,13 @@ "packages": { "": { "name": "@tokenysolutions/t-rex", - "version": "4.1.0-beta5", + "version": "4.1.1", "license": "SEE LICENSE IN LICENSE.md", "devDependencies": { "@commitlint/cli": "^17.6.1", "@nomicfoundation/hardhat-toolbox": "^2.0.2", "@nomiclabs/hardhat-solhint": "^3.0.1", - "@onchain-id/solidity": "^2.0.0", + "@onchain-id/solidity": "^2.2.0", "@openzeppelin/contracts": "^4.8.3", "@openzeppelin/contracts-upgradeable": "^4.8.3", "@primitivefi/hardhat-dodoc": "^0.2.3", @@ -2429,9 +2429,9 @@ } }, "node_modules/@onchain-id/solidity": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@onchain-id/solidity/-/solidity-2.1.0.tgz", - "integrity": "sha512-BBpBmUs8gg99N5NIKzANbs3R6DUhd90z2GEFvScVBhpHvpQhI5IzPNFRP6cXua1Lo0dWdNffxsteB3eUY1XMZw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@onchain-id/solidity/-/solidity-2.2.0.tgz", + "integrity": "sha512-rUE5kFToihuvRAhtZXihOrpsqPOxN66v9hmTzlZOQJ2B+yKruLg4/nU/sJ7VCnheW4IeEm8OlA2oUJsdm1j88w==", "dev": true }, "node_modules/@openzeppelin/contracts": { diff --git a/package.json b/package.json index 55686019..22aecea2 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@commitlint/cli": "^17.6.1", "@nomicfoundation/hardhat-toolbox": "^2.0.2", "@nomiclabs/hardhat-solhint": "^3.0.1", - "@onchain-id/solidity": "^2.0.0", + "@onchain-id/solidity": "^2.2.0", "@openzeppelin/contracts": "^4.8.3", "@openzeppelin/contracts-upgradeable": "^4.8.3", "@primitivefi/hardhat-dodoc": "^0.2.3", diff --git a/test/fixtures/deploy-full-suite.fixture.ts b/test/fixtures/deploy-full-suite.fixture.ts index eefa68ed..56198973 100644 --- a/test/fixtures/deploy-full-suite.fixture.ts +++ b/test/fixtures/deploy-full-suite.fixture.ts @@ -12,40 +12,68 @@ export async function deployIdentityProxy(implementationAuthority: Contract['add return ethers.getContractAt('Identity', identity.address, signer); } +// Function to deploy a single contract through ContractsDeployer +async function deployAndLoadContract(contractsDeployer: Contract, contractName: string): Promise { + // Fetch bytecode + const ContractFactory = await ethers.getContractFactory(contractName); + const bytecode = ContractFactory.bytecode; + + // Deploy contract using ContractsDeployer + await contractsDeployer.deployContract(contractName, bytecode); + + // Retrieve the deployed contract address + const address = await contractsDeployer.getContract(contractName); + + // Load the contract + return ethers.getContractAt(contractName, address); +} + +async function deployAndLoadContractWithArgs(contractsDeployer: Contract, contractName: string, constructorArgs: unknown[]): Promise { + // Create a ContractFactory with the ABI, bytecode, and constructor arguments + const ContractFactory = await ethers.getContractFactory(contractName); + const deployTx = ContractFactory.getDeployTransaction(...constructorArgs); + + // Deploy contract using ContractsDeployer with the combined bytecode and constructor args + await contractsDeployer.deployContract(contractName, deployTx.data); + + // Retrieve the deployed contract address + const address = await contractsDeployer.getContract(contractName); + + // Load the contract + return ethers.getContractAt(contractName, address); +} + export async function deployFullSuiteFixture() { const [deployer, tokenIssuer, tokenAgent, tokenAdmin, claimIssuer, aliceWallet, bobWallet, charlieWallet, davidWallet, anotherWallet] = await ethers.getSigners(); const claimIssuerSigningKey = ethers.Wallet.createRandom(); const aliceActionKey = ethers.Wallet.createRandom(); + const contractsDeployer = await ethers.deployContract('ContractsDeployer', deployer); + await contractsDeployer.addAgent(deployer.address); // Deploy implementations - const claimTopicsRegistryImplementation = await ethers.deployContract('ClaimTopicsRegistry', deployer); - const trustedIssuersRegistryImplementation = await ethers.deployContract('TrustedIssuersRegistry', deployer); - const identityRegistryStorageImplementation = await ethers.deployContract('IdentityRegistryStorage', deployer); - const identityRegistryImplementation = await ethers.deployContract('IdentityRegistry', deployer); - const modularComplianceImplementation = await ethers.deployContract('ModularCompliance', deployer); - const tokenImplementation = await ethers.deployContract('Token', deployer); - const identityImplementation = await new ethers.ContractFactory( - OnchainID.contracts.Identity.abi, - OnchainID.contracts.Identity.bytecode, - deployer, - ).deploy(deployer.address, true); - - const identityImplementationAuthority = await new ethers.ContractFactory( - OnchainID.contracts.ImplementationAuthority.abi, - OnchainID.contracts.ImplementationAuthority.bytecode, - deployer, - ).deploy(identityImplementation.address); + const claimTopicsRegistryImplementation = await deployAndLoadContract(contractsDeployer, 'ClaimTopicsRegistry'); + const trustedIssuersRegistryImplementation = await deployAndLoadContract(contractsDeployer, 'TrustedIssuersRegistry'); + const identityRegistryStorageImplementation = await deployAndLoadContract(contractsDeployer, 'IdentityRegistryStorage'); + const identityRegistryImplementation = await deployAndLoadContract(contractsDeployer, 'IdentityRegistry'); + const modularComplianceImplementation = await deployAndLoadContract(contractsDeployer, 'ModularCompliance'); + const tokenImplementation = await deployAndLoadContract(contractsDeployer, 'Token'); + const identityImplementation = await deployAndLoadContractWithArgs(contractsDeployer, 'Identity', [deployer.address, true]); + const identityImplementationAuthority = await deployAndLoadContractWithArgs(contractsDeployer, 'ImplementationAuthority', [ + identityImplementation.address, + ]); + await contractsDeployer.recoverContractOwnership(identityImplementationAuthority.address, deployer.address); const identityFactory = await new ethers.ContractFactory(OnchainID.contracts.Factory.abi, OnchainID.contracts.Factory.bytecode, deployer).deploy( identityImplementationAuthority.address, ); - const trexImplementationAuthority = await ethers.deployContract( - 'TREXImplementationAuthority', - [true, ethers.constants.AddressZero, ethers.constants.AddressZero], - deployer, - ); + const trexImplementationAuthority = await deployAndLoadContractWithArgs(contractsDeployer, 'TREXImplementationAuthority', [ + true, + ethers.constants.AddressZero, + ethers.constants.AddressZero, + ]); + await contractsDeployer.recoverContractOwnership(trexImplementationAuthority.address, deployer.address); const versionStruct = { major: 4, minor: 0,