From 01fd82038811ba65798f3ff8fe8273fd191dcb38 Mon Sep 17 00:00:00 2001 From: Skima Harvey <64636974+skimaharvey@users.noreply.github.com> Date: Fri, 4 Aug 2023 13:14:12 +0200 Subject: [PATCH] feat: create LSP23MultiChainDeployment (#649) * feat: create LSP23MultiChainDeployment * refactor: improve LSP23 readability (#650) * refactor: improve LSP23 readability * tests: create helper to calculate deployed addresses * tests: add test for lsp23 * refactor: add suggested changes & surther simplify * refactor: remove event split, restore the initial events. * refactor: fix generation of salt * refactor: make salt generation functions virtual * chore: fix typo * refactor: move `emit` before external call to `IPostDeploymentModule` * chore: add suggested changes * docs: improve Natspec as suggetsed * docs: fix Natspec typo * chore: remove unrelated file --------- Co-authored-by: maxvia87 --------- Co-authored-by: b00ste.lyx <62855857+b00ste@users.noreply.github.com> Co-authored-by: Jean Cvllr <31145285+CJ42@users.noreply.github.com> --- .../IOwnerControlledContractDeployer.sol | 174 ++++++++ .../IPostDeploymentModule.sol | 10 + .../LSP23MultiChainDeployment/LSP23Errors.sol | 24 ++ .../OwnerControlledContractDeployer.sol | 388 ++++++++++++++++++ ...iversalProfileInitPostDeploymentModule.sol | 59 +++ .../LSP23MultiChainDeployment.behaviour.ts | 151 +++++++ tests/LSP23MultiChainDeployment/helpers.ts | 50 +++ 7 files changed, 856 insertions(+) create mode 100644 contracts/LSP23MultiChainDeployment/IOwnerControlledContractDeployer.sol create mode 100644 contracts/LSP23MultiChainDeployment/IPostDeploymentModule.sol create mode 100644 contracts/LSP23MultiChainDeployment/LSP23Errors.sol create mode 100644 contracts/LSP23MultiChainDeployment/OwnerControlledContractDeployer.sol create mode 100644 contracts/LSP23MultiChainDeployment/modules/UniversalProfileInitPostDeploymentModule.sol create mode 100644 tests/LSP23MultiChainDeployment/LSP23MultiChainDeployment.behaviour.ts create mode 100644 tests/LSP23MultiChainDeployment/helpers.ts diff --git a/contracts/LSP23MultiChainDeployment/IOwnerControlledContractDeployer.sol b/contracts/LSP23MultiChainDeployment/IOwnerControlledContractDeployer.sol new file mode 100644 index 000000000..54b00cc6e --- /dev/null +++ b/contracts/LSP23MultiChainDeployment/IOwnerControlledContractDeployer.sol @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +interface IOwnerControlledContractDeployer { + event DeployedContracts( + address indexed controlledContract, + address indexed ownerContract, + ControlledContractDeployment controlledContractDeployment, + OwnerContractDeployment ownerContractDeployment, + address postDeploymentModule, + bytes postDeploymentModuleCalldata + ); + + event DeployedERC1167Proxies( + address indexed controlledContract, + address indexed ownerContract, + ControlledContractDeploymentInit controlledContractDeploymentInit, + OwnerContractDeploymentInit ownerContractDeploymentInit, + address postDeploymentModule, + bytes postDeploymentModuleCalldata + ); + + /** + * @param salt A unique value used to ensure each created proxies are unique. (Can be used to deploy the contract at a desired address.) + * @param fundingAmount The value to be sent with the deployment transaction. + * @param creationBytecode The bytecode of the contract with the constructor params. + */ + struct ControlledContractDeployment { + bytes32 salt; + uint256 fundingAmount; + bytes creationBytecode; + } + + /** + * @param fundingAmount The value to be sent with the deployment transaction. + * @param creationBytecode The constructor + runtime bytecode (without the controlled contract's address as param) + * @param addControlledContractAddress If set to `true`, this will append the controlled contract's address + the `extraConstructorParams` to the `creationBytecode`. + * @param extraConstructorParams Params to be appended to the `creationBytecode` (after the controlled contract address) if `addControlledContractAddress` is set to `true`. + */ + struct OwnerContractDeployment { + uint256 fundingAmount; + bytes creationBytecode; + bool addControlledContractAddress; + bytes extraConstructorParams; + } + + /** + * @param salt A unique value used to ensure each created proxies are unique. (Can be used to deploy the contract at a desired address.) + * @param fundingAmount The value to be sent with the deployment transaction. + * @param implementationContract The address of the contract that will be used as a base contract for the proxy. + * @param initializationCalldata The calldata used to initialise the contract. (initialization should be similar to a constructor in a normal contract.) + */ + struct ControlledContractDeploymentInit { + bytes32 salt; + uint256 fundingAmount; + address implementationContract; + bytes initializationCalldata; + } + + /** + * @param fundingAmount The value to be sent with the deployment transaction. + * @param implementationContract The address of the contract that will be used as a base contract for the proxy. + * @param initializationCalldata The first part of the initialisation calldata, everything before the controlled contract address. + * @param addControlledContractAddress If set to `true`, this will append the controlled contract's address + the `extraInitializationParams` to the `initializationCalldata`. + * @param extraInitializationParams Params to be appended to the `initializationCalldata` (after the controlled contract address) if `addControlledContractAddress` is set to `true` + */ + struct OwnerContractDeploymentInit { + uint256 fundingAmount; + address implementationContract; + bytes initializationCalldata; + bool addControlledContractAddress; + bytes extraInitializationParams; + } + + /** + * @dev Deploys a contract and its owner contract. + * @notice Contracts deployed. Contract Address: `controlledContractAddress`. Owner Contract Address: `ownerContractAddress` + * + * @param controlledContractDeployment Contains the needed parameter to deploy a contract. (`salt`, `fundingAmount`, `creationBytecode`) + * @param ownerContractDeployment Contains the needed parameter to deploy the owner contract. (`fundingAmount`, `creationBytecode`, `addControlledContractAddress`, `extraConstructorParams`) + * @param postDeploymentModule The module to be executed after deployment + * @param postDeploymentModuleCalldata The data to be passed to the post deployment module + * + * @return controlledContractAddress The address of the deployed controlled contract. + * @return ownerContractAddress The address of the deployed owner contract. + */ + function deployContracts( + ControlledContractDeployment calldata controlledContractDeployment, + OwnerContractDeployment calldata ownerContractDeployment, + address postDeploymentModule, + bytes calldata postDeploymentModuleCalldata + ) + external + payable + returns ( + address controlledContractAddress, + address ownerContractAddress + ); + + /** + * @dev Deploys proxies of a contract and its owner contract + * @notice Contract proxies deployed. Contract Proxy Address: `controlledContractAddress`. Owner Contract Proxy Address: `ownerContractAddress` + * + * @param controlledContractDeploymentInit Contains the needed parameter to deploy a proxy contract. (`salt`, `fundingAmount`, `implementationContract`, `initializationCalldata`) + * @param ownerContractDeploymentInit Contains the needed parameter to deploy the owner proxy contract. (`fundingAmount`, `implementationContract`, `addControlledContractAddress`, `initializationCalldata`, `extraInitializationParams`) + * @param postDeploymentModule The module to be executed after deployment. + * @param postDeploymentModuleCalldata The data to be passed to the post deployment module. + * + * @return controlledContractAddress The address of the deployed controlled contract proxy + * @return ownerContractAddress The address of the deployed owner contract proxy + */ + function deployERC1167Proxies( + ControlledContractDeploymentInit + calldata controlledContractDeploymentInit, + OwnerContractDeploymentInit calldata ownerContractDeploymentInit, + address postDeploymentModule, + bytes calldata postDeploymentModuleCalldata + ) + external + payable + returns ( + address controlledContractAddress, + address ownerContractAddress + ); + + /** + * @dev Computes the addresses of the controlled and owner contracts to be created + * + * @param controlledContractDeployment Contains the needed parameter to deploy a contract. (`salt`, `fundingAmount`, `creationBytecode`) + * @param ownerContractDeployment Contains the needed parameter to deploy the owner contract. (`fundingAmount`, `creationBytecode`, `addControlledContractAddress`, `extraConstructorParams`) + * @param postDeploymentModule The module to be executed after deployment + * @param postDeploymentModuleCalldata The data to be passed to the post deployment module + * + * @return controlledContractAddress The address of the deployed controlled contract. + * @return ownerContractAddress The address of the deployed owner contract. + */ + function computeAddresses( + ControlledContractDeployment calldata controlledContractDeployment, + OwnerContractDeployment calldata ownerContractDeployment, + address postDeploymentModule, + bytes calldata postDeploymentModuleCalldata + ) + external + view + returns ( + address controlledContractAddress, + address ownerContractAddress + ); + + /** + * @dev Computes the addresses of the controlled and owner contract proxies to be created. + * + * @param controlledContractDeploymentInit Contains the needed parameter to deploy a proxy contract. (`salt`, `fundingAmount`, `implementationContract`, `initializationCalldata`) + * @param ownerContractDeploymentInit Contains the needed parameter to deploy the owner proxy contract. (`fundingAmount`, `implementationContract`, `addControlledContractAddress`, `initializationCalldata`, `extraInitializationParams`) + * @param postDeploymentModule The module to be executed after deployment. + * @param postDeploymentModuleCalldata The data to be passed to the post deployment module. + * + * @return controlledContractAddress The address of the deployed controlled contract proxy + * @return ownerContractAddress The address of the deployed owner contract proxy + */ + function computeERC1167Addresses( + ControlledContractDeploymentInit + calldata controlledContractDeploymentInit, + OwnerContractDeploymentInit calldata ownerContractDeploymentInit, + address postDeploymentModule, + bytes calldata postDeploymentModuleCalldata + ) + external + view + returns ( + address controlledContractAddress, + address ownerContractAddress + ); +} diff --git a/contracts/LSP23MultiChainDeployment/IPostDeploymentModule.sol b/contracts/LSP23MultiChainDeployment/IPostDeploymentModule.sol new file mode 100644 index 000000000..083051c57 --- /dev/null +++ b/contracts/LSP23MultiChainDeployment/IPostDeploymentModule.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +interface IPostDeploymentModule { + function executePostDeployment( + address ownerControlledContract, + address ownerContract, + bytes calldata calldataToPostDeploymentModule + ) external; +} diff --git a/contracts/LSP23MultiChainDeployment/LSP23Errors.sol b/contracts/LSP23MultiChainDeployment/LSP23Errors.sol new file mode 100644 index 000000000..90eb8a440 --- /dev/null +++ b/contracts/LSP23MultiChainDeployment/LSP23Errors.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +/** + * @dev Reverts when the `msg.value` sent is not equal to the sum of value used for the deployment of the contract & its owner contract. + * @notice Invalid value sent. + */ +error InvalidValueSum(); + +/** + * @dev Reverts when the deployment & intialisation of the contract has failed. + * @notice Failed to deploy & initialise the Controlled Contract Proxy. Error: `errorData`. + * + * @param errorData Potentially information about why the deployment & intialisation have failed. + */ +error ControlledContractProxyInitFailureError(bytes errorData); + +/** + * @dev Reverts when the deployment & intialisation of the owner contract has failed. + * @notice Failed to deploy & initialise the Owner Contract Proxy. Error: `errorData`. + * + * @param errorData Potentially information about why the deployment & intialisation have failed. + */ +error OwnerContractProxyInitFailureError(bytes errorData); diff --git a/contracts/LSP23MultiChainDeployment/OwnerControlledContractDeployer.sol b/contracts/LSP23MultiChainDeployment/OwnerControlledContractDeployer.sol new file mode 100644 index 000000000..85955a495 --- /dev/null +++ b/contracts/LSP23MultiChainDeployment/OwnerControlledContractDeployer.sol @@ -0,0 +1,388 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {IPostDeploymentModule} from "./IPostDeploymentModule.sol"; +import { + IOwnerControlledContractDeployer +} from "./IOwnerControlledContractDeployer.sol"; +import { + InvalidValueSum, + ControlledContractProxyInitFailureError, + OwnerContractProxyInitFailureError +} from "./LSP23Errors.sol"; + +contract OwnerControlledContractDeployer is IOwnerControlledContractDeployer { + /** + * @inheritdoc IOwnerControlledContractDeployer + */ + function deployContracts( + ControlledContractDeployment calldata controlledContractDeployment, + OwnerContractDeployment calldata ownerContractDeployment, + address postDeploymentModule, + bytes calldata postDeploymentModuleCalldata + ) + public + payable + returns ( + address controlledContractAddress, + address ownerContractAddress + ) + { + /* check that the msg.value is equal to the sum of the values of the controlled and owner contracts */ + if ( + msg.value != + controlledContractDeployment.fundingAmount + + ownerContractDeployment.fundingAmount + ) { + revert InvalidValueSum(); + } + + controlledContractAddress = _deployControlledContract( + controlledContractDeployment, + ownerContractDeployment, + postDeploymentModule, + postDeploymentModuleCalldata + ); + + ownerContractAddress = _deployOwnerContract( + ownerContractDeployment, + controlledContractAddress + ); + + emit DeployedContracts( + controlledContractAddress, + ownerContractAddress, + controlledContractDeployment, + ownerContractDeployment, + postDeploymentModule, + postDeploymentModuleCalldata + ); + + /* execute the post deployment module logic in the postDeploymentModule */ + IPostDeploymentModule(postDeploymentModule).executePostDeployment( + controlledContractAddress, + ownerContractAddress, + postDeploymentModuleCalldata + ); + } + + /** + * @inheritdoc IOwnerControlledContractDeployer + */ + function deployERC1167Proxies( + ControlledContractDeploymentInit + calldata controlledContractDeploymentInit, + OwnerContractDeploymentInit calldata ownerContractDeploymentInit, + address postDeploymentModule, + bytes calldata postDeploymentModuleCalldata + ) + public + payable + returns ( + address controlledContractAddress, + address ownerContractAddress + ) + { + /* check that the msg.value is equal to the sum of the values of the controlled and owner contracts */ + if ( + msg.value != + controlledContractDeploymentInit.fundingAmount + + ownerContractDeploymentInit.fundingAmount + ) { + revert InvalidValueSum(); + } + + /* deploy the controlled contract proxy with the controlledContractGeneratedSalt */ + controlledContractAddress = _deployAndInitializeControlledContractProxy( + controlledContractDeploymentInit, + ownerContractDeploymentInit, + postDeploymentModule, + postDeploymentModuleCalldata + ); + + /* deploy the owner contract proxy */ + ownerContractAddress = _deployAndInitializeOwnerContractProxy( + ownerContractDeploymentInit, + controlledContractAddress + ); + + emit DeployedERC1167Proxies( + controlledContractAddress, + ownerContractAddress, + controlledContractDeploymentInit, + ownerContractDeploymentInit, + postDeploymentModule, + postDeploymentModuleCalldata + ); + + /* execute the post deployment logic in the postDeploymentModule */ + IPostDeploymentModule(postDeploymentModule).executePostDeployment( + controlledContractAddress, + ownerContractAddress, + postDeploymentModuleCalldata + ); + } + + /** + * @inheritdoc IOwnerControlledContractDeployer + */ + function computeAddresses( + ControlledContractDeployment calldata controlledContractDeployment, + OwnerContractDeployment calldata ownerContractDeployment, + address postDeploymentModule, + bytes calldata postDeploymentModuleCalldata + ) + public + view + returns ( + address controlledContractAddress, + address ownerContractAddress + ) + { + bytes32 controlledContractGeneratedSalt = _generateControlledContractSalt( + controlledContractDeployment, + ownerContractDeployment, + postDeploymentModule, + postDeploymentModuleCalldata + ); + + controlledContractAddress = Create2.computeAddress( + controlledContractGeneratedSalt, + keccak256(controlledContractDeployment.creationBytecode) + ); + + bytes memory ownerContractByteCodeWithAllParams; + if (ownerContractDeployment.addControlledContractAddress) { + ownerContractByteCodeWithAllParams = abi.encodePacked( + ownerContractDeployment.creationBytecode, + abi.encode(controlledContractAddress), + ownerContractDeployment.extraConstructorParams + ); + } else { + ownerContractByteCodeWithAllParams = ownerContractDeployment + .creationBytecode; + } + + ownerContractAddress = Create2.computeAddress( + keccak256(abi.encodePacked(controlledContractAddress)), + keccak256(ownerContractByteCodeWithAllParams) + ); + } + + /** + * @inheritdoc IOwnerControlledContractDeployer + */ + function computeERC1167Addresses( + ControlledContractDeploymentInit + calldata controlledContractDeploymentInit, + OwnerContractDeploymentInit calldata ownerContractDeploymentInit, + address postDeploymentModule, + bytes calldata postDeploymentModuleCalldata + ) + public + view + returns ( + address controlledContractAddress, + address ownerContractAddress + ) + { + bytes32 controlledContractGeneratedSalt = _generateControlledProxyContractSalt( + controlledContractDeploymentInit, + ownerContractDeploymentInit, + postDeploymentModule, + postDeploymentModuleCalldata + ); + + controlledContractAddress = Clones.predictDeterministicAddress( + controlledContractDeploymentInit.implementationContract, + controlledContractGeneratedSalt + ); + + ownerContractAddress = Clones.predictDeterministicAddress( + ownerContractDeploymentInit.implementationContract, + keccak256(abi.encodePacked(controlledContractAddress)) + ); + } + + function _deployControlledContract( + ControlledContractDeployment calldata controlledContractDeployment, + OwnerContractDeployment calldata ownerContractDeployment, + address postDeploymentModule, + bytes calldata postDeploymentModuleCalldata + ) internal returns (address controlledContractAddress) { + bytes32 controlledContractGeneratedSalt = _generateControlledContractSalt( + controlledContractDeployment, + ownerContractDeployment, + postDeploymentModule, + postDeploymentModuleCalldata + ); + + /* deploy the controlled contract */ + controlledContractAddress = Create2.deploy( + controlledContractDeployment.fundingAmount, + controlledContractGeneratedSalt, + controlledContractDeployment.creationBytecode + ); + } + + function _deployOwnerContract( + OwnerContractDeployment calldata ownerContractDeployment, + address controlledContractAddress + ) internal returns (address ownerContractAddress) { + /** + * If `addControlledContractAddress` is `true`, the following will be appended to the constructor params: + * - The controlled contract address + * - `extraConstructorParams` + */ + bytes memory ownerContractByteCode = ownerContractDeployment + .creationBytecode; + + if (ownerContractDeployment.addControlledContractAddress) { + ownerContractByteCode = abi.encodePacked( + ownerContractByteCode, + abi.encode(controlledContractAddress), + ownerContractDeployment.extraConstructorParams + ); + } + + /* Here owner refers to the future owner of the controlled contract at the end of the transaction */ + ownerContractAddress = Create2.deploy( + ownerContractDeployment.fundingAmount, + keccak256(abi.encodePacked(controlledContractAddress)), + ownerContractByteCode + ); + } + + function _deployAndInitializeControlledContractProxy( + ControlledContractDeploymentInit + calldata controlledContractDeploymentInit, + OwnerContractDeploymentInit calldata ownerContractDeploymentInit, + address postDeploymentModule, + bytes calldata postDeploymentModuleCalldata + ) internal returns (address controlledContractAddress) { + bytes32 controlledContractGeneratedSalt = _generateControlledProxyContractSalt( + controlledContractDeploymentInit, + ownerContractDeploymentInit, + postDeploymentModule, + postDeploymentModuleCalldata + ); + + /* deploy the controlled contract proxy with the controlledContractGeneratedSalt */ + controlledContractAddress = Clones.cloneDeterministic( + controlledContractDeploymentInit.implementationContract, + controlledContractGeneratedSalt + ); + + /* initialize the controlled contract proxy */ + (bool success, bytes memory returnedData) = controlledContractAddress + .call{value: msg.value}( + controlledContractDeploymentInit.initializationCalldata + ); + if (!success) { + revert ControlledContractProxyInitFailureError(returnedData); + } + } + + function _deployAndInitializeOwnerContractProxy( + OwnerContractDeploymentInit calldata ownerContractDeploymentInit, + address controlledContractAddress + ) internal returns (address ownerContractAddress) { + /* deploy the controlled contract proxy with the controlledContractGeneratedSalt */ + ownerContractAddress = Clones.cloneDeterministic( + ownerContractDeploymentInit.implementationContract, + keccak256(abi.encodePacked(controlledContractAddress)) + ); + + /** + * If `addControlledContractAddress` is `true`, the following will be appended to the `initializationCalldata`: + * - The controlled contract address + * - `extraInitialisationBytes` + */ + bytes memory ownerInitializationBytes = ownerContractDeploymentInit + .initializationCalldata; + + if (ownerContractDeploymentInit.addControlledContractAddress) { + ownerInitializationBytes = abi.encodePacked( + ownerInitializationBytes, + abi.encode(controlledContractAddress), + ownerContractDeploymentInit.extraInitializationParams + ); + } + + /* initialize the controlled contract proxy */ + (bool success, bytes memory returnedData) = ownerContractAddress.call{ + value: msg.value + }(ownerInitializationBytes); + if (!success) { + revert OwnerContractProxyInitFailureError(returnedData); + } + } + + function _generateControlledContractSalt( + ControlledContractDeployment calldata controlledContractDeployment, + OwnerContractDeployment calldata ownerContractDeployment, + address postDeploymentModule, + bytes calldata postDeploymentModuleCalldata + ) internal pure virtual returns (bytes32 controlledContractGeneratedSalt) { + /* generate salt for the controlled contract + * the salt is generated by hashing the following elements: + * - the salt + * - the owner contract bytecode + * - the owner addControlledContractAddress boolean + * - the owner extraConstructorParams + * - the postDeploymentModule address + * - the postDeploymentModuleCalldata + * + */ + controlledContractGeneratedSalt = keccak256( + abi.encode( + controlledContractDeployment.salt, + controlledContractDeployment.creationBytecode, + ownerContractDeployment.creationBytecode, + ownerContractDeployment.addControlledContractAddress, + ownerContractDeployment.extraConstructorParams, + postDeploymentModule, + postDeploymentModuleCalldata + ) + ); + } + + function _generateControlledProxyContractSalt( + ControlledContractDeploymentInit + calldata controlledContractDeploymentInit, + OwnerContractDeploymentInit calldata ownerContractDeploymentInit, + address postDeploymentModule, + bytes calldata postDeploymentModuleCalldata + ) + internal + pure + virtual + returns (bytes32 controlledProxyContractGeneratedSalt) + { + /** + * Generate the salt for the controlled contract + * The salt is generated by hashing the following elements: + * - the salt + * - the owner implementation contract address + * - the owner contract addControlledContractAddress boolean + * - the owner contract initialization calldata + * - the owner contract extra initialization params (if any) + * - the postDeploymentModule address + * - the callda to the post deployment module + * + */ + controlledProxyContractGeneratedSalt = keccak256( + abi.encode( + controlledContractDeploymentInit.salt, + ownerContractDeploymentInit.implementationContract, + ownerContractDeploymentInit.initializationCalldata, + ownerContractDeploymentInit.addControlledContractAddress, + ownerContractDeploymentInit.extraInitializationParams, + postDeploymentModule, + postDeploymentModuleCalldata + ) + ); + } +} diff --git a/contracts/LSP23MultiChainDeployment/modules/UniversalProfileInitPostDeploymentModule.sol b/contracts/LSP23MultiChainDeployment/modules/UniversalProfileInitPostDeploymentModule.sol new file mode 100644 index 000000000..bb6d4408c --- /dev/null +++ b/contracts/LSP23MultiChainDeployment/modules/UniversalProfileInitPostDeploymentModule.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {UniversalProfileInit} from "../../UniversalProfileInit.sol"; + +contract UniversalProfileInitPostDeploymentModule is UniversalProfileInit { + constructor() { + _disableInitializers(); + } + + function setDataAndTransferOwnership( + bytes32[] memory dataKeys, + bytes[] memory dataValues, + address newOwner + ) public payable { + // check that the msg.sender is the owner + require( + msg.sender == owner(), + "UniversalProfileInitPostDeploymentModule: setDataAndTransferOwnership only allowed through delegate call" + ); + + // update the dataKeys and dataValues in the UniversalProfile contract + for (uint256 i = 0; i < dataKeys.length; ) { + _setData(dataKeys[i], dataValues[i]); + + unchecked { + ++i; + } + } + + // transfer the ownership of the UniversalProfile contract to the newOwner + _setOwner(newOwner); + } + + function executePostDeployment( + address universalProfile, + address keyManager, + bytes calldata setDataBatchBytes + ) public { + // retrieve the dataKeys and dataValues to setData from the initializationCalldata bytes + (bytes32[] memory dataKeys, bytes[] memory dataValues) = abi.decode( + setDataBatchBytes, + (bytes32[], bytes[]) + ); + + // call the execute function with delegate_call on the universalProfile contract to setData and transferOwnership + UniversalProfileInit(payable(universalProfile)).execute( + 4, + address(this), + 0, + abi.encodeWithSignature( + "setDataAndTransferOwnership(bytes32[],bytes[],address)", + dataKeys, + dataValues, + keyManager + ) + ); + } +} diff --git a/tests/LSP23MultiChainDeployment/LSP23MultiChainDeployment.behaviour.ts b/tests/LSP23MultiChainDeployment/LSP23MultiChainDeployment.behaviour.ts new file mode 100644 index 000000000..ae511e876 --- /dev/null +++ b/tests/LSP23MultiChainDeployment/LSP23MultiChainDeployment.behaviour.ts @@ -0,0 +1,151 @@ +import { ethers } from 'hardhat'; +import { expect } from 'chai'; +import { OwnerControlledContractDeployer } from '../../typechain-types'; +import { ERC725YDataKeys } from '../../constants.ts'; +import { calculateProxiesAddresses } from './helpers'; + +describe('UniversalProfileDeployer', function () { + it('should deploy proxies for Universal Profile and Key Manager', async function () { + const [allPermissionsSigner, universalReceiver, recoverySigner] = await ethers.getSigners(); + + const KeyManagerInitFactory = await ethers.getContractFactory('LSP6KeyManagerInit'); + const keyManagerInit = await KeyManagerInitFactory.deploy(); + + const UniversalProfileInitFactory = await ethers.getContractFactory('UniversalProfileInit'); + const universalProfileInit = await UniversalProfileInitFactory.deploy(); + + const OwnerControlledContractDeployerFactory = await ethers.getContractFactory( + 'OwnerControlledContractDeployer', + ); + + const ownerControlledContractDeployer = await OwnerControlledContractDeployerFactory.deploy(); + + const UPDelegatorPostDeploymentManagerFactory = await ethers.getContractFactory( + 'UniversalProfileInitPostDeploymentModule', + ); + + const upPostDeploymentModule = await UPDelegatorPostDeploymentManagerFactory.deploy(); + + const salt = ethers.utils.randomBytes(32); + + const ownerControlledDeploymentInit: OwnerControlledContractDeployer.ControlledContractDeploymentInitStruct = + { + salt, + value: 0, + implementationContract: universalProfileInit.address, + initializationCalldata: universalProfileInit.interface.encodeFunctionData('initialize', [ + upPostDeploymentModule.address, + ]), + }; + + const ownerDeploymentInit: OwnerControlledContractDeployer.OwnerContractDeploymentInitStruct = { + value: 0, + implementationContract: keyManagerInit.address, + addControlledContractAddress: true, + initializationCalldata: '0xc4d66de8', + extraInitializationParams: '0x', + }; + + const allPermissionsSignerPermissionsKey = + '0x4b80742de2bf82acb3630000' + allPermissionsSigner.address.slice(2); + + const universalReceiverPermissionsKey = + '0x4b80742de2bf82acb3630000' + universalReceiver.address.slice(2); + + const recoveryAddressPermissionsKey = + '0x4b80742de2bf82acb3630000' + recoverySigner.address.slice(2); + + const allPermissionsSignerPermissionsValue = + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; + + const create16BytesUint = (value: number) => { + return ethers.utils.hexZeroPad(ethers.utils.hexlify(value), 16).slice(2); + }; + + const types = ['bytes32[]', 'bytes[]']; + + const encodedBytes = ethers.utils.defaultAbiCoder.encode(types, [ + [ + ERC725YDataKeys.LSP3.LSP3Profile, // LSP3Metadata + ERC725YDataKeys.LSP1.LSP1UniversalReceiverDelegate, // URD Address + universalReceiverPermissionsKey, // URD Permissions + recoveryAddressPermissionsKey, // Recovery Address permissions + allPermissionsSignerPermissionsKey, // Signers permissions + ERC725YDataKeys.LSP6['AddressPermissions[]'].length, // Number of address with permissions + ERC725YDataKeys.LSP6['AddressPermissions[]'].index + create16BytesUint(0), // Index of the first address + ERC725YDataKeys.LSP6['AddressPermissions[]'].index + create16BytesUint(1), // Index of the second address + ERC725YDataKeys.LSP6['AddressPermissions[]'].index + create16BytesUint(2), // Index of the third address + ], + [ + ethers.utils.randomBytes(32), // LSP3Metadata + universalReceiver.address, // URD Address + allPermissionsSignerPermissionsValue, // URD Permissions + allPermissionsSignerPermissionsValue, // Recovery Address permissions + allPermissionsSignerPermissionsValue, // Signers permissions + ethers.utils.defaultAbiCoder.encode(['uint256'], [3]), // Address Permissions array length + universalReceiver.address, + recoverySigner.address, + allPermissionsSigner.address, + ], + ]); + + // get the address of the UP and the KeyManager contracts + const [upAddress, keyManagerAddress] = + await ownerControlledContractDeployer.callStatic.deployERC1167Proxies( + ownerControlledDeploymentInit, + ownerDeploymentInit, + upPostDeploymentModule.address, + encodedBytes, + ); + + await ownerControlledContractDeployer.deployERC1167Proxies( + ownerControlledDeploymentInit, + ownerDeploymentInit, + upPostDeploymentModule.address, + encodedBytes, + ); + + const upProxy = UniversalProfileInitFactory.attach(upAddress); + const keyManagerProxy = KeyManagerInitFactory.attach(keyManagerAddress); + + const upProxyOwner = await upProxy.owner(); + const keyManagerProxyOwner = await keyManagerProxy.target(); + + const [expectedUpProxyAddress, expectedKeyManagerProxyAddress] = + await ownerControlledContractDeployer.computeERC1167Addresses( + ownerControlledDeploymentInit.salt, + ownerControlledDeploymentInit.implementationContract, + ownerDeploymentInit.implementationContract, + ownerDeploymentInit.initializationCalldata, + ownerDeploymentInit.addControlledContractAddress, + ownerDeploymentInit.extraInitializationParams, + upPostDeploymentModule.address, + encodedBytes, + ); + + const [calculatedUpProxyAddress, calculatedKMProxyAddress] = await calculateProxiesAddresses( + ownerControlledDeploymentInit.salt, + ownerControlledDeploymentInit.implementationContract, + ownerDeploymentInit.implementationContract, + ownerDeploymentInit.initializationCalldata, + ownerDeploymentInit.addControlledContractAddress, + ownerDeploymentInit.extraInitializationParams, + upPostDeploymentModule.address, + encodedBytes, + ownerControlledContractDeployer.address, + ); + + expect(upAddress).to.equal(expectedUpProxyAddress); + expect(upAddress).to.equal(expectedUpProxyAddress); + expect(upAddress).to.equal(calculatedUpProxyAddress); + + expect(keyManagerAddress).to.equal(expectedKeyManagerProxyAddress); + expect(keyManagerAddress).to.equal(expectedKeyManagerProxyAddress); + expect(keyManagerAddress).to.equal(calculatedKMProxyAddress); + + expect(upProxyOwner).to.equal(keyManagerProxy.address); + expect(upProxyOwner).to.equal(keyManagerProxy.address); + expect(keyManagerProxyOwner).to.equal(upProxy.address); + expect(keyManagerProxyOwner).to.equal(upProxy.address); + }); +}); diff --git a/tests/LSP23MultiChainDeployment/helpers.ts b/tests/LSP23MultiChainDeployment/helpers.ts new file mode 100644 index 000000000..847d2a0b5 --- /dev/null +++ b/tests/LSP23MultiChainDeployment/helpers.ts @@ -0,0 +1,50 @@ +import { ethers } from 'ethers'; + +export async function calculateProxiesAddresses( + salt: ethers.utils.BytesLike, + ownerControlledImplementationContractAddress: string, + ownerImplementationContractAddress: string, + ownerInitializationCalldata: ethers.utils.BytesLike, + ownerAddControlledContractAddress: boolean, + ownerExtraInitializationParams: ethers.utils.BytesLike, + upPostDeploymentModuleAddress: string, + postDeploymentCalldata: ethers.utils.BytesLike, + ownerControlledContractDeployerAddress: string, +): Promise<[string, string]> { + const generatedSalt = ethers.utils.keccak256( + ethers.utils.defaultAbiCoder.encode( + ['bytes32', 'address', 'bytes', 'bool', 'bytes', 'address', 'bytes'], + [ + salt, + ownerImplementationContractAddress, + ownerInitializationCalldata, + ownerAddControlledContractAddress, + ownerExtraInitializationParams, + upPostDeploymentModuleAddress, + postDeploymentCalldata, + ], + ), + ); + + const expectedOwnerControlledAddress = ethers.utils.getCreate2Address( + ownerControlledContractDeployerAddress, + generatedSalt, + ethers.utils.keccak256( + '0x3d602d80600a3d3981f3363d3d373d3d3d363d73' + + ownerControlledImplementationContractAddress.slice(2) + + '5af43d82803e903d91602b57fd5bf3', + ), + ); + + const expectedOwnerAddress = ethers.utils.getCreate2Address( + ownerControlledContractDeployerAddress, + ethers.utils.keccak256(expectedOwnerControlledAddress), + ethers.utils.keccak256( + '0x3d602d80600a3d3981f3363d3d373d3d3d363d73' + + ownerImplementationContractAddress.slice(2) + + '5af43d82803e903d91602b57fd5bf3', + ), + ); + + return [expectedOwnerControlledAddress, expectedOwnerAddress]; +}