diff --git a/op-chain-ops/foundry/artifact.go b/op-chain-ops/foundry/artifact.go index 70e3085b09618..a3e0a6e07678f 100644 --- a/op-chain-ops/foundry/artifact.go +++ b/op-chain-ops/foundry/artifact.go @@ -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"` @@ -77,7 +78,7 @@ type Metadata struct { Settings struct { // Remappings of the contract imports - Remappings json.RawMessage `json:"remappings"` + Remappings []string `json:"remappings"` // 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"` @@ -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"` } @@ -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 +} diff --git a/op-deployer/cmd/op-deployer/main.go b/op-deployer/cmd/op-deployer/main.go index a3bd8363981fd..2560d6a0c025c 100644 --- a/op-deployer/cmd/op-deployer/main.go +++ b/op-deployer/cmd/op-deployer/main.go @@ -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" @@ -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 diff --git a/op-deployer/pkg/deployer/flags.go b/op-deployer/pkg/deployer/flags.go index 8ef70b77b62f8..81ab036693bbc 100644 --- a/op-deployer/pkg/deployer/flags.go +++ b/op-deployer/pkg/deployer/flags.go @@ -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 @@ -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 ( @@ -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, @@ -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.", @@ -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)...) @@ -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) } diff --git a/op-deployer/pkg/deployer/inspect/l1.go b/op-deployer/pkg/deployer/inspect/l1.go index cabcc997c4377..e8bccb0238ffc 100644 --- a/op-deployer/pkg/deployer/inspect/l1.go +++ b/op-deployer/pkg/deployer/inspect/l1.go @@ -2,6 +2,7 @@ package inspect import ( "fmt" + "reflect" "github.com/ethereum-optimism/optimism/op-chain-ops/genesis" @@ -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, diff --git a/op-deployer/pkg/deployer/verify/artifacts.go b/op-deployer/pkg/deployer/verify/artifacts.go new file mode 100644 index 0000000000000..6c70b5915d34e --- /dev/null +++ b/op-deployer/pkg/deployer/verify/artifacts.go @@ -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 +} diff --git a/op-deployer/pkg/deployer/verify/constructors.go b/op-deployer/pkg/deployer/verify/constructors.go new file mode 100644 index 0000000000000..fa1cdb193d21a --- /dev/null +++ b/op-deployer/pkg/deployer/verify/constructors.go @@ -0,0 +1,331 @@ +package verify + +import ( + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/lmittmann/w3" + "github.com/lmittmann/w3/module/eth" +) + +type constructorArgEncoder func(*Verifier) (string, error) + +var constructorArgEncoders = map[string]constructorArgEncoder{ + "ProxyAdminAddress": encodeProxyAdminArgs, + "OpcmAddress": encodeOpcmArgs, + "DelayedWETHImplAddress": encodeDelayedWETHArgs, + "OptimismPortalImplAddress": encodeOptimismPortalArgs, + "PreimageOracleSingletonAddress": encodePreimageOracleArgs, + "MipsSingletonAddress": encodeMipsArgs, + "SuperchainConfigProxyAddress": encodeSuperchainConfigProxyArgs, + "PermissionedDisputeGameAddress": encodePermissionedDisputeGameArgs, +} + +func (v *Verifier) getEncodedConstructorArgs(contractName string) (string, error) { + encoder, exists := constructorArgEncoders[contractName] + if !exists { + return "", nil + } + return encoder(v) +} + +func encodeProxyAdminArgs(v *Verifier) (string, error) { + addr := v.st.AppliedIntent.SuperchainRoles.ProxyAdminOwner + padded := common.LeftPadBytes(addr.Bytes(), 32) + return hexutil.Encode(padded)[2:], nil +} + +func encodeDelayedWETHArgs(v *Verifier) (string, error) { + var withdrawalDelay big.Int + withdrawalDelayFn := w3.MustNewFunc("delay()", "uint256") + if err := v.w3Client.Call( + eth.CallFunc(v.st.ImplementationsDeployment.DelayedWETHImplAddress, withdrawalDelayFn).Returns(&withdrawalDelay), + ); err != nil { + return "", err + } + paddedDelay := common.LeftPadBytes(withdrawalDelay.Bytes(), 32) + return strings.TrimPrefix(hexutil.Encode(paddedDelay), "0x"), nil +} + +func encodeOptimismPortalArgs(v *Verifier) (string, error) { + var maturityDelay big.Int + proofMaturityDelayFn := w3.MustNewFunc("proofMaturityDelaySeconds()", "uint256") + if err := v.w3Client.Call( + eth.CallFunc(v.st.ImplementationsDeployment.OptimismPortalImplAddress, proofMaturityDelayFn).Returns(&maturityDelay), + ); err != nil { + return "", err + } + + var finalityDelay big.Int + disputeGameFinalityDelayFn := w3.MustNewFunc("disputeGameFinalityDelaySeconds()", "uint256") + if err := v.w3Client.Call( + eth.CallFunc(v.st.ImplementationsDeployment.OptimismPortalImplAddress, disputeGameFinalityDelayFn).Returns(&finalityDelay), + ); err != nil { + return "", err + } + + paddedMaturity := common.LeftPadBytes(maturityDelay.Bytes(), 32) + paddedFinality := common.LeftPadBytes(finalityDelay.Bytes(), 32) + concatenated := append(paddedMaturity, paddedFinality...) + return strings.TrimPrefix(hexutil.Encode(concatenated), "0x"), nil +} + +func encodePreimageOracleArgs(v *Verifier) (string, error) { + var minProposalSize big.Int + minProposalSizeFn := w3.MustNewFunc("minProposalSize()", "uint256") + if err := v.w3Client.Call( + eth.CallFunc(v.st.ImplementationsDeployment.PreimageOracleSingletonAddress, minProposalSizeFn).Returns(&minProposalSize), + ); err != nil { + return "", err + } + + var challengePeriod big.Int + challengePeriodFn := w3.MustNewFunc("challengePeriod()", "uint256") + if err := v.w3Client.Call( + eth.CallFunc(v.st.ImplementationsDeployment.PreimageOracleSingletonAddress, challengePeriodFn).Returns(&challengePeriod), + ); err != nil { + return "", err + } + + paddedMinProposalSize := common.LeftPadBytes(minProposalSize.Bytes(), 32) + paddedChallengePeriod := common.LeftPadBytes(challengePeriod.Bytes(), 32) + concatenated := append(paddedMinProposalSize, paddedChallengePeriod...) + return strings.TrimPrefix(hexutil.Encode(concatenated), "0x"), nil +} + +func encodeMipsArgs(v *Verifier) (string, error) { + addr := v.st.ImplementationsDeployment.PreimageOracleSingletonAddress + padded := common.LeftPadBytes(addr.Bytes(), 32) + return hexutil.Encode(padded)[2:], nil +} + +func encodeOpcmArgs(v *Verifier) (string, error) { + type Blueprints struct { + AddressManager common.Address `abi:"field0"` + Proxy common.Address `abi:"field1"` + ProxyAdmin common.Address `abi:"field2"` + L1ChugSplashProxy common.Address `abi:"field3"` + ResolvedDelegateProxy common.Address `abi:"field4"` + PermissionedDisputeGame1 common.Address `abi:"field5"` + PermissionedDisputeGame2 common.Address `abi:"field6"` + PermissionlessDisputeGame1 common.Address `abi:"field7"` + PermissionlessDisputeGame2 common.Address `abi:"field8"` + } + + type Implementations struct { + SuperchainConfigImpl common.Address `abi:"field0"` + ProtocolVersionsImpl common.Address `abi:"field1"` + L1ERC721BridgeImpl common.Address `abi:"field2"` + OptimismPortalImpl common.Address `abi:"field3"` + SystemConfigImpl common.Address `abi:"field4"` + OptimismMintableERC20FactoryImpl common.Address `abi:"field5"` + L1CrossDomainMessengerImpl common.Address `abi:"field6"` + L1StandardBridgeImpl common.Address `abi:"field7"` + DisputeGameFactoryImpl common.Address `abi:"field8"` + AnchorStateRegistryImpl common.Address `abi:"field9"` + DelayedWETHImpl common.Address `abi:"field10"` + MipsImpl common.Address `abi:"field11"` + } + + var blueprints Blueprints + blueprintsFn := w3.MustNewFunc("blueprints()", "(address addressManager,address proxy,address proxyAdmin,address l1ChugSplashProxy,address resolvedDelegateProxy,address permissionedDisputeGame1,address permissionedDisputeGame2,address permissionlessDisputeGame1,address permissionlessDisputeGame2)") + if err := v.w3Client.Call(eth.CallFunc(v.st.ImplementationsDeployment.OpcmAddress, blueprintsFn).Returns(&blueprints)); err != nil { + return "", err + } + + var impls Implementations + implementationsFn := w3.MustNewFunc("implementations()", "(address superchainConfigImpl,address protocolVersionsImpl,address l1ERC721BridgeImpl,address optimismPortalImpl,address systemConfigImpl,address optimismMintableERC20FactoryImpl,address l1CrossDomainMessengerImpl,address l1StandardBridgeImpl,address disputeGameFactoryImpl,address anchorStateRegistryImpl,address delayedWETHImpl,address mipsImpl)") + if err := v.w3Client.Call(eth.CallFunc(v.st.ImplementationsDeployment.OpcmAddress, implementationsFn).Returns(&impls)); err != nil { + return "", err + } + + var release string + releaseFn := w3.MustNewFunc("l1ContractsRelease()", "string") + if err := v.w3Client.Call(eth.CallFunc(v.st.ImplementationsDeployment.OpcmAddress, releaseFn).Returns(&release)); err != nil { + return "", err + } + + var isRc bool + isRcFn := w3.MustNewFunc("isRC()", "bool") + if err := v.w3Client.Call(eth.CallFunc(v.st.ImplementationsDeployment.OpcmAddress, isRcFn).Returns(&isRc)); err != nil { + return "", err + } + if isRc { + // Opcm code appends the "-rc" suffix, so we need to remove it to recreate the constructor arg + release = strings.TrimSuffix(release, "-rc") + } + + var upgradeController common.Address + upgradeControllerFn := w3.MustNewFunc("upgradeController()", "address") + if err := v.w3Client.Call(eth.CallFunc(v.st.ImplementationsDeployment.OpcmAddress, upgradeControllerFn).Returns(&upgradeController)); err != nil { + return "", err + } + + var superchainConfig common.Address + superchainConfigFn := w3.MustNewFunc("superchainConfig()", "address") + if err := v.w3Client.Call(eth.CallFunc(v.st.ImplementationsDeployment.OpcmAddress, superchainConfigFn).Returns(&superchainConfig)); err != nil { + return "", err + } + + var protocolVersions common.Address + protocolVersionsFn := w3.MustNewFunc("protocolVersions()", "address") + if err := v.w3Client.Call(eth.CallFunc(v.st.ImplementationsDeployment.OpcmAddress, protocolVersionsFn).Returns(&protocolVersions)); err != nil { + return "", err + } + + var superchainProxyAdmin common.Address + superchainProxyAdminFn := w3.MustNewFunc("superchainProxyAdmin()", "address") + if err := v.w3Client.Call(eth.CallFunc(v.st.ImplementationsDeployment.OpcmAddress, superchainProxyAdminFn).Returns(&superchainProxyAdmin)); err != nil { + return "", err + } + + result := []byte{} + result = append(result, common.LeftPadBytes(superchainConfig.Bytes(), 32)...) + result = append(result, common.LeftPadBytes(protocolVersions.Bytes(), 32)...) + result = append(result, common.LeftPadBytes(superchainProxyAdmin.Bytes(), 32)...) + + // Calculate dynamic offset for _l1ContractsRelease. + // 3 addresses + // 1 dynamic offset for _l1ContractsRelease, + // 9 addresses for blueprints + // 12 addresses for implementations + // 1 address for _upgradeController. + // -------------------------------- + // Total: 26 slots + // Offset = 26 * 32 = 832 bytes. + offset := big.NewInt(26 * 32) // 832 + result = append(result, common.LeftPadBytes(offset.Bytes(), 32)...) + + // blueprints + result = append(result, common.LeftPadBytes(blueprints.AddressManager.Bytes(), 32)...) + result = append(result, common.LeftPadBytes(blueprints.Proxy.Bytes(), 32)...) + result = append(result, common.LeftPadBytes(blueprints.ProxyAdmin.Bytes(), 32)...) + result = append(result, common.LeftPadBytes(blueprints.L1ChugSplashProxy.Bytes(), 32)...) + result = append(result, common.LeftPadBytes(blueprints.ResolvedDelegateProxy.Bytes(), 32)...) + result = append(result, common.LeftPadBytes(blueprints.PermissionedDisputeGame1.Bytes(), 32)...) + result = append(result, common.LeftPadBytes(blueprints.PermissionedDisputeGame2.Bytes(), 32)...) + result = append(result, common.LeftPadBytes(blueprints.PermissionlessDisputeGame1.Bytes(), 32)...) + result = append(result, common.LeftPadBytes(blueprints.PermissionlessDisputeGame2.Bytes(), 32)...) + + // implementations + result = append(result, common.LeftPadBytes(impls.SuperchainConfigImpl.Bytes(), 32)...) + result = append(result, common.LeftPadBytes(impls.ProtocolVersionsImpl.Bytes(), 32)...) + result = append(result, common.LeftPadBytes(impls.L1ERC721BridgeImpl.Bytes(), 32)...) + result = append(result, common.LeftPadBytes(impls.OptimismPortalImpl.Bytes(), 32)...) + result = append(result, common.LeftPadBytes(impls.SystemConfigImpl.Bytes(), 32)...) + result = append(result, common.LeftPadBytes(impls.OptimismMintableERC20FactoryImpl.Bytes(), 32)...) + result = append(result, common.LeftPadBytes(impls.L1CrossDomainMessengerImpl.Bytes(), 32)...) + result = append(result, common.LeftPadBytes(impls.L1StandardBridgeImpl.Bytes(), 32)...) + result = append(result, common.LeftPadBytes(impls.DisputeGameFactoryImpl.Bytes(), 32)...) + result = append(result, common.LeftPadBytes(impls.AnchorStateRegistryImpl.Bytes(), 32)...) + result = append(result, common.LeftPadBytes(impls.DelayedWETHImpl.Bytes(), 32)...) + result = append(result, common.LeftPadBytes(impls.MipsImpl.Bytes(), 32)...) + + // upgrade controller + result = append(result, common.LeftPadBytes(upgradeController.Bytes(), 32)...) + + // l1ContractsRelease (dynamic args appended to the end) + releaseBytes := []byte(release) + result = append(result, common.LeftPadBytes(big.NewInt(int64(len(releaseBytes))).Bytes(), 32)...) + result = append(result, common.RightPadBytes(releaseBytes, (len(releaseBytes)+31)/32*32)...) + + return strings.TrimPrefix(hexutil.Encode(result), "0x"), nil +} + +func encodeSuperchainConfigProxyArgs(v *Verifier) (string, error) { + addr := v.st.SuperchainDeployment.ProxyAdminAddress + padded := common.LeftPadBytes(addr.Bytes(), 32) + return strings.TrimPrefix(hexutil.Encode(padded), "0x"), nil +} + +func encodePermissionedDisputeGameArgs(v *Verifier) (string, error) { + chainState, err := v.st.Chain(v.l2ChainID) + if err != nil { + return "", err + } + addr := chainState.PermissionedDisputeGameAddress + result := []byte{} + + var gameType uint32 + gameTypeFn := w3.MustNewFunc("gameType()", "uint32") + if err := v.w3Client.Call(eth.CallFunc(addr, gameTypeFn).Returns(&gameType)); err != nil { + return "", err + } + result = append(result, common.LeftPadBytes(big.NewInt(int64(gameType)).Bytes(), 32)...) + + var absolutePrestate [32]byte + absolutePrestateFn := w3.MustNewFunc("absolutePrestate()", "bytes32") + if err := v.w3Client.Call(eth.CallFunc(addr, absolutePrestateFn).Returns(&absolutePrestate)); err != nil { + return "", err + } + result = append(result, absolutePrestate[:]...) + + var maxGameDepth big.Int + maxGameDepthFn := w3.MustNewFunc("maxGameDepth()", "uint256") + if err := v.w3Client.Call(eth.CallFunc(addr, maxGameDepthFn).Returns(&maxGameDepth)); err != nil { + return "", err + } + result = append(result, common.LeftPadBytes(maxGameDepth.Bytes(), 32)...) + + var splitDepth big.Int + splitDepthFn := w3.MustNewFunc("splitDepth()", "uint256") + if err := v.w3Client.Call(eth.CallFunc(addr, splitDepthFn).Returns(&splitDepth)); err != nil { + return "", err + } + result = append(result, common.LeftPadBytes(splitDepth.Bytes(), 32)...) + + var clockExtension uint64 + clockExtensionFn := w3.MustNewFunc("clockExtension()", "uint64") + if err := v.w3Client.Call(eth.CallFunc(addr, clockExtensionFn).Returns(&clockExtension)); err != nil { + return "", err + } + result = append(result, common.LeftPadBytes(big.NewInt(int64(clockExtension)).Bytes(), 32)...) + + var maxClockDuration uint64 + maxClockDurationFn := w3.MustNewFunc("maxClockDuration()", "uint64") + if err := v.w3Client.Call(eth.CallFunc(addr, maxClockDurationFn).Returns(&maxClockDuration)); err != nil { + return "", err + } + result = append(result, common.LeftPadBytes(big.NewInt(int64(maxClockDuration)).Bytes(), 32)...) + var vm common.Address + vmFn := w3.MustNewFunc("vm()", "address") + if err := v.w3Client.Call(eth.CallFunc(addr, vmFn).Returns(&vm)); err != nil { + return "", err + } + result = append(result, common.LeftPadBytes(vm.Bytes(), 32)...) + + var weth common.Address + wethFn := w3.MustNewFunc("weth()", "address") + if err := v.w3Client.Call(eth.CallFunc(addr, wethFn).Returns(&weth)); err != nil { + return "", err + } + result = append(result, common.LeftPadBytes(weth.Bytes(), 32)...) + + var anchorStateRegistry common.Address + anchorStateRegistryFn := w3.MustNewFunc("anchorStateRegistry()", "address") + if err := v.w3Client.Call(eth.CallFunc(addr, anchorStateRegistryFn).Returns(&anchorStateRegistry)); err != nil { + return "", err + } + result = append(result, common.LeftPadBytes(anchorStateRegistry.Bytes(), 32)...) + + var l2ChainId big.Int + l2ChainIdFn := w3.MustNewFunc("l2ChainId()", "uint256") + if err := v.w3Client.Call(eth.CallFunc(addr, l2ChainIdFn).Returns(&l2ChainId)); err != nil { + return "", err + } + result = append(result, common.LeftPadBytes(l2ChainId.Bytes(), 32)...) + + chainIntent, err := v.st.AppliedIntent.Chain(v.l2ChainID) + if err != nil { + return "", err + } + proposer := chainIntent.Roles.Proposer + result = append(result, common.LeftPadBytes(proposer.Bytes(), 32)...) + + challenger := chainIntent.Roles.Challenger + result = append(result, common.LeftPadBytes(challenger.Bytes(), 32)...) + + return strings.TrimPrefix(hexutil.Encode(result), "0x"), nil +} diff --git a/op-deployer/pkg/deployer/verify/etherscan.go b/op-deployer/pkg/deployer/verify/etherscan.go new file mode 100644 index 0000000000000..9f1a5e5b31c20 --- /dev/null +++ b/op-deployer/pkg/deployer/verify/etherscan.go @@ -0,0 +1,235 @@ +package verify + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "github.com/ethereum/go-ethereum/common" +) + +type EtherscanResponse struct { + Status string `json:"status"` + Message string `json:"message"` + Result string `json:"result"` +} + +func getAPIEndpoint(chainID uint64) string { + switch chainID { + case 1: + return "https://api.etherscan.io/api" // mainnet + case 11155111: + return "https://api-sepolia.etherscan.io/api" // sepolia + default: + return "" + } +} + +func (v *Verifier) verifyContract(address common.Address, contractName string) error { + verified, err := v.isVerified(address) + if err != nil { + return fmt.Errorf("failed to check verification status: %w", err) + } + if verified { + v.log.Info("Contract is already verified", "name", contractName, "address", address.Hex()) + v.numSkipped++ + return nil + } + + v.log.Info("Formatting etherscan verification request", "name", contractName, "address", address.Hex()) + source, err := v.getContractArtifact(contractName) + if err != nil { + return fmt.Errorf("failed to get contract source: %w", err) + } + + optimized := "0" + if source.Optimizer.Enabled { + optimized = "1" + } + + data := url.Values{ + "apikey": {v.apiKey}, + "module": {"contract"}, + "action": {"verifysourcecode"}, + "contractaddress": {address.Hex()}, + "codeformat": {"solidity-standard-json-input"}, + "sourceCode": {source.StandardInput}, + "contractname": {source.ContractName}, + "compilerversion": {fmt.Sprintf("v%s", source.CompilerVersion)}, + "optimizationUsed": {optimized}, + "runs": {fmt.Sprintf("%d", source.Optimizer.Runs)}, + "evmversion": {source.EVMVersion}, + "constructorArguements": {source.ConstructorArgs}, + } + + req, err := http.NewRequest("POST", v.etherscanUrl, strings.NewReader(data.Encode())) + if err != nil { + return fmt.Errorf("failed to create verification request: %w", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + resp, err := v.sendRateLimitedRequest(req) + if err != nil { + return fmt.Errorf("failed to submit verification request: %w", err) + } + defer resp.Body.Close() + + var result EtherscanResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return fmt.Errorf("failed to decode response: %w", err) + } + + if result.Status != "1" { + return fmt.Errorf("verification request failed: status=%s message=%s result=%s", + result.Status, result.Message, result.Result) + } + v.log.Info("Verification request submitted", "name", contractName, "address", address.Hex()) + err = v.checkVerificationStatus(result.Result) + if err == nil { + v.log.Info("Verification complete", "name", contractName, "address", address.Hex()) + v.numVerified++ + } + return err +} + +// sendRateLimitedRequest is a helper function which waits for a rate limit token +// before sending a request +func (v *Verifier) sendRateLimitedRequest(req *http.Request) (*http.Response, error) { + if err := v.rateLimiter.Wait(context.Background()); err != nil { + return nil, fmt.Errorf("rate limiter error: %w", err) + } + + return http.DefaultClient.Do(req) +} + +func (v *Verifier) isVerified(address common.Address) (bool, error) { + req, err := http.NewRequest("GET", fmt.Sprintf("%s?module=contract&action=getabi&address=%s&apikey=%s", + v.etherscanUrl, address.Hex(), v.apiKey), nil) + if err != nil { + return false, err + } + + resp, err := v.sendRateLimitedRequest(req) + if err != nil { + return false, err + } + defer resp.Body.Close() + + var result EtherscanResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return false, err + } + + v.log.Debug("Contract verification status", "status", result.Status, "message", result.Message) + return result.Status == "1", nil +} + +func (v *Verifier) checkVerificationStatus(reqId string) error { + req, err := http.NewRequest("GET", fmt.Sprintf("%s?apikey=%s&module=contract&action=checkverifystatus&guid=%s", + v.etherscanUrl, v.apiKey, reqId), nil) + if err != nil { + return fmt.Errorf("failed to create checkverifystatus request: %w", err) + } + + for i := 0; i < 10; i++ { // Try 10 times with increasing delays + v.log.Info("Checking verification status", "guid", reqId) + time.Sleep(time.Duration(i+2) * time.Second) + + resp, err := v.sendRateLimitedRequest(req) + if err != nil { + return fmt.Errorf("failed to send checkverifystatus request: %w", err) + } + defer resp.Body.Close() + + var result EtherscanResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return fmt.Errorf("failed to decode checkverifystatus response: %w", err) + } + + if result.Status == "1" { + return nil + } + if result.Result == "Already Verified" { + v.log.Info("Contract is already verified") + return nil + } + if result.Result != "Pending in queue" { + return fmt.Errorf("verification failed: %s, %s", result.Result, result.Message) + } + } + return fmt.Errorf("verification timed out") +} + +type StandardInput struct { + Language string `json:"language"` + Sources map[string]SourceContent `json:"sources"` + Settings Settings `json:"settings"` +} + +type SourceContent struct { + Content string `json:"content"` +} + +type Settings struct { + Optimizer OptimizerSettings `json:"optimizer"` + EVMVersion string `json:"evmVersion"` + Metadata MetadataSettings `json:"metadata"` + OutputSelection OutputSelection `json:"outputSelection"` +} + +type OptimizerSettings struct { + Enabled bool `json:"enabled"` + Runs int `json:"runs"` +} + +type MetadataSettings struct { + UseLiteralContent bool `json:"useLiteralContent"` + BytecodeHash string `json:"bytecodeHash"` +} + +type OutputSelection struct { + All map[string]OutputSelectionDetails `json:"*"` +} + +type OutputSelectionDetails struct { + All []string `json:"*"` +} + +func newStandardInput( + sources map[string]SourceContent, + optimizer OptimizerSettings, + evmVersion string, +) StandardInput { + return StandardInput{ + Language: "Solidity", + Sources: sources, + Settings: Settings{ + Optimizer: OptimizerSettings{ + Enabled: optimizer.Enabled, + Runs: optimizer.Runs, + }, + EVMVersion: evmVersion, + Metadata: MetadataSettings{ + UseLiteralContent: true, + BytecodeHash: "none", + }, + OutputSelection: OutputSelection{ + All: map[string]OutputSelectionDetails{ + "*": { + All: []string{ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "metadata", + }, + }, + }, + }, + }, + } +} diff --git a/op-deployer/pkg/deployer/verify/verifier.go b/op-deployer/pkg/deployer/verify/verifier.go new file mode 100644 index 0000000000000..99d0aad648aae --- /dev/null +++ b/op-deployer/pkg/deployer/verify/verifier.go @@ -0,0 +1,201 @@ +package verify + +import ( + "context" + "fmt" + "reflect" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "github.com/lmittmann/w3" + "github.com/lmittmann/w3/module/eth" + "github.com/urfave/cli/v2" + "golang.org/x/time/rate" + + "github.com/ethereum-optimism/optimism/op-chain-ops/foundry" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/inspect" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/pipeline" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/state" + op_service "github.com/ethereum-optimism/optimism/op-service" + "github.com/ethereum-optimism/optimism/op-service/ctxinterrupt" + oplog "github.com/ethereum-optimism/optimism/op-service/log" +) + +type Verifier struct { + apiKey string + l1ChainID uint64 + l2ChainID common.Hash + st *state.State + artifactsFS foundry.StatDirFs + log log.Logger + etherscanUrl string + rateLimiter *rate.Limiter + w3Client *w3.Client + numVerified int + numSkipped int +} + +func NewVerifier(apiKey string, l1ChainID uint64, l2ChainID common.Hash, st *state.State, artifactsFS foundry.StatDirFs, l log.Logger, w3Client *w3.Client) (*Verifier, error) { + etherscanUrl := getAPIEndpoint(l1ChainID) + if etherscanUrl == "" { + return nil, fmt.Errorf("unsupported L1 chain ID: %d", l1ChainID) + } + + if l2ChainID == (common.Hash{}) { + l2ChainID = st.AppliedIntent.Chains[0].ID + } + + return &Verifier{ + apiKey: apiKey, + l1ChainID: l1ChainID, + l2ChainID: l2ChainID, + st: st, + artifactsFS: artifactsFS, + log: l, + etherscanUrl: etherscanUrl, + rateLimiter: rate.NewLimiter(rate.Limit(3), 2), + w3Client: w3Client, + }, nil +} + +func VerifyCLI(cliCtx *cli.Context) error { + logCfg := oplog.ReadCLIConfig(cliCtx) + l := oplog.NewLogger(oplog.AppOut(cliCtx), logCfg) + oplog.SetGlobalLogHandler(l.Handler()) + + l1RPCUrl := cliCtx.String(deployer.L1RPCURLFlagName) + workdir := cliCtx.String(deployer.WorkdirFlagName) + etherscanAPIKey := cliCtx.String(deployer.EtherscanAPIKeyFlagName) + bundleName := cliCtx.String(deployer.ContractBundleFlagName) + contractName := cliCtx.String(deployer.ContractNameFlagName) + l2ChainIDRaw := cliCtx.String(deployer.L2ChainIDFlagName) + + var l2ChainID common.Hash + var err error + if l2ChainIDRaw != "" { + l2ChainID, err = op_service.Parse256BitChainID(l2ChainIDRaw) + if err != nil { + return fmt.Errorf("invalid L2 chain ID '%s': %w", l2ChainIDRaw, err) + } + } + + ctx := ctxinterrupt.WithCancelOnInterrupt(cliCtx.Context) + + w3Client, err := w3.Dial(l1RPCUrl) + if err != nil { + return fmt.Errorf("failed to connect to L1: %w", err) + } + defer w3Client.Close() + + var l1ChainId uint64 + if err := w3Client.Call(eth.ChainID().Returns(&l1ChainId)); err != nil { + return fmt.Errorf("failed to get chain ID: %w", err) + } + + st, err := pipeline.ReadState(workdir) + if err != nil { + return fmt.Errorf("failed to read state: %w", err) + } + + if l1ChainId != st.AppliedIntent.L1ChainID { + return fmt.Errorf("rpc l1 chain ID does not match state l1 chain ID: %d != %d", l1ChainId, st.AppliedIntent.L1ChainID) + } + + artifactsFS, err := artifacts.Download(ctx, st.AppliedIntent.L1ContractsLocator, nil, deployer.DefaultCacheDir) + if err != nil { + return fmt.Errorf("failed to get artifacts: %w", err) + } + l.Info("Downloaded artifacts", "path", artifactsFS) + + v, err := NewVerifier(etherscanAPIKey, l1ChainId, l2ChainID, st, artifactsFS, l, w3Client) + if err != nil { + return fmt.Errorf("failed to create verifier: %w", err) + } + + defer func() { + v.log.Info("final results", "numVerified", v.numVerified, "numSkipped", v.numSkipped) + }() + + if bundleName == "" && contractName == "" { + if err := v.verifyAll(ctx); err != nil { + return err + } + } else if bundleName != "" && contractName == "" { + if err := v.verifyContractBundle(bundleName); err != nil { + return err + } + } else if bundleName != "" && contractName != "" { + if err := v.verifySingleContract(ctx, contractName, bundleName); err != nil { + return err + } + } else { + // If a contract name is provided without a contract bundle, report an error. + return fmt.Errorf("contract-name flag provided without contract-bundle flag") + } + v.log.Info("--- SUCCESS ---") + return nil +} + +func (v *Verifier) verifyAll(ctx context.Context) error { + for _, bundleName := range inspect.ContractBundles { + if err := v.verifyContractBundle(bundleName); err != nil { + return fmt.Errorf("failed to verify bundle %s: %w", bundleName, err) + } + } + return nil +} + +func (v *Verifier) verifyContractBundle(bundleName string) error { + // Retrieve the L1 contracts from state. + l1Contracts, err := inspect.L1(v.st, v.l2ChainID) + if err != nil { + return fmt.Errorf("failed to extract L1 contracts from state: %w", err) + } + + // Select the appropriate bundle based on the input bundleName. + var bundle interface{} + switch bundleName { + case inspect.SuperchainBundle: + bundle = l1Contracts.SuperchainDeployment + case inspect.ImplementationsBundle: + bundle = l1Contracts.ImplementationsDeployment + case inspect.OpChainBundle: + bundle = l1Contracts.OpChainDeployment + default: + return fmt.Errorf("invalid contract bundle: %s", bundleName) + } + + // Use reflection to iterate over fields of the bundle. + val := reflect.ValueOf(bundle) + typ := val.Type() + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + if field.Type() == reflect.TypeOf(common.Address{}) { + addr := field.Interface().(common.Address) + if addr != (common.Address{}) { // Skip zero addresses + name := typ.Field(i).Name + if err := v.verifyContract(addr, name); err != nil { + return fmt.Errorf("failed to verify %s: %w", name, err) + } + } + } + } + return nil +} + +func (v *Verifier) verifySingleContract(ctx context.Context, contractName string, bundleName string) error { + l1Contracts, err := inspect.L1(v.st, v.l2ChainID) + if err != nil { + return fmt.Errorf("failed to extract L1 contracts from state: %w", err) + } + + v.log.Info("Looking up contract address", "name", contractName, "bundle", bundleName) + addr, err := l1Contracts.GetContractAddress(contractName, bundleName) + if err != nil { + return fmt.Errorf("failed to find address for contract %s: %w", contractName, err) + } + + return v.verifyContract(addr, contractName) +} diff --git a/packages/contracts-bedrock/foundry.toml b/packages/contracts-bedrock/foundry.toml index aafcc6c22b3d6..7f94d2bc88559 100644 --- a/packages/contracts-bedrock/foundry.toml +++ b/packages/contracts-bedrock/foundry.toml @@ -12,6 +12,7 @@ snapshots = 'notarealpath' # workaround for foundry#9477 optimizer = true optimizer_runs = 999999 +use_literal_content = true # IMPORTANT: # When adding any new compiler profiles or compilation restrictions, you must