diff --git a/.changeset/weak-crews-flash.md b/.changeset/weak-crews-flash.md new file mode 100644 index 000000000..cfb8813ed --- /dev/null +++ b/.changeset/weak-crews-flash.md @@ -0,0 +1,5 @@ +--- +'@sphinx-labs/plugins': patch +--- + +Set FOUNDRY_SENDER and ETH_FROM during transaction collection diff --git a/docs/cli-existing-project.md b/docs/cli-existing-project.md index 9d4a79bb9..7ffdf5a8e 100644 --- a/docs/cli-existing-project.md +++ b/docs/cli-existing-project.md @@ -206,7 +206,6 @@ Update your `foundry.toml` file to include a few settings required by Sphinx. We build_info = true extra_output = ['storageLayout'] fs_permissions = [{ access = "read-write", path = "./"}] -always_use_create_2_factory = true ``` ## 10. Run tests diff --git a/docs/writing-scripts.md b/docs/writing-scripts.md index 9ba6c0b3e..8b4bea56b 100644 --- a/docs/writing-scripts.md +++ b/docs/writing-scripts.md @@ -9,6 +9,7 @@ This guide covers the essential information for writing deployment scripts with - [Configuration options](#configuration-options) - [Deployment failures](#deployment-failures) - [Silent transaction failures](#silent-transaction-failures) +- [Script Environment](#script-environment) ## Your Gnosis Safe @@ -67,3 +68,8 @@ As any Solidity developer knows, smart contracts generally revert upon failure. With Sphinx, a deployment will only fail if a transaction reverts. This means that if a transaction returns a success condition instead of reverting, the deployment will _not_ fail, and the executor will continue to submit transactions for the deployment. If you want to avoid this behavior, we recommend designing your smart contracts so that they revert upon failure. For example, OpenZeppelin prevents the silent failure described above with their [`SafeERC20`](https://docs.openzeppelin.com/contracts/5.x/api/token/erc20#SafeERC20) contract, which reverts if an operation fails. + +## Script Environment +When deploying with Sphinx via either the `propose` or `deploy` CLI commands, Sphinx will invoke your Forge script on your behalf. When the script runs, we configure a few options and environment variables by default. + +Sphinx sets the `FOUNDRY_SENDER` and `ETH_FROM` environment variables to the address of your Gnosis Safe. Sphinx also uses the `--always_use_create_2_factory` CLI flag, which causes CREATE2 deployments to occur via the default CREATE2 factory. diff --git a/packages/contracts/contracts/foundry/Sphinx.sol b/packages/contracts/contracts/foundry/Sphinx.sol index b1cd6f0cf..ee1219758 100644 --- a/packages/contracts/contracts/foundry/Sphinx.sol +++ b/packages/contracts/contracts/foundry/Sphinx.sol @@ -202,8 +202,13 @@ abstract contract Sphinx { // Deploy the Gnosis Safe if it's not already deployed. This is necessary because we're // going to call the Gnosis Safe to estimate the gas. + // This also also ensures that the safe's nonce is incremented as a contract instead of an EOA. if (address(safe).code.length == 0) { - _sphinxDeployModuleAndGnosisSafe(); + sphinxUtils.deployModuleAndGnosisSafe( + sphinxConfig.owners, + sphinxConfig.threshold, + safe + ); } // Take a snapshot of the current state. We'll revert to the snapshot after we run the diff --git a/packages/contracts/contracts/foundry/SphinxUtils.sol b/packages/contracts/contracts/foundry/SphinxUtils.sol index ab36382b7..cb4f8ca26 100644 --- a/packages/contracts/contracts/foundry/SphinxUtils.sol +++ b/packages/contracts/contracts/foundry/SphinxUtils.sol @@ -704,13 +704,17 @@ contract SphinxUtils is SphinxConstants, StdUtils { ); } + function getGnosisSafeProxyInitCode() public pure returns (bytes memory) { + return + hex"608060405234801561001057600080fd5b506040516101e63803806101e68339818101604052602081101561003357600080fd5b8101908080519060200190929190505050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1614156100ca576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260228152602001806101c46022913960400191505060405180910390fd5b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505060ab806101196000396000f3fe608060405273ffffffffffffffffffffffffffffffffffffffff600054167fa619486e0000000000000000000000000000000000000000000000000000000060003514156050578060005260206000f35b3660008037600080366000845af43d6000803e60008114156070573d6000fd5b3d6000f3fea2646970667358221220d1429297349653a4918076d650332de1a1068c5f3e07c5c82360c277770b955264736f6c63430007060033496e76616c69642073696e676c65746f6e20616464726573732070726f7669646564"; + } + function getGnosisSafeProxyAddress(SphinxConfig memory _config) public pure returns (address) { bytes memory safeInitializerData = getGnosisSafeInitializerData(_config); bytes32 salt = keccak256( abi.encodePacked(keccak256(safeInitializerData), _config.saltNonce) ); - bytes - memory safeProxyInitCode = hex"608060405234801561001057600080fd5b506040516101e63803806101e68339818101604052602081101561003357600080fd5b8101908080519060200190929190505050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1614156100ca576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260228152602001806101c46022913960400191505060405180910390fd5b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505060ab806101196000396000f3fe608060405273ffffffffffffffffffffffffffffffffffffffff600054167fa619486e0000000000000000000000000000000000000000000000000000000060003514156050578060005260206000f35b3660008037600080366000845af43d6000803e60008114156070573d6000fd5b3d6000f3fea2646970667358221220d1429297349653a4918076d650332de1a1068c5f3e07c5c82360c277770b955264736f6c63430007060033496e76616c69642073696e676c65746f6e20616464726573732070726f7669646564"; + bytes memory safeProxyInitCode = getGnosisSafeProxyInitCode(); bytes memory deploymentData = abi.encodePacked( safeProxyInitCode, uint256(uint160(safeSingletonAddress)) @@ -1159,4 +1163,116 @@ contract SphinxUtils is SphinxConstants, StdUtils { revert("AccountAccess kind is incorrect. Should never happen."); } } + + function getModuleInitializerMultiSendData() private pure returns (bytes memory) { + ISphinxModuleProxyFactory moduleProxyFactory = ISphinxModuleProxyFactory( + sphinxModuleProxyFactoryAddress + ); + + // Encode the data that will deploy the Sphinx Module. + bytes memory encodedDeployModuleCall = abi.encodeWithSelector( + moduleProxyFactory.deploySphinxModuleProxyFromSafe.selector, + // Use the zero-hash as the salt. + bytes32(0) + ); + // Encode the data in a format that can be executed using `MultiSend`. + bytes memory deployModuleMultiSendData = abi.encodePacked( + // We use `Call` so that the Gnosis Safe calls the `SphinxModuleProxyFactory` to deploy + // the Sphinx Module. This makes it easier for off-chain tooling to calculate the + // deployed Sphinx Module address because the `SphinxModuleProxyFactory`'s address is a + // global constant. + uint8(IEnum.GnosisSafeOperation.Call), + moduleProxyFactory, + uint256(0), + encodedDeployModuleCall.length, + encodedDeployModuleCall + ); + + // Encode the data that will enable the Sphinx Module in the Gnosis Safe. + bytes memory encodedEnableModuleCall = abi.encodeWithSelector( + moduleProxyFactory.enableSphinxModuleProxyFromSafe.selector, + // Use the zero-hash as the salt. + bytes32(0) + ); + // Encode the data in a format that can be executed using `MultiSend`. + bytes memory enableModuleMultiSendData = abi.encodePacked( + // We can only enable the module by delegatecalling the `SphinxModuleProxyFactory` + // from the Gnosis Safe. + uint8(IEnum.GnosisSafeOperation.DelegateCall), + moduleProxyFactory, + uint256(0), + encodedEnableModuleCall.length, + encodedEnableModuleCall + ); + + // Encode the entire `MultiSend` data. + bytes memory multiSendData = abi.encodeWithSelector( + IMultiSend.multiSend.selector, + abi.encodePacked(deployModuleMultiSendData, enableModuleMultiSendData) + ); + + return multiSendData; + } + + /** + * @notice Deploys a Gnosis Safe, deploys a Sphinx Module, + * and enables the Sphinx Module in the Gnosis Safe + */ + function deployModuleAndGnosisSafe( + address[] memory _owners, + uint256 _threshold, + address _safeAddress + ) external { + // Get the encoded data that'll be sent to the `MultiSend` contract to deploy and enable the + // Sphinx Module in the Gnosis Safe. + bytes memory multiSendData = getModuleInitializerMultiSendData(); + + // Deploy the Gnosis Safe Proxy to its expected address. We use cheatcodes to deploy the + // Gnosis Safe instead of the standard deployment process to avoid a bug in Foundry. + // Specifically, Foundry throws an error if we attempt to deploy a contract at the same + // address as the `FOUNDRY_SENDER`. We must set the Gnosis Safe as the `FOUNDRY_SENDER` so + // that deployed linked library addresses match the production environment. If we deploy the + // Gnosis Safe without using cheatcodes, Foundry would throw an error here. + deployContractTo( + getGnosisSafeProxyInitCode(), + abi.encode(safeSingletonAddress), + _safeAddress + ); + + // Initialize the Gnosis Safe proxy. + IGnosisSafe(_safeAddress).setup( + sortAddresses(_owners), + _threshold, + multiSendAddress, + multiSendData, + // This is the default fallback handler used by Gnosis Safe during their + // standard deployments. + compatibilityFallbackHandlerAddress, + // The following fields are for specifying an optional payment as part of the + // deployment. We don't use them. + address(0), + 0, + payable(address(0)) + ); + } + + /** + * @notice Deploy a contract to the given address. Slightly modified from + * `StdCheats.sol:deployCodeTo`. + */ + function deployContractTo( + bytes memory _initCode, + bytes memory _abiEncodedConstructorArgs, + address _where + ) public { + require(_where.code.length == 0, "SphinxUtils: contract already exists"); + vm.etch(_where, abi.encodePacked(_initCode, _abiEncodedConstructorArgs)); + (bool success, bytes memory runtimeBytecode) = _where.call(""); + require(success, "SphinxUtils: failed to create runtime bytecode"); + vm.etch(_where, runtimeBytecode); + if (vm.getNonce(_where) == 0) { + // Set the nonce to be 1, which is the initial nonce for contracts. + vm.setNonce(_where, 1); + } + } } diff --git a/packages/plugins/src/cli/deploy.ts b/packages/plugins/src/cli/deploy.ts index 725c2b391..db31d7dd0 100644 --- a/packages/plugins/src/cli/deploy.ts +++ b/packages/plugins/src/cli/deploy.ts @@ -52,6 +52,7 @@ import { assertValidVersions, compile, getInitCodeWithArgsArray, + getSphinxConfigFromScript, readInterface, writeSystemContracts, } from '../foundry/utils' @@ -198,6 +199,7 @@ export const deploy = async ( systemContractsFilePath, '--rpc-url', forkUrl, + '--always-use-create-2-factory', ] if ( isLegacyTransactionsRequiredForNetwork( @@ -210,6 +212,13 @@ export const deploy = async ( forgeScriptCollectArgs.push('--target-contract', targetContract) } + const { safeAddress } = await getSphinxConfigFromScript( + scriptPath, + sphinxPluginTypesInterface, + targetContract, + spinner + ) + // Collect the transactions. const spawnOutput = await spawnAsync('forge', forgeScriptCollectArgs, { // Set the block gas limit to the max amount allowed by Foundry. This overrides lower block @@ -217,6 +226,8 @@ export const deploy = async ( // gas. We use the `FOUNDRY_BLOCK_GAS_LIMIT` environment variable because it has a higher // priority than `DAPP_BLOCK_GAS_LIMIT`. FOUNDRY_BLOCK_GAS_LIMIT: MAX_UINT64.toString(), + FOUNDRY_SENDER: safeAddress, + ETH_FROM: safeAddress, }) if (spawnOutput.code !== 0) { diff --git a/packages/plugins/src/cli/propose/index.ts b/packages/plugins/src/cli/propose/index.ts index 425e2f438..adbf10455 100644 --- a/packages/plugins/src/cli/propose/index.ts +++ b/packages/plugins/src/cli/propose/index.ts @@ -76,7 +76,7 @@ export const buildNetworkConfigArray: BuildNetworkConfigArray = async ( buildInfos?: BuildInfos isEmpty: boolean }> => { - const { testnets, mainnets } = await getSphinxConfigFromScript( + const { testnets, mainnets, safeAddress } = await getSphinxConfigFromScript( scriptPath, sphinxPluginTypesInterface, targetContract, @@ -121,6 +121,7 @@ export const buildNetworkConfigArray: BuildNetworkConfigArray = async ( '--sig', 'sphinxCollectProposal(string)', deploymentInfoPath, + '--always-use-create-2-factory', ] if ( @@ -141,6 +142,8 @@ export const buildNetworkConfigArray: BuildNetworkConfigArray = async ( // gas. We use the `FOUNDRY_BLOCK_GAS_LIMIT` environment variable because it has a higher // priority than `DAPP_BLOCK_GAS_LIMIT`. FOUNDRY_BLOCK_GAS_LIMIT: MAX_UINT64.toString(), + FOUNDRY_SENDER: safeAddress, + ETH_FROM: safeAddress, }) if (spawnOutput.code !== 0) { diff --git a/packages/plugins/src/foundry/options.ts b/packages/plugins/src/foundry/options.ts index 91a5d3e85..93bb86832 100644 --- a/packages/plugins/src/foundry/options.ts +++ b/packages/plugins/src/foundry/options.ts @@ -24,12 +24,6 @@ export const resolvePaths = (outPath: string, buildInfoPath: string) => { } export const checkRequiredTomlOptions = (toml: FoundryToml) => { - if (toml.alwaysUseCreate2Factory !== true) { - throw new Error( - 'Missing required option in foundry.toml file:\nalways_use_create_2_factory = true\nPlease update your foundry.toml file and try again.' - ) - } - if (toml.buildInfo !== true) { throw new Error( 'Missing required option in foundry.toml file:\nbuild_info = true\nPlease update your foundry.toml file and try again.' diff --git a/packages/plugins/src/foundry/utils/index.ts b/packages/plugins/src/foundry/utils/index.ts index 33599a29b..5dfb7e2af 100644 --- a/packages/plugins/src/foundry/utils/index.ts +++ b/packages/plugins/src/foundry/utils/index.ts @@ -1352,7 +1352,13 @@ export const assertValidVersions = async ( const output = await callForgeScriptFunction<{ libraryVersion: { value: string } forkInstalled: { value: string } - }>(scriptPath, 'sphinxValidate()', [], undefined, targetContract) + }>( + scriptPath, + 'sphinxValidate()', + ['--always-use-create-2-factory'], + undefined, + targetContract + ) const libraryVersion = output.returns.libraryVersion.value // The raw string is wrapped in two sets of quotes, so we remove the outer quotes here. @@ -1369,9 +1375,9 @@ export const assertValidVersions = async ( if (forkInstalled === 'false') { throw new Error( - `Detected invalid Foundry version. Please use Sphinx's fork of Foundry by\n` + + `Detected invalid Foundry version. Please install Sphinx's fork of Foundry by\n` + `running the command:\n` + - `foundryup --repo sphinx-labs/foundry --branch sphinx-patch-v0.1.0` + `npx sphinx install` ) } } diff --git a/packages/plugins/src/sample-project/sample-foundry-config.ts b/packages/plugins/src/sample-project/sample-foundry-config.ts index 7668fb187..944b96748 100644 --- a/packages/plugins/src/sample-project/sample-foundry-config.ts +++ b/packages/plugins/src/sample-project/sample-foundry-config.ts @@ -27,7 +27,6 @@ build_info = true extra_output = ['storageLayout'] fs_permissions=[{access="read", path="./out"}, {access="read-write", path="./cache"}] allow_paths = ["../.."] -always_use_create_2_factory = true ${fetchConfigRemappings(includeStandard)} [rpc_endpoints]