Skip to content

Commit

Permalink
feat(pg): support the CREATE opcode
Browse files Browse the repository at this point in the history
  • Loading branch information
sam-goldman committed Feb 5, 2024
1 parent e6b9a93 commit ce65752
Show file tree
Hide file tree
Showing 40 changed files with 2,008 additions and 1,302 deletions.
7 changes: 7 additions & 0 deletions .changeset/fluffy-tables-yawn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@sphinx-labs/contracts': patch
'@sphinx-labs/plugins': patch
'@sphinx-labs/core': patch
---

Decode actions when creating parsed config
8 changes: 8 additions & 0 deletions .changeset/large-turkeys-explain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@sphinx-labs/plugins': minor
'@sphinx-labs/contracts': patch
'@sphinx-labs/core': patch
'@sphinx-labs/demo': patch
---

Add support for `CREATE` opcode deployments
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ A demo of the DevOps Platform is on the [website's landing page](https://sphinx.

## Current Limitations

- Sphinx supports `CREATE2` and `CREATE3` but not the `CREATE` opcode, i.e. `new MyContract(...)`.
- You cannot deploy [libraries](https://docs.soliditylang.org/en/v0.8.24/contracts.html#libraries).
- You cannot send ETH as part of a deployment.
- You can only use the [Deploy CLI Command](https://github.com/sphinx-labs/sphinx/blob/main/docs/cli-deploy.md) on live networks if your Gnosis Safe has a single owner. (Deployments with the DevOps Platform support an arbitrary number of owners).

Expand Down
140 changes: 79 additions & 61 deletions packages/contracts/contracts/foundry/Sphinx.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
DeploymentInfo,
NetworkInfo,
Wallet,
SphinxTransaction,
GnosisSafeTransaction,
ExecutionMode,
SystemContractInfo
} from "./SphinxPluginTypes.sol";
Expand Down Expand Up @@ -68,8 +68,6 @@ abstract contract Sphinx {

SphinxUtils private sphinxUtils;

bool private isCollecting;

bool private sphinxModifierEnabled;

constructor() {
Expand All @@ -96,7 +94,9 @@ abstract contract Sphinx {
vm.makePersistent(address(sphinxUtils));
}

function sphinxCollectProposal(string memory _deploymentInfoPath) external {
function sphinxCollectProposal(
string memory _deploymentInfoPath
) external {
sphinxUtils.validateProposal(sphinxConfig);

DeploymentInfo memory deploymentInfo = sphinxCollect(
Expand All @@ -109,7 +109,8 @@ abstract contract Sphinx {

function sphinxCollectDeployment(
ExecutionMode _executionMode,
string memory _deploymentInfoPath
string memory _deploymentInfoPath,
string memory _systemContractsFilePath
) external {
address deployer;
if (_executionMode == ExecutionMode.LiveNetworkCLI) {
Expand All @@ -124,7 +125,21 @@ abstract contract Sphinx {
revert("Incorrect execution type.");
}

DeploymentInfo memory deploymentInfo = sphinxCollect(_executionMode, deployer);
SystemContractInfo[] memory systemContracts = abi.decode(
vm.parseBytes(vm.readFile(_systemContractsFilePath)),
(SystemContractInfo[])
);

// Deploy the Sphinx system contracts. This is necessary because several Sphinx and Gnosis
// Safe contracts are required to deploy a Gnosis Safe, which itself must be deployed
// because we're going to call the Gnosis Safe to estimate the gas. Also, deploying the
// Gnosis Safe ensures that its nonce is treated like a contract instead of an EOA.
sphinxUtils.deploySphinxSystem(systemContracts);

DeploymentInfo memory deploymentInfo = sphinxCollect(
_executionMode,
deployer
);
vm.writeFile(_deploymentInfoPath, vm.toString(abi.encode(deploymentInfo)));
}

Expand Down Expand Up @@ -161,15 +176,35 @@ abstract contract Sphinx {
deploymentInfo.arbitraryChain = false;
deploymentInfo.requireSuccess = true;

isCollecting = true;
// 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.
if (address(safe).code.length == 0) {
_sphinxDeployModuleAndGnosisSafe();
}

// Take a snapshot of the current state. We'll revert to the snapshot after we run the
// user's script but before we execute the user's transactions via the Gnosis Safe to
// estimate the Merkle leaf gas fields. It's necessary to revert the snapshot because the
// gas estimation won't work if it runs against chain state where the user's transactions
// have already occurred.
uint256 snapshotId = vm.snapshot();

vm.startStateDiffRecording();
// Delegatecall the `run()` function on this contract to collect the transactions. This
// pattern gives us flexibility to support function names other than `run()` in the future.
(bool success, ) = address(this).delegatecall(abi.encodeWithSignature("run()"));
// Throw an error if the deployment script fails. The error message in the user's script is
// displayed by Foundry's stack trace, so it'd be redundant to include the data returned by
// the delegatecall in our error message.
require(success, "Sphinx: Deployment script failed.");
deploymentInfo.accountAccesses = vm.stopAndReturnStateDiff();

vm.revertTo(snapshotId);
deploymentInfo.gasEstimates = _sphinxEstimateMerkleLeafGas(
deploymentInfo.accountAccesses,
IGnosisSafe(safe),
module
);

return deploymentInfo;
}
Expand Down Expand Up @@ -215,7 +250,7 @@ abstract contract Sphinx {
"Sphinx: You must broadcast deployments using the 'sphinx deploy' CLI command."
);
require(
callerMode != VmSafe.CallerMode.RecurrentBroadcast || isCollecting,
callerMode != VmSafe.CallerMode.RecurrentBroadcast,
"Sphinx: You must broadcast deployments using the 'sphinx deploy' CLI command."
);
require(
Expand All @@ -233,18 +268,11 @@ abstract contract Sphinx {

sphinxUtils.validate(sphinxConfig);

if (isCollecting) {
// Execute the user's 'run()' function.
vm.startBroadcast(safeAddress());
_;
vm.stopBroadcast();
} else {
// Prank the Gnosis Safe then execute the user's `run()` function. We prank the Gnosis
// Safe to replicate the deployment process on live networks.
vm.startPrank(safeAddress());
_;
vm.stopPrank();
}
// Prank the Gnosis Safe then execute the user's script. We prank the Gnosis
// Safe to replicate the production environment.
vm.startPrank(safeAddress());
_;
vm.stopPrank();

if (callerMode == VmSafe.CallerMode.RecurrentPrank) vm.startPrank(msgSender);

Expand Down Expand Up @@ -303,63 +331,53 @@ abstract contract Sphinx {
* Merkle leaf's gas, resulting in a failed deployment on-chain. This situation uses
* contrived numbers, but the point is that using `gasleft` is accurate even if
* there's a large gas refund.
*
* @return abiEncodedGasArray The ABI encoded array of gas estimates. There's one element per
* `EXECUTE` Merkle leaf. We ABI encode the array because Foundry
* makes it difficult to reliably parse complex data types off-chain.
* Specifically, an array element looks like this in the returned
* JSON: `27222 [2.722e4]`.
*/
function sphinxEstimateMerkleLeafGas(
string memory _leafGasParamsFilePath
) external returns (bytes memory abiEncodedGasArray) {
(SphinxTransaction[] memory txnArray, SystemContractInfo[] memory systemContracts) = abi
.decode(
vm.parseBytes(vm.readFile(_leafGasParamsFilePath)),
(SphinxTransaction[], SystemContractInfo[])
);

// Deploy the Sphinx system contracts. This is necessary because several Sphinx and Gnosis
// Safe contracts are required to deploy a Gnosis Safe, which itself must be deployed
// because we're going to call the Gnosis Safe to estimate the gas. Also, this is necessary
// because the system contracts may not already be deployed on the current network.
sphinxUtils.deploySphinxSystem(systemContracts);

IGnosisSafe safe = IGnosisSafe(safeAddress());
address module = sphinxModule();
address managedServiceAddress = constants.managedServiceAddress();

function _sphinxEstimateMerkleLeafGas(
Vm.AccountAccess[] memory _accountAccesses,
IGnosisSafe _safe,
address _moduleAddress
) private returns (uint256[] memory) {
GnosisSafeTransaction[] memory txnArray = sphinxUtils.makeGnosisSafeTransactions(
address(_safe),
_accountAccesses
);
uint256[] memory gasEstimates = new uint256[](txnArray.length);

// 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.
if (address(safe).code.length == 0) {
// Deploy the Gnosis Safe and Sphinx Module. It's not strictly necessary to prank the
// Managed Service contract, but this replicates the prod environment for the DevOps
// Platform, so we do it anyways.
vm.startPrank(managedServiceAddress);
_sphinxDeployModuleAndGnosisSafe();
vm.stopPrank();
}

// We prank the Sphinx Module to replicate the production environment. In prod, the Sphinx
// Module calls the Gnosis Safe.
vm.startPrank(module);
vm.startPrank(_moduleAddress);

for (uint256 i = 0; i < txnArray.length; i++) {
SphinxTransaction memory txn = txnArray[i];
GnosisSafeTransaction memory txn = txnArray[i];
uint256 startGas = gasleft();
bool success = safe.execTransactionFromModule(
bool success = _safe.execTransactionFromModule(
txn.to,
txn.value,
txn.txData,
txn.operation
);
gasEstimates[i] = startGas - gasleft();
uint256 finalGas = gasleft();

require(success, "Sphinx: failed to call Gnosis Safe from Sphinx Module");

// Include a buffer to ensure the user's transaction doesn't fail on-chain due to
// variations between the simulation and the live execution environment. There are a
// couple areas in particular that could lead to variations:
// 1. The on-chain state could vary, which could impact the cost of execution. This is
// inherently a source of variation because there's a delay between the simulation
// and execution.
// 2. Foundry's simulation is treated as a single transaction, which means SLOADs are
// more likely to be "warm" (i.e. cheaper) than the production environment, where
// transactions may be split between batches.
//
// We chose to multiply the gas by 1.3 because multiplying it by a higher number could
// make a very large transaction unexecutable on-chain. Since the 1.3x multiplier
// doesn't impact small transactions very much, we add a constant amount of 20k too.
gasEstimates[i] = 20_000 + ((startGas - finalGas) * 13) / 10;
}

vm.stopPrank();

return abi.encode(gasEstimates);
return gasEstimates;
}
}
1 change: 1 addition & 0 deletions packages/contracts/contracts/foundry/SphinxConstants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ contract SphinxConstants {
string public constant sphinxLibraryVersion = 'v0.17.1';
address public constant compatibilityFallbackHandlerAddress = 0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4;
address public constant multiSendAddress = 0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761;
address public constant createCallAddress = 0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4;
address public constant sphinxModuleProxyFactoryAddress = 0x8f3301c9Eada5642B5bB12FD047D3EBb2932E619;
address public constant managedServiceAddress = 0xB5E96127D417b1B3ef8438496a38A143167209c7;
address public constant safeFactoryAddress = 0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2;
Expand Down
11 changes: 6 additions & 5 deletions packages/contracts/contracts/foundry/SphinxPluginTypes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity ^0.8.0;
import { SphinxLeafType, SphinxLeaf, SphinxLeafWithProof } from "../core/SphinxDataTypes.sol";
import { Network } from "./SphinxConstants.sol";
import { IEnum } from "./interfaces/IEnum.sol";
import { Vm, VmSafe } from "../../lib/forge-std/src/Vm.sol";

struct HumanReadableAction {
string reason;
Expand All @@ -15,13 +16,11 @@ struct SphinxMerkleTree {
SphinxLeafWithProof[] leavesWithProofs;
}

struct SphinxTransaction {
struct GnosisSafeTransaction {
address to;
uint256 value;
bytes txData;
IEnum.GnosisSafeOperation operation;
uint256 gas;
bool requireSuccess;
}

struct FoundryContractConfig {
Expand Down Expand Up @@ -88,6 +87,8 @@ struct DeploymentInfo {
InitialChainState initialState;
bool arbitraryChain;
string sphinxLibraryVersion;
Vm.AccountAccess[] accountAccesses;
uint256[] gasEstimates;
}

enum ExecutionMode {
Expand Down Expand Up @@ -205,10 +206,10 @@ contract SphinxPluginTypes {

function sphinxConfigType() external view returns (SphinxConfig memory sphinxConfig) {}

function leafGasParams()
function systemContractInfoArrayType()
external
view
returns (SphinxTransaction[] memory txnArray, SystemContractInfo[] memory systemContracts)
returns (SystemContractInfo[] memory systemContracts)
{}

function sphinxLeafWithProofType()
Expand Down
Loading

0 comments on commit ce65752

Please sign in to comment.