From 5965e59df051ca3c5453ba9089eb5a9e3d62ac22 Mon Sep 17 00:00:00 2001 From: Ameesha Agrawal Date: Thu, 17 Oct 2024 13:38:19 +0400 Subject: [PATCH] feat: deploy proxy factory and socket safe --- .../utils/{ => multisig}/MultiSigWrapper.sol | 6 +- contracts/utils/{ => multisig}/SafeL2.sol | 0 .../proxies/IProxyCreationCallback.sol | 23 +++ .../utils/multisig/proxies/SafeProxy.sol | 63 +++++++ .../multisig/proxies/SafeProxyFactory.sol | 163 ++++++++++++++++++ scripts/deploy/scripts/deploySocket.ts | 73 +++++++- test/MultiSigWrapper.t.sol | 2 +- 7 files changed, 323 insertions(+), 7 deletions(-) rename contracts/utils/{ => multisig}/MultiSigWrapper.sol (97%) rename contracts/utils/{ => multisig}/SafeL2.sol (100%) create mode 100644 contracts/utils/multisig/proxies/IProxyCreationCallback.sol create mode 100644 contracts/utils/multisig/proxies/SafeProxy.sol create mode 100644 contracts/utils/multisig/proxies/SafeProxyFactory.sol diff --git a/contracts/utils/MultiSigWrapper.sol b/contracts/utils/multisig/MultiSigWrapper.sol similarity index 97% rename from contracts/utils/MultiSigWrapper.sol rename to contracts/utils/multisig/MultiSigWrapper.sol index 5288ed67..0edfc4a6 100644 --- a/contracts/utils/MultiSigWrapper.sol +++ b/contracts/utils/multisig/MultiSigWrapper.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.19; -import "../libraries/RescueFundsLib.sol"; +import "../../libraries/RescueFundsLib.sol"; -import "../utils/AccessControl.sol"; -import {RESCUE_ROLE} from "../utils/AccessRoles.sol"; +import "../../utils/AccessControl.sol"; +import {RESCUE_ROLE} from "../../utils/AccessRoles.sol"; import "solady/utils/LibSort.sol"; import {ISafe, Enum} from "./SafeL2.sol"; diff --git a/contracts/utils/SafeL2.sol b/contracts/utils/multisig/SafeL2.sol similarity index 100% rename from contracts/utils/SafeL2.sol rename to contracts/utils/multisig/SafeL2.sol diff --git a/contracts/utils/multisig/proxies/IProxyCreationCallback.sol b/contracts/utils/multisig/proxies/IProxyCreationCallback.sol new file mode 100644 index 00000000..6fe1abec --- /dev/null +++ b/contracts/utils/multisig/proxies/IProxyCreationCallback.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.7.0 <0.9.0; +import "./SafeProxy.sol"; + +/** + * @title IProxyCreationCallback + * @dev An interface for a contract that implements a callback function to be executed after the creation of a proxy instance. + */ +interface IProxyCreationCallback { + /** + * @dev Function to be called after the creation of a SafeProxy instance. + * @param proxy The newly created SafeProxy instance. + * @param _singleton The address of the singleton contract used to create the proxy. + * @param initializer The initializer function call data. + * @param saltNonce The nonce used to generate the salt for the proxy deployment. + */ + function proxyCreated( + SafeProxy proxy, + address _singleton, + bytes calldata initializer, + uint256 saltNonce + ) external; +} diff --git a/contracts/utils/multisig/proxies/SafeProxy.sol b/contracts/utils/multisig/proxies/SafeProxy.sol new file mode 100644 index 00000000..24768569 --- /dev/null +++ b/contracts/utils/multisig/proxies/SafeProxy.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.7.0 <0.9.0; + +/** + * @title IProxy - Helper interface to access the singleton address of the Proxy on-chain. + * @author Richard Meissner - @rmeissner + */ +interface IProxy { + function masterCopy() external view returns (address); +} + +/** + * @title SafeProxy - Generic proxy contract allows to execute all transactions applying the code of a master contract. + * @author Stefan George - + * @author Richard Meissner - + */ +contract SafeProxy { + // Singleton always needs to be first declared variable, to ensure that it is at the same location in the contracts to which calls are delegated. + // To reduce deployment costs this variable is internal and needs to be retrieved via `getStorageAt` + address internal singleton; + + /** + * @notice Constructor function sets address of singleton contract. + * @param _singleton Singleton address. + */ + constructor(address _singleton) { + require(_singleton != address(0), "Invalid singleton address provided"); + singleton = _singleton; + } + + /// @dev Fallback function forwards all transactions and returns all received return data. + fallback() external payable { + // solhint-disable-next-line no-inline-assembly + assembly { + let _singleton := and( + sload(0), + 0xffffffffffffffffffffffffffffffffffffffff + ) + // 0xa619486e == keccak("masterCopy()"). The value is right padded to 32-bytes with 0s + if eq( + calldataload(0), + 0xa619486e00000000000000000000000000000000000000000000000000000000 + ) { + mstore(0, _singleton) + return(0, 0x20) + } + calldatacopy(0, 0, calldatasize()) + let success := delegatecall( + gas(), + _singleton, + 0, + calldatasize(), + 0, + 0 + ) + returndatacopy(0, 0, returndatasize()) + if eq(success, 0) { + revert(0, returndatasize()) + } + return(0, returndatasize()) + } + } +} diff --git a/contracts/utils/multisig/proxies/SafeProxyFactory.sol b/contracts/utils/multisig/proxies/SafeProxyFactory.sol new file mode 100644 index 00000000..1cbdff34 --- /dev/null +++ b/contracts/utils/multisig/proxies/SafeProxyFactory.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.7.0 <0.9.0; + +import "./SafeProxy.sol"; +import "./IProxyCreationCallback.sol"; + +/** + * @title Proxy Factory - Allows to create a new proxy contract and execute a message call to the new proxy within one transaction. + * @author Stefan George - @Georgi87 + */ +contract SafeProxyFactory { + event ProxyCreation(SafeProxy indexed proxy, address singleton); + + /// @dev Allows to retrieve the creation code used for the Proxy deployment. With this it is easily possible to calculate predicted address. + function proxyCreationCode() public pure returns (bytes memory) { + return type(SafeProxy).creationCode; + } + + /** + * @notice Internal method to create a new proxy contract using CREATE2. Optionally executes an initializer call to a new proxy. + * @param _singleton Address of singleton contract. Must be deployed at the time of execution. + * @param initializer (Optional) Payload for a message call to be sent to a new proxy contract. + * @param salt Create2 salt to use for calculating the address of the new proxy contract. + * @return proxy Address of the new proxy contract. + */ + function deployProxy( + address _singleton, + bytes memory initializer, + bytes32 salt + ) internal returns (SafeProxy proxy) { + require(isContract(_singleton), "Singleton contract not deployed"); + + bytes memory deploymentData = abi.encodePacked( + type(SafeProxy).creationCode, + uint256(uint160(_singleton)) + ); + // solhint-disable-next-line no-inline-assembly + assembly { + proxy := create2( + 0x0, + add(0x20, deploymentData), + mload(deploymentData), + salt + ) + } + require(address(proxy) != address(0), "Create2 call failed"); + + if (initializer.length > 0) { + // solhint-disable-next-line no-inline-assembly + assembly { + if eq( + call( + gas(), + proxy, + 0, + add(initializer, 0x20), + mload(initializer), + 0, + 0 + ), + 0 + ) { + revert(0, 0) + } + } + } + } + + /** + * @notice Deploys a new proxy with `_singleton` singleton and `saltNonce` salt. Optionally executes an initializer call to a new proxy. + * @param _singleton Address of singleton contract. Must be deployed at the time of execution. + * @param initializer Payload for a message call to be sent to a new proxy contract. + * @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract. + */ + function createProxyWithNonce( + address _singleton, + bytes memory initializer, + uint256 saltNonce + ) public returns (SafeProxy proxy) { + // If the initializer changes the proxy address should change too. Hashing the initializer data is cheaper than just concatinating it + bytes32 salt = keccak256( + abi.encodePacked(keccak256(initializer), saltNonce) + ); + proxy = deployProxy(_singleton, initializer, salt); + emit ProxyCreation(proxy, _singleton); + } + + /** + * @notice Deploys a new chain-specific proxy with `_singleton` singleton and `saltNonce` salt. Optionally executes an initializer call to a new proxy. + * @dev Allows to create a new proxy contract that should exist only on 1 network (e.g. specific governance or admin accounts) + * by including the chain id in the create2 salt. Such proxies cannot be created on other networks by replaying the transaction. + * @param _singleton Address of singleton contract. Must be deployed at the time of execution. + * @param initializer Payload for a message call to be sent to a new proxy contract. + * @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract. + */ + function createChainSpecificProxyWithNonce( + address _singleton, + bytes memory initializer, + uint256 saltNonce + ) public returns (SafeProxy proxy) { + // If the initializer changes the proxy address should change too. Hashing the initializer data is cheaper than just concatinating it + bytes32 salt = keccak256( + abi.encodePacked(keccak256(initializer), saltNonce, getChainId()) + ); + proxy = deployProxy(_singleton, initializer, salt); + emit ProxyCreation(proxy, _singleton); + } + + /** + * @notice Deploy a new proxy with `_singleton` singleton and `saltNonce` salt. + * Optionally executes an initializer call to a new proxy and calls a specified callback address `callback`. + * @param _singleton Address of singleton contract. Must be deployed at the time of execution. + * @param initializer Payload for a message call to be sent to a new proxy contract. + * @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract. + * @param callback Callback that will be invoked after the new proxy contract has been successfully deployed and initialized. + */ + function createProxyWithCallback( + address _singleton, + bytes memory initializer, + uint256 saltNonce, + IProxyCreationCallback callback + ) public returns (SafeProxy proxy) { + uint256 saltNonceWithCallback = uint256( + keccak256(abi.encodePacked(saltNonce, callback)) + ); + proxy = createProxyWithNonce( + _singleton, + initializer, + saltNonceWithCallback + ); + if (address(callback) != address(0)) + callback.proxyCreated(proxy, _singleton, initializer, saltNonce); + } + + /** + * @notice Returns true if `account` is a contract. + * @dev This function will return false if invoked during the constructor of a contract, + * as the code is not actually created until after the constructor finishes. + * @param account The address being queried + * @return True if `account` is a contract + */ + function isContract(address account) internal view returns (bool) { + uint256 size; + // solhint-disable-next-line no-inline-assembly + assembly { + size := extcodesize(account) + } + return size > 0; + } + + /** + * @notice Returns the ID of the chain the contract is currently deployed on. + * @return The ID of the current chain as a uint256. + */ + function getChainId() public view returns (uint256) { + uint256 id; + // solhint-disable-next-line no-inline-assembly + assembly { + id := chainid() + } + return id; + } +} diff --git a/scripts/deploy/scripts/deploySocket.ts b/scripts/deploy/scripts/deploySocket.ts index 92d732d0..19b499c7 100644 --- a/scripts/deploy/scripts/deploySocket.ts +++ b/scripts/deploy/scripts/deploySocket.ts @@ -1,4 +1,4 @@ -import { Contract, constants } from "ethers"; +import { Contract, Transaction, constants, utils } from "ethers"; import { DeployParams, getInstance, @@ -46,15 +46,30 @@ export const deploySocket = async ( // safe wrapper deployment const safe: Contract = await getOrDeploy( "SafeL2", - "contracts/utils/SafeL2.sol", + "contracts/utils/multisig/SafeL2.sol", [], deployUtils ); deployUtils.addresses["SafeL2"] = safe.address; + const safeProxyFactory: Contract = await getOrDeploy( + "SafeProxyFactory", + "contracts/utils/multisig/proxies/SafeProxyFactory.sol", + [], + deployUtils + ); + deployUtils.addresses["SafeProxyFactory"] = safeProxyFactory.address; + + const proxyAddress = await createSocketSafe( + safeProxyFactory, + safe.address, + [socketOwner] + ); + deployUtils.addresses["SocketSafeProxy"] = proxyAddress; + const multisigWrapper: Contract = await getOrDeploy( "MultiSigWrapper", - "contracts/utils/MultiSigWrapper.sol", + "contracts/utils/multisig/MultiSigWrapper.sol", [socketOwner, deployUtils.addresses["SafeL2"]], deployUtils ); @@ -230,3 +245,55 @@ export const deploySocket = async ( deployedAddresses: deployUtils.addresses, }; }; + +// Assuming you are in a contract context +async function createSocketSafe(safeProxyFactory, safeAddress, owners) { + const addressZero = "0x0000000000000000000000000000000000000000"; + const functionSignature = + "setup(address[],uint256,address,bytes,address,address,uint256,address)"; + const functionSelector = utils.id(functionSignature).slice(0, 10); + + const encodedParameters = utils.defaultAbiCoder.encode( + [ + "address[]", + "uint256", + "address", + "bytes", + "address", + "address", + "uint256", + "address", + ], + [ + owners, + owners.length, + addressZero, + "0x", + addressZero, + addressZero, + 0, + addressZero, + ] + ); + const encodedData = functionSelector + encodedParameters.slice(2); // Remove '0x' from encodedParameters + + const tx = await safeProxyFactory.createChainSpecificProxyWithNonce( + safeAddress, + encodedData, + 0 + ); + const receipt = await tx.wait(); + + const safeSetupEvent = receipt.events?.find( + (event: Event) => event.event === "ProxyCreation" + ); + + if (safeSetupEvent) { + const proxy = safeSetupEvent.args.proxy; + console.log(`Safe proxy: ${proxy}`); + } else { + console.log( + "Safe proxy created event not found in the transaction receipt" + ); + } +} diff --git a/test/MultiSigWrapper.t.sol b/test/MultiSigWrapper.t.sol index 4534bb36..cf376b37 100644 --- a/test/MultiSigWrapper.t.sol +++ b/test/MultiSigWrapper.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.19; import {Test} from "forge-std/Test.sol"; -import "../contracts/utils/MultiSigWrapper.sol"; +import "../contracts/utils/multisig/MultiSigWrapper.sol"; import {MockSafe} from "../contracts/mocks/MockSafe.sol"; contract MultiSigWrapperTestHelper is MultiSigWrapper {