Skip to content

Commit

Permalink
feat(op-deployer): Proxy bootstrap command (#13213)
Browse files Browse the repository at this point in the history
* feat(op-deployer): `Proxy` bootstrap command

* code review updates

* linter

---------

Co-authored-by: Matthew Slipper <me@matthewslipper.com>
  • Loading branch information
clabby and mslipper authored Dec 6, 2024
1 parent d6fa448 commit f17da35
Show file tree
Hide file tree
Showing 5 changed files with 347 additions and 0 deletions.
21 changes: 21 additions & 0 deletions op-deployer/pkg/deployer/bootstrap/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const (
ReleaseFlagName = "release"
DelayedWethProxyFlagName = "delayed-weth-proxy"
DelayedWethImplFlagName = "delayed-weth-impl"
ProxyOwnerFlagName = "proxy-owner"
)

var (
Expand Down Expand Up @@ -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(),
}
)

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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,
},
}
178 changes: 178 additions & 0 deletions op-deployer/pkg/deployer/bootstrap/proxy.go
Original file line number Diff line number Diff line change
@@ -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
}
30 changes: 30 additions & 0 deletions op-deployer/pkg/deployer/opcm/proxy.go
Original file line number Diff line number Diff line change
@@ -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")
}
34 changes: 34 additions & 0 deletions op-deployer/pkg/deployer/opcm/proxy_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
84 changes: 84 additions & 0 deletions packages/contracts-bedrock/scripts/deploy/DeployProxy.s.sol
Original file line number Diff line number Diff line change
@@ -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"
);
}
}

0 comments on commit f17da35

Please sign in to comment.