Skip to content

Commit

Permalink
feat(pg): Set FOUNDRY_SENDER and ETH_FROM during collection
Browse files Browse the repository at this point in the history
  • Loading branch information
RPate97 committed Feb 28, 2024
1 parent 643d8e3 commit 56bae32
Show file tree
Hide file tree
Showing 10 changed files with 159 additions and 15 deletions.
5 changes: 5 additions & 0 deletions .changeset/weak-crews-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sphinx-labs/plugins': patch
---

Set FOUNDRY_SENDER and ETH_FROM during transaction collection
1 change: 0 additions & 1 deletion docs/cli-existing-project.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions docs/writing-scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
7 changes: 6 additions & 1 deletion packages/contracts/contracts/foundry/Sphinx.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
120 changes: 118 additions & 2 deletions packages/contracts/contracts/foundry/SphinxUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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);
}
}
}
11 changes: 11 additions & 0 deletions packages/plugins/src/cli/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
assertValidVersions,
compile,
getInitCodeWithArgsArray,
getSphinxConfigFromScript,
readInterface,
writeSystemContracts,
} from '../foundry/utils'
Expand Down Expand Up @@ -198,6 +199,7 @@ export const deploy = async (
systemContractsFilePath,
'--rpc-url',
forkUrl,
'--always-use-create-2-factory',
]
if (
isLegacyTransactionsRequiredForNetwork(
Expand All @@ -210,13 +212,22 @@ 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
// gas limits specified in the user's `foundry.toml`, which can cause the script to run out of
// 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) {
Expand Down
5 changes: 4 additions & 1 deletion packages/plugins/src/cli/propose/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -121,6 +121,7 @@ export const buildNetworkConfigArray: BuildNetworkConfigArray = async (
'--sig',
'sphinxCollectProposal(string)',
deploymentInfoPath,
'--always-use-create-2-factory',
]

if (
Expand All @@ -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) {
Expand Down
6 changes: 0 additions & 6 deletions packages/plugins/src/foundry/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
Expand Down
12 changes: 9 additions & 3 deletions packages/plugins/src/foundry/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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`
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down

0 comments on commit 56bae32

Please sign in to comment.