From f17da354edf105c86652784d016a3e11b3c8585f Mon Sep 17 00:00:00 2001 From: clabby Date: Thu, 5 Dec 2024 22:38:09 -0500 Subject: [PATCH] feat(op-deployer): `Proxy` bootstrap command (#13213) * feat(op-deployer): `Proxy` bootstrap command * code review updates * linter --------- Co-authored-by: Matthew Slipper --- op-deployer/pkg/deployer/bootstrap/flags.go | 21 +++ op-deployer/pkg/deployer/bootstrap/proxy.go | 178 ++++++++++++++++++ op-deployer/pkg/deployer/opcm/proxy.go | 30 +++ op-deployer/pkg/deployer/opcm/proxy_test.go | 34 ++++ .../scripts/deploy/DeployProxy.s.sol | 84 +++++++++ 5 files changed, 347 insertions(+) create mode 100644 op-deployer/pkg/deployer/bootstrap/proxy.go create mode 100644 op-deployer/pkg/deployer/opcm/proxy.go create mode 100644 op-deployer/pkg/deployer/opcm/proxy_test.go create mode 100644 packages/contracts-bedrock/scripts/deploy/DeployProxy.s.sol diff --git a/op-deployer/pkg/deployer/bootstrap/flags.go b/op-deployer/pkg/deployer/bootstrap/flags.go index f6a130f075a0..20cf02b93376 100644 --- a/op-deployer/pkg/deployer/bootstrap/flags.go +++ b/op-deployer/pkg/deployer/bootstrap/flags.go @@ -34,6 +34,7 @@ const ( ReleaseFlagName = "release" DelayedWethProxyFlagName = "delayed-weth-proxy" DelayedWethImplFlagName = "delayed-weth-impl" + ProxyOwnerFlagName = "proxy-owner" ) var ( @@ -167,6 +168,13 @@ var ( Name: ReleaseFlagName, Usage: "Release to deploy.", EnvVars: deployer.PrefixEnvVar("RELEASE"), + Value: common.Address{}.Hex(), + } + ProxyOwnerFlag = &cli.StringFlag{ + Name: ProxyOwnerFlagName, + Usage: "Proxy owner address.", + EnvVars: deployer.PrefixEnvVar("PROXY_OWNER"), + Value: common.Address{}.Hex(), } ) @@ -224,6 +232,13 @@ var MIPSFlags = append(BaseFPVMFlags, MIPSVersionFlag) var AsteriscFlags = BaseFPVMFlags +var ProxyFlags = []cli.Flag{ + deployer.L1RPCURLFlag, + deployer.PrivateKeyFlag, + ArtifactsLocatorFlag, + ProxyOwnerFlag, +} + var Commands = []*cli.Command{ { Name: "opcm", @@ -264,4 +279,10 @@ var Commands = []*cli.Command{ Flags: cliapp.ProtectFlags(AsteriscFlags), Action: AsteriscCLI, }, + { + Name: "proxy", + Usage: "Bootstrap a ERC-1967 Proxy without an implementation set.", + Flags: cliapp.ProtectFlags(ProxyFlags), + Action: ProxyCLI, + }, } diff --git a/op-deployer/pkg/deployer/bootstrap/proxy.go b/op-deployer/pkg/deployer/bootstrap/proxy.go new file mode 100644 index 000000000000..c96e497c8c73 --- /dev/null +++ b/op-deployer/pkg/deployer/bootstrap/proxy.go @@ -0,0 +1,178 @@ +package bootstrap + +import ( + "context" + "crypto/ecdsa" + "fmt" + "strings" + + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts" + "github.com/ethereum/go-ethereum/common" + + "github.com/ethereum-optimism/optimism/op-deployer/pkg/env" + + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/broadcaster" + + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm" + opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto" + "github.com/ethereum-optimism/optimism/op-service/ctxinterrupt" + "github.com/ethereum-optimism/optimism/op-service/ioutil" + "github.com/ethereum-optimism/optimism/op-service/jsonutil" + oplog "github.com/ethereum-optimism/optimism/op-service/log" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rpc" + "github.com/urfave/cli/v2" +) + +type ProxyConfig struct { + L1RPCUrl string + PrivateKey string + Logger log.Logger + ArtifactsLocator *artifacts.Locator + + privateKeyECDSA *ecdsa.PrivateKey + + Owner common.Address +} + +func (c *ProxyConfig) Check() error { + if c.L1RPCUrl == "" { + return fmt.Errorf("l1RPCUrl must be specified") + } + + if c.PrivateKey == "" { + return fmt.Errorf("private key must be specified") + } + + privECDSA, err := crypto.HexToECDSA(strings.TrimPrefix(c.PrivateKey, "0x")) + if err != nil { + return fmt.Errorf("failed to parse private key: %w", err) + } + c.privateKeyECDSA = privECDSA + + if c.Logger == nil { + return fmt.Errorf("logger must be specified") + } + + if c.ArtifactsLocator == nil { + return fmt.Errorf("artifacts locator must be specified") + } + + if c.Owner == (common.Address{}) { + return fmt.Errorf("proxy owner must be specified") + } + + return nil +} + +func ProxyCLI(cliCtx *cli.Context) error { + logCfg := oplog.ReadCLIConfig(cliCtx) + l := oplog.NewLogger(oplog.AppOut(cliCtx), logCfg) + oplog.SetGlobalLogHandler(l.Handler()) + + l1RPCUrl := cliCtx.String(deployer.L1RPCURLFlagName) + privateKey := cliCtx.String(deployer.PrivateKeyFlagName) + artifactsURLStr := cliCtx.String(ArtifactsLocatorFlagName) + artifactsLocator := new(artifacts.Locator) + if err := artifactsLocator.UnmarshalText([]byte(artifactsURLStr)); err != nil { + return fmt.Errorf("failed to parse artifacts URL: %w", err) + } + + owner := common.HexToAddress(cliCtx.String(ProxyOwnerFlagName)) + + ctx := ctxinterrupt.WithCancelOnInterrupt(cliCtx.Context) + + return Proxy(ctx, ProxyConfig{ + L1RPCUrl: l1RPCUrl, + PrivateKey: privateKey, + Logger: l, + ArtifactsLocator: artifactsLocator, + Owner: owner, + }) +} + +func Proxy(ctx context.Context, cfg ProxyConfig) error { + if err := cfg.Check(); err != nil { + return fmt.Errorf("invalid config for Proxy: %w", err) + } + + lgr := cfg.Logger + progressor := func(curr, total int64) { + lgr.Info("artifacts download progress", "current", curr, "total", total) + } + + artifactsFS, cleanup, err := artifacts.Download(ctx, cfg.ArtifactsLocator, progressor) + if err != nil { + return fmt.Errorf("failed to download artifacts: %w", err) + } + defer func() { + if err := cleanup(); err != nil { + lgr.Warn("failed to clean up artifacts", "err", err) + } + }() + + l1Client, err := ethclient.Dial(cfg.L1RPCUrl) + if err != nil { + return fmt.Errorf("failed to connect to L1 RPC: %w", err) + } + + chainID, err := l1Client.ChainID(ctx) + if err != nil { + return fmt.Errorf("failed to get chain ID: %w", err) + } + + signer := opcrypto.SignerFnFromBind(opcrypto.PrivateKeySignerFn(cfg.privateKeyECDSA, chainID)) + chainDeployer := crypto.PubkeyToAddress(cfg.privateKeyECDSA.PublicKey) + + bcaster, err := broadcaster.NewKeyedBroadcaster(broadcaster.KeyedBroadcasterOpts{ + Logger: lgr, + ChainID: chainID, + Client: l1Client, + Signer: signer, + From: chainDeployer, + }) + if err != nil { + return fmt.Errorf("failed to create broadcaster: %w", err) + } + + l1RPC, err := rpc.Dial(cfg.L1RPCUrl) + if err != nil { + return fmt.Errorf("failed to connect to L1 RPC: %w", err) + } + + l1Host, err := env.DefaultForkedScriptHost( + ctx, + bcaster, + lgr, + chainDeployer, + artifactsFS, + l1RPC, + ) + if err != nil { + return fmt.Errorf("failed to create script host: %w", err) + } + + dgo, err := opcm.DeployProxy( + l1Host, + opcm.DeployProxyInput{ + Owner: cfg.Owner, + }, + ) + if err != nil { + return fmt.Errorf("error deploying proxy: %w", err) + } + + if _, err := bcaster.Broadcast(ctx); err != nil { + return fmt.Errorf("failed to broadcast: %w", err) + } + + lgr.Info("deployed new ERC-1967 proxy") + + if err := jsonutil.WriteJSON(dgo, ioutil.ToStdOut()); err != nil { + return fmt.Errorf("failed to write output: %w", err) + } + return nil +} diff --git a/op-deployer/pkg/deployer/opcm/proxy.go b/op-deployer/pkg/deployer/opcm/proxy.go new file mode 100644 index 000000000000..850c337f83f4 --- /dev/null +++ b/op-deployer/pkg/deployer/opcm/proxy.go @@ -0,0 +1,30 @@ +package opcm + +import ( + "github.com/ethereum/go-ethereum/common" + + "github.com/ethereum-optimism/optimism/op-chain-ops/script" +) + +type DeployProxyInput struct { + Owner common.Address +} + +func (input *DeployProxyInput) InputSet() bool { + return true +} + +type DeployProxyOutput struct { + Proxy common.Address +} + +type DeployProxyScript struct { + Run func(input, output common.Address) error +} + +func DeployProxy( + host *script.Host, + input DeployProxyInput, +) (DeployProxyOutput, error) { + return RunBasicScript[DeployProxyInput, DeployProxyOutput](host, input, "DeployProxy.s.sol", "DeployProxy") +} diff --git a/op-deployer/pkg/deployer/opcm/proxy_test.go b/op-deployer/pkg/deployer/opcm/proxy_test.go new file mode 100644 index 000000000000..bb9cb350a369 --- /dev/null +++ b/op-deployer/pkg/deployer/opcm/proxy_test.go @@ -0,0 +1,34 @@ +package opcm + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/broadcaster" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/testutil" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/env" + "github.com/ethereum-optimism/optimism/op-service/testlog" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "github.com/stretchr/testify/require" +) + +func TestDeployProxy(t *testing.T) { + _, artifacts := testutil.LocalArtifacts(t) + + host, err := env.DefaultScriptHost( + broadcaster.NoopBroadcaster(), + testlog.Logger(t, log.LevelInfo), + common.Address{'D'}, + artifacts, + ) + require.NoError(t, err) + + input := DeployProxyInput{ + Owner: common.Address{0xab}, + } + + output, err := DeployProxy(host, input) + require.NoError(t, err) + + require.NotEmpty(t, output.Proxy) +} diff --git a/packages/contracts-bedrock/scripts/deploy/DeployProxy.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployProxy.s.sol new file mode 100644 index 000000000000..08eadc671bc3 --- /dev/null +++ b/packages/contracts-bedrock/scripts/deploy/DeployProxy.s.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +// Forge +import { Script } from "forge-std/Script.sol"; + +// Scripts +import { BaseDeployIO } from "scripts/deploy/BaseDeployIO.sol"; +import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; + +// Interfaces +import { IProxy } from "interfaces/universal/IProxy.sol"; + +/// @title DeployProxyInput +contract DeployProxyInput is BaseDeployIO { + // Specify the owner of the proxy that is being deployed + address internal _owner; + + function set(bytes4 _sel, address _value) public { + if (_sel == this.owner.selector) { + require(_value != address(0), "DeployProxy: owner cannot be empty"); + _owner = _value; + } else { + revert("DeployProxy: unknown selector"); + } + } + + function owner() public view returns (address) { + require(_owner != address(0), "DeployProxy: owner not set"); + return _owner; + } +} + +/// @title DeployProxyOutput +contract DeployProxyOutput is BaseDeployIO { + IProxy internal _proxy; + + function set(bytes4 _sel, address _value) public { + if (_sel == this.proxy.selector) { + require(_value != address(0), "DeployProxy: proxy cannot be zero address"); + _proxy = IProxy(payable(_value)); + } else { + revert("DeployProxy: unknown selector"); + } + } + + function proxy() public view returns (IProxy) { + DeployUtils.assertValidContractAddress(address(_proxy)); + return _proxy; + } +} + +/// @title DeployProxy +contract DeployProxy is Script { + function run(DeployProxyInput _mi, DeployProxyOutput _mo) public { + deployProxySingleton(_mi, _mo); + checkOutput(_mi, _mo); + } + + function deployProxySingleton(DeployProxyInput _mi, DeployProxyOutput _mo) internal { + address owner = _mi.owner(); + vm.broadcast(msg.sender); + IProxy proxy = IProxy( + DeployUtils.create1({ + _name: "Proxy", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IProxy.__constructor__, (owner))) + }) + ); + + vm.label(address(proxy), "Proxy"); + _mo.set(_mo.proxy.selector, address(proxy)); + } + + function checkOutput(DeployProxyInput _mi, DeployProxyOutput _mo) public { + DeployUtils.assertValidContractAddress(address(_mo.proxy())); + IProxy prox = _mo.proxy(); + vm.prank(_mi.owner()); + address proxyOwner = prox.admin(); + + require( + proxyOwner == _mi.owner(), "DeployProxy: owner of proxy does not match the owner specified in the input" + ); + } +}