Skip to content
Closed
28 changes: 27 additions & 1 deletion op-chain-ops/foundry/artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ func (a Artifact) MarshalJSON() ([]byte, error) {
// foundry artifacts.
type artifactMarshaling struct {
ABI json.RawMessage `json:"abi"`
Source string `json:"source"`
StorageLayout solc.StorageLayout `json:"storageLayout"`
DeployedBytecode DeployedBytecode `json:"deployedBytecode"`
Bytecode Bytecode `json:"bytecode"`
Expand All @@ -77,7 +78,7 @@ type Metadata struct {

Settings struct {
// Remappings of the contract imports
Remappings json.RawMessage `json:"remappings"`
Remappings []string `json:"remappings"`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change made it easier to construct the etherscan verification request

// Optimizer settings affect the compiler output, but can be arbitrary.
// We load them opaquely, to include it in the hash of what we run.
Optimizer json.RawMessage `json:"optimizer"`
Expand All @@ -102,6 +103,7 @@ type Metadata struct {
type ContractSource struct {
Keccak256 common.Hash `json:"keccak256"`
URLs []string `json:"urls"`
Content string `json:"content"`
License string `json:"license"`
}

Expand Down Expand Up @@ -153,3 +155,27 @@ func ReadArtifact(path string) (*Artifact, error) {
}
return &artifact, nil
}

// SearchRemappings applies the configured remappings to a given source path,
// or returns the source path unchanged if no remapping is found. It assumes that
// each remapping is of the form "alias/=actualPath".
func (a Artifact) SearchRemappings(sourcePath string) string {
for _, mapping := range a.Metadata.Settings.Remappings {
parts := strings.Split(mapping, "/=")
if len(parts) != 2 {
continue
}
alias := parts[0]
if !strings.HasSuffix(alias, "/") {
alias += "/"
}
actualPath := parts[1]
if !strings.HasSuffix(actualPath, "/") {
actualPath += "/"
}
if strings.HasPrefix(sourcePath, actualPath) {
return alias + sourcePath[len(actualPath):]
}
}
return sourcePath
}
7 changes: 7 additions & 0 deletions op-deployer/cmd/op-deployer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/clean"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/upgrade"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/verify"

"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/bootstrap"
Expand Down Expand Up @@ -66,6 +67,12 @@ func main() {
Usage: "cleans up various things",
Subcommands: clean.Commands,
},
{
Name: "verify",
Usage: "verifies deployed contracts on Etherscan",
Flags: cliapp.ProtectFlags(deployer.VerifyFlags),
Action: verify.VerifyCLI,
},
}
app.Writer = os.Stdout
app.ErrWriter = os.Stderr
Expand Down
59 changes: 47 additions & 12 deletions op-deployer/pkg/deployer/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,19 @@ import (
)

const (
EnvVarPrefix = "DEPLOYER"
L1RPCURLFlagName = "l1-rpc-url"
CacheDirFlagName = "cache-dir"
L1ChainIDFlagName = "l1-chain-id"
L2ChainIDsFlagName = "l2-chain-ids"
WorkdirFlagName = "workdir"
OutdirFlagName = "outdir"
PrivateKeyFlagName = "private-key"
IntentTypeFlagName = "intent-type"
EnvVarPrefix = "DEPLOYER"
L1RPCURLFlagName = "l1-rpc-url"
CacheDirFlagName = "cache-dir"
L1ChainIDFlagName = "l1-chain-id"
L2ChainIDsFlagName = "l2-chain-ids"
WorkdirFlagName = "workdir"
OutdirFlagName = "outdir"
PrivateKeyFlagName = "private-key"
IntentTypeFlagName = "intent-type"
EtherscanAPIKeyFlagName = "etherscan-api-key"
ContractBundleFlagName = "contract-bundle"
ContractNameFlagName = "contract-name"
L2ChainIDFlagName = "l2-chain-id"
)

type DeploymentTarget string
Expand All @@ -48,14 +52,15 @@ func NewDeploymentTarget(s string) (DeploymentTarget, error) {
}
}

var homeDir string
var DefaultCacheDir string

func init() {
var err error
homeDir, err = os.UserHomeDir()
homeDir, err := os.UserHomeDir()
if err != nil {
panic(fmt.Sprintf("failed to get home directory: %s", err))
}
DefaultCacheDir = path.Join(homeDir, ".op-deployer/cache")
}

var (
Expand All @@ -72,7 +77,7 @@ var (
Usage: "Cache directory. " +
"If set, the deployer will attempt to cache downloaded artifacts in the specified directory.",
EnvVars: PrefixEnvVar("CACHE_DIR"),
Value: path.Join(homeDir, ".op-deployer/cache"),
Value: DefaultCacheDir,
}
L1ChainIDFlag = &cli.Uint64Flag{
Name: L1ChainIDFlagName,
Expand All @@ -85,6 +90,11 @@ var (
Usage: "Comma-separated list of L2 chain IDs to deploy.",
EnvVars: PrefixEnvVar("L2_CHAIN_IDS"),
}
L2ChainIDFlag = &cli.StringFlag{
Name: L2ChainIDFlagName,
Usage: "Single L2 chain ID",
EnvVars: PrefixEnvVar("L2_CHAIN_ID"),
}
WorkdirFlag = &cli.StringFlag{
Name: WorkdirFlagName,
Usage: "Directory storing intent and stage. Defaults to the current directory.",
Expand Down Expand Up @@ -117,6 +127,22 @@ var (
"intent-config-type",
},
}
EtherscanAPIKeyFlag = &cli.StringFlag{
Name: EtherscanAPIKeyFlagName,
Usage: "etherscan API key for contract verification.",
EnvVars: PrefixEnvVar("ETHERSCAN_API_KEY"),
Required: true,
}
ContractBundleFlag = &cli.StringFlag{
Name: ContractBundleFlagName,
Usage: "contract bundle/grouping (superchain|implementations|opchain)",
EnvVars: PrefixEnvVar("CONTRACT_BUNDLE"),
}
ContractNameFlag = &cli.StringFlag{
Name: ContractNameFlagName,
Usage: "contract name (matching a field within state.json)",
EnvVars: PrefixEnvVar("CONTRACT_NAME"),
}
)

var GlobalFlags = append([]cli.Flag{CacheDirFlag}, oplog.CLIFlags(EnvVarPrefix)...)
Expand All @@ -141,6 +167,15 @@ var UpgradeFlags = []cli.Flag{
DeploymentTargetFlag,
}

var VerifyFlags = []cli.Flag{
L1RPCURLFlag,
WorkdirFlag,
EtherscanAPIKeyFlag,
ContractBundleFlag,
ContractNameFlag,
L2ChainIDFlag,
}

func PrefixEnvVar(name string) []string {
return op_service.PrefixEnvVar(EnvVarPrefix, name)
}
Expand Down
34 changes: 34 additions & 0 deletions op-deployer/pkg/deployer/inspect/l1.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package inspect

import (
"fmt"
"reflect"

"github.com/ethereum-optimism/optimism/op-chain-ops/genesis"

Expand All @@ -20,6 +21,39 @@ type L1Contracts struct {
ImplementationsDeployment ImplementationsDeployment `json:"implementationsDeployment"`
}

const (
SuperchainBundle = "superchain"
ImplementationsBundle = "implementations"
OpChainBundle = "opchain"
)

var ContractBundles = []string{
SuperchainBundle,
ImplementationsBundle,
OpChainBundle,
}

func (l L1Contracts) GetContractAddress(name string, bundleName string) (common.Address, error) {
var bundle interface{}
switch bundleName {
case SuperchainBundle:
bundle = l.SuperchainDeployment
case ImplementationsBundle:
bundle = l.ImplementationsDeployment
case OpChainBundle:
bundle = l.OpChainDeployment
default:
return common.Address{}, fmt.Errorf("invalid contract bundle type: %s", bundleName)
}

field := reflect.ValueOf(bundle).FieldByName(name)
if !field.IsValid() {
return common.Address{}, fmt.Errorf("contract %s not found in %s bundle", name, bundleName)
}

return field.Interface().(common.Address), nil
}

func (l L1Contracts) AsL1Deployments() *genesis.L1Deployments {
return &genesis.L1Deployments{
AddressManager: l.OpChainDeployment.AddressManagerAddress,
Expand Down
105 changes: 105 additions & 0 deletions op-deployer/pkg/deployer/verify/artifacts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package verify

import (
"encoding/json"
"fmt"
"path"
"strings"

"github.com/ethereum-optimism/optimism/op-chain-ops/foundry"
)

type contractArtifact struct {
ContractName string
CompilerVersion string
Optimizer OptimizerSettings
EVMVersion string
StandardInput string
ConstructorArgs string
}

// Map state.json struct's contract field names to forge artifact names
var contractNameExceptions = map[string]string{
"OptimismPortalImpl": "OptimismPortal2",
"L1StandardBridgeProxy": "L1ChugSplashProxy",
"L1CrossDomainMessengerProxy": "ResolvedDelegateProxy",
"Opcm": "OPContractsManager",
}

func getArtifactName(name string) string {
lookupName := strings.TrimSuffix(name, "Address")

if artifactName, exists := contractNameExceptions[lookupName]; exists {
return artifactName
}

lookupName = strings.TrimSuffix(lookupName, "Proxy")
lookupName = strings.TrimSuffix(lookupName, "Impl")
lookupName = strings.TrimSuffix(lookupName, "Singleton")

// If it was a proxy and not a special case, return "Proxy"
if strings.HasSuffix(name, "ProxyAddress") {
return "Proxy"
}

return lookupName
}

func (v *Verifier) getContractArtifact(name string) (*contractArtifact, error) {
artifactName := getArtifactName(name)
artifactPath := path.Join(artifactName+".sol", artifactName+".json")

v.log.Info("Opening artifact", "path", artifactPath, "name", name)
f, err := v.artifactsFS.Open(artifactPath)
if err != nil {
return nil, fmt.Errorf("failed to open artifact: %w", err)
}
defer f.Close()

var art foundry.Artifact
if err := json.NewDecoder(f).Decode(&art); err != nil {
return nil, fmt.Errorf("failed to decode artifact: %w", err)
}

// Add all sources (main contract and dependencies)
sources := make(map[string]SourceContent)
for sourcePath, sourceInfo := range art.Metadata.Sources {
remappedKey := art.SearchRemappings(sourcePath)
sources[remappedKey] = SourceContent{Content: sourceInfo.Content}
v.log.Debug("added source contract", "originalPath", sourcePath, "remappedKey", remappedKey)
}

var optimizer OptimizerSettings
if err := json.Unmarshal(art.Metadata.Settings.Optimizer, &optimizer); err != nil {
return nil, fmt.Errorf("failed to parse optimizer settings: %w", err)
}

standardInput := newStandardInput(sources, optimizer, art.Metadata.Settings.EVMVersion)
standardInputJSON, err := json.Marshal(standardInput)
if err != nil {
return nil, fmt.Errorf("failed to generate standard input: %w", err)
}

// Get the contract name from the compilation target
var contractName string
for contractFile, name := range art.Metadata.Settings.CompilationTarget {
contractName = contractFile + ":" + name
break
}
v.log.Info("contractName", "name", contractName)

constructorArgs, err := v.getEncodedConstructorArgs(name)
if err != nil {
return nil, fmt.Errorf("failed to get constructor args: %w", err)
}
v.log.Debug("constructorArgs", "args", constructorArgs)

return &contractArtifact{
ContractName: contractName,
CompilerVersion: art.Metadata.Compiler.Version,
Optimizer: optimizer,
EVMVersion: art.Metadata.Settings.EVMVersion,
StandardInput: string(standardInputJSON),
ConstructorArgs: constructorArgs,
}, nil
}
Loading