Commit f17da354 authored by clabby's avatar clabby Committed by GitHub

feat(op-deployer): `Proxy` bootstrap command (#13213)

* feat(op-deployer): `Proxy` bootstrap command

* code review updates

* linter

---------
Co-authored-by: default avatarMatthew Slipper <me@matthewslipper.com>
parent d6fa448a
...@@ -34,6 +34,7 @@ const ( ...@@ -34,6 +34,7 @@ const (
ReleaseFlagName = "release" ReleaseFlagName = "release"
DelayedWethProxyFlagName = "delayed-weth-proxy" DelayedWethProxyFlagName = "delayed-weth-proxy"
DelayedWethImplFlagName = "delayed-weth-impl" DelayedWethImplFlagName = "delayed-weth-impl"
ProxyOwnerFlagName = "proxy-owner"
) )
var ( var (
...@@ -167,6 +168,13 @@ var ( ...@@ -167,6 +168,13 @@ var (
Name: ReleaseFlagName, Name: ReleaseFlagName,
Usage: "Release to deploy.", Usage: "Release to deploy.",
EnvVars: deployer.PrefixEnvVar("RELEASE"), 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) ...@@ -224,6 +232,13 @@ var MIPSFlags = append(BaseFPVMFlags, MIPSVersionFlag)
var AsteriscFlags = BaseFPVMFlags var AsteriscFlags = BaseFPVMFlags
var ProxyFlags = []cli.Flag{
deployer.L1RPCURLFlag,
deployer.PrivateKeyFlag,
ArtifactsLocatorFlag,
ProxyOwnerFlag,
}
var Commands = []*cli.Command{ var Commands = []*cli.Command{
{ {
Name: "opcm", Name: "opcm",
...@@ -264,4 +279,10 @@ var Commands = []*cli.Command{ ...@@ -264,4 +279,10 @@ var Commands = []*cli.Command{
Flags: cliapp.ProtectFlags(AsteriscFlags), Flags: cliapp.ProtectFlags(AsteriscFlags),
Action: AsteriscCLI, Action: AsteriscCLI,
}, },
{
Name: "proxy",
Usage: "Bootstrap a ERC-1967 Proxy without an implementation set.",
Flags: cliapp.ProtectFlags(ProxyFlags),
Action: ProxyCLI,
},
} }
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
}
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")
}
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)
}
// 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"
);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment