diff --git a/.prettierignore b/.prettierignore index 38a630a2a..b13b94b60 100644 --- a/.prettierignore +++ b/.prettierignore @@ -47,4 +47,5 @@ yarn.lock packages/generated/* +scripts/bytecode-diff/templates/* packages/proto/src/protocol_*.ts diff --git a/scripts/bytecode-diff/README.md b/scripts/bytecode-diff/README.md index 245f1a888..0f18dd67c 100644 --- a/scripts/bytecode-diff/README.md +++ b/scripts/bytecode-diff/README.md @@ -83,6 +83,28 @@ diamonds: ``` +### Run keccak256 hash generation on deployed contracts + +```bash +GOWORK=off go run main.go add-hashes gamma deployed-diffs/facet_diff_090624_1.yaml + +# output to new yaml file suffixed with _hashed.yaml including bytecodeHash for each contract in deployments section +➜ bytecode-diff git:(jt/net-62-upgrade-script-2) ✗ yq e '.deployments' deployed-diffs/facet_diff_090624_1_hashed.yaml +Architect: + address: 0xa18a3df4f63cdcae943d9c76730adf2812388de4 + baseScanLink: https://sepolia.basescan.org/tx/0x4280ef1300fe001e7d85e7495eba13fc99be53ee7a7060e753d466f8bebf1622 + bytecodeHash: 0x20d0a86e9ea31a39663285aacfe88705983520a4482a7bac5ada891c9adfe090 + deploymentDate: 2024-09-06 19:04 + transactionHash: 0x4280ef1300fe001e7d85e7495eba13fc99be53ee7a7060e753d466f8bebf1622 +Banning: + address: 0x4d88d1fbba6ce6bcdb4381549ee0b7c0d2b56919 + baseScanLink: https://sepolia.basescan.org/tx/0x4ccbaf9750bcd0971975e73a24b05f1c51d4703cf72a406356c79eb54de9c33c + bytecodeHash: 0xa2ce3e77ba060ff1d59ed384e1c6c5788f308ad8bbbef612eb3e5de4e1d79de8 + deploymentDate: 2024-09-06 19:05 + transactionHash: 0x4ccbaf9750bcd0971975e73a24b05f1c51d4703cf72a406356c79eb54de9c33c +... +``` + ### Flags ```bash diff --git a/scripts/bytecode-diff/cmd/root.go b/scripts/bytecode-diff/cmd/root.go new file mode 100644 index 000000000..03d92d94a --- /dev/null +++ b/scripts/bytecode-diff/cmd/root.go @@ -0,0 +1,370 @@ +package cmd + +import ( + "bytecode-diff/utils" + "fmt" + "os" + "path/filepath" + + "github.com/joho/godotenv" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +var ( + supportedEnvironments = []string{"alpha", "gamma", "omega"} + baseRpcUrl string + facetSourcePath string + compiledFacetsPath string + sourceDiffDir string + sourceDiff bool + reportOutDir string + originEnvironment string + targetEnvironment string + deploymentsPath string + baseSepoliaRpcUrl string + logLevel string +) + +func init() { + log := zerolog.New(os.Stderr).With().Timestamp().Logger() + utils.SetLogger(log) + + rootCmd.PersistentFlags(). + StringVar(&logLevel, "log-level", "info", "Set the logging level (debug, info, warn, error)") + rootCmd.Flags().StringVarP(&baseRpcUrl, "base-rpc", "b", "", "Base RPC provider URL") + rootCmd.Flags().StringVarP(&baseSepoliaRpcUrl, "base-sepolia-rpc", "", "", "Base Sepolia RPC provider URL") + rootCmd.Flags().BoolVarP(&sourceDiff, "source-diff-only", "s", false, "Run source code diff") + rootCmd.Flags().StringVar(&sourceDiffDir, "source-diff-log", "source-diffs", "Path to diff log file") + rootCmd.Flags().StringVar(&compiledFacetsPath, "compiled-facets", "../../contracts/out", "Path to compiled facets") + rootCmd.Flags().StringVar(&facetSourcePath, "facets", "", "Path to facet source files") + rootCmd.Flags().BoolP("verbose", "v", false, "Enable verbose output") + rootCmd.Flags().StringVar(&reportOutDir, "report-out-dir", "deployed-diffs", "Path to report output directory") + rootCmd.Flags(). + StringVar(&deploymentsPath, "deployments", "../../contracts/deployments", "Path to deployments directory") + + rootCmd.AddCommand(AddHashesCmd) +} + +func setLogLevel(level string) { + switch level { + case "debug": + zerolog.SetGlobalLevel(zerolog.DebugLevel) + case "info": + zerolog.SetGlobalLevel(zerolog.InfoLevel) + case "warn": + zerolog.SetGlobalLevel(zerolog.WarnLevel) + case "error": + zerolog.SetGlobalLevel(zerolog.ErrorLevel) + default: + zerolog.SetGlobalLevel(zerolog.InfoLevel) + } +} + +func Execute() { + if err := godotenv.Load(); err != nil { + log.Warn().Msg("No .env file found") + } + + if err := rootCmd.Execute(); err != nil { + log.Error().Err(err).Msg("Error executing root command") + os.Exit(1) + } +} + +var rootCmd = &cobra.Command{ + Use: "bytecode-diff [origin_environment] [target_environment]", + Short: "A tool to retrieve and display contract bytecode diff for Base", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + setLogLevel(logLevel) + }, + Args: func(cmd *cobra.Command, args []string) error { + if sourceDiff { + if len(args) != 0 { + return fmt.Errorf("no positional arguments expected when --source-diff-only is set") + } + } else { + if len(args) < 2 { + return fmt.Errorf("at least two arguments required when --source-diff-only is not set, [origin_environment], [target_environment]") + } + } + return nil + }, + PreRun: func(cmd *cobra.Command, args []string) { + if sourceDiff { + envSourceDiffDir := os.Getenv("SOURCE_DIFF_DIR") + if envSourceDiffDir != "" { + sourceDiffDir = envSourceDiffDir + } + + if sourceDiffDir == "" { + sourceDiffDir = cmd.Flag("source-diff-log").Value.String() + } + + facetSourcePath = os.Getenv("FACET_SOURCE_PATH") + if facetSourcePath == "" { + facetSourcePath = cmd.Flag("facets").Value.String() + } + if facetSourcePath == "" { + log.Fatal(). + Msg("Facet source path is missing. Set it using --facets flag or FACET_SOURCE_PATH environment variable") + } + + compiledFacetsPath = os.Getenv("COMPILED_FACETS_PATH") + log.Debug().Str("compiledFacetsPath", compiledFacetsPath).Msg("Compiled facets path from environment") + if compiledFacetsPath == "" { + compiledFacetsPath = cmd.Flag("compiled-facets").Value.String() + log.Debug().Str("compiledFacetsPath", compiledFacetsPath).Msg("Compiled facets path from flag") + } + if compiledFacetsPath == "" { + log.Fatal(). + Msg("Compiled facets path is missing. Set it using --compiled-facets flag or COMPILED_FACETS_PATH environment variable") + } + + envReportOutDir := os.Getenv("REPORT_OUT_DIR") + if envReportOutDir != "" { + reportOutDir = envReportOutDir + } + if reportOutDir == "" { + reportOutDir = cmd.Flag("report-out-dir").Value.String() + } + if reportOutDir == "" { + log.Fatal(). + Msg("Report out directory is missing. Set it using --report-out-dir flag or REPORT_OUT_DIR environment variable") + } + return + } + + envDeploymentsPath := os.Getenv("DEPLOYMENTS_PATH") + if envDeploymentsPath != "" { + deploymentsPath = envDeploymentsPath + } + if deploymentsPath == "" { + deploymentsPath = cmd.Flag("deployments").Value.String() + } + if deploymentsPath == "" { + log.Fatal(). + Msg("Deployments path is missing. Set it using --deployments flag or DEPLOYMENTS_PATH environment variable") + } + }, + Run: func(cmd *cobra.Command, args []string) { + verbose, _ := cmd.Flags().GetBool("verbose") + if sourceDiff { + + log.Info(). + Str("facetSourcePath", facetSourcePath). + Str("compiledFacetsPath", compiledFacetsPath). + Msg("Running diff for facet path recursively only compiled facet contracts") + + if err := executeSourceDiff(verbose, facetSourcePath, compiledFacetsPath, sourceDiffDir); err != nil { + log.Fatal().Err(err).Msg("Error executing source diff") + return + } + } else { + + originEnvironment, targetEnvironment = args[0], args[1] + for _, environment := range []string{originEnvironment, targetEnvironment} { + if !utils.Contains(supportedEnvironments, environment) { + log.Fatal().Str("environment", environment).Msg("Environment not supported. Environment can be one of alpha, gamma, or omega.") + } + } + + log.Info().Str("originEnvironment", originEnvironment).Str("targetEnvironment", targetEnvironment).Msg("Environment") + + if baseRpcUrl == "" { + baseRpcUrl = os.Getenv("BASE_RPC_URL") + if baseRpcUrl == "" { + log.Fatal().Msg("Base RPC URL not provided. Set it using --base-rpc flag or BASE_RPC_URL environment variable") + } + } + + if baseSepoliaRpcUrl == "" { + baseSepoliaRpcUrl = os.Getenv("BASE_SEPOLIA_RPC_URL") + if baseSepoliaRpcUrl == "" { + log.Fatal().Msg("Base Sepolia RPC URL not provided. Set it using --base-sepolia-rpc flag or BASE_SEPOLIA_RPC_URL environment variable") + } + } + + basescanAPIKey := os.Getenv("BASESCAN_API_KEY") + if basescanAPIKey == "" { + log.Fatal().Msg("BaseScan API key not provided. Set it using BASESCAN_API_KEY environment variable") + } + + log.Info().Str("originEnvironment", originEnvironment).Str("targetEnvironment", targetEnvironment).Msg("Running diff for environment") + // Create BaseConfig struct + baseConfig := utils.BaseConfig{ + BaseRpcUrl: baseRpcUrl, + BaseSepoliaRpcUrl: baseSepoliaRpcUrl, + BasescanAPIKey: basescanAPIKey, + } + + if err := executeEnvrionmentDiff(verbose, baseConfig, deploymentsPath, originEnvironment, targetEnvironment, reportOutDir); err != nil { + log.Fatal().Err(err).Msg("Error executing environment diff") + } + } + }, +} + +func executeSourceDiff(verbose bool, facetSourcePath, compiledFacetsPath string, reportOutDir string) error { + facetFiles, err := utils.GetFacetFiles(facetSourcePath) + if err != nil { + log.Error(). + Str("facetSourcePath", facetSourcePath). + Str("compiledFacetsPath", compiledFacetsPath). + Err(err). + Msg("Error getting facet files") + return err + } + log.Debug().Int("facetFilesCount", len(facetFiles)).Msg("Facet files length") + + compiledHashes, err := utils.GetCompiledFacetHashes(compiledFacetsPath, facetFiles) + if err != nil { + log.Error(). + Err(err). + Str("compiledFacetsPath", compiledFacetsPath). + Msg("Error getting compiled facet hashes") + return err + } + + if verbose { + log.Info().Int("compiledHashesCount", len(compiledHashes)).Msg("Compiled Facet Hashes") + for file, hash := range compiledHashes { + log.Info().Str("file", file).Str("hash", hash).Msg("Compiled Facet Hash") + } + } + + err = utils.CreateFacetHashesReport(compiledFacetsPath, compiledHashes, reportOutDir, verbose) + if err != nil { + log.Error().Err(err).Msg("Error creating facet hashes report") + return err + } + + return nil +} + +func executeEnvrionmentDiff( + verbose bool, + baseConfig utils.BaseConfig, + deploymentsPath, originEnvironment, targetEnvironment string, + reportOutDir string, +) error { + // walk environment diamonds and get all facet addresses from DiamondLoupe facet view + baseDiamonds := []utils.Diamond{ + utils.BaseRegistry, + utils.Space, + utils.SpaceFactory, + utils.SpaceOwner, + } + originDeploymentsPath := filepath.Join(deploymentsPath, originEnvironment) + originDiamonds, err := utils.GetDiamondAddresses(originDeploymentsPath, baseDiamonds, verbose) + if err != nil { + log.Error().Err(err).Msgf("Error getting diamond addresses for origin environment %s", originEnvironment) + return err + } + targetDeploymentsPath := filepath.Join(deploymentsPath, targetEnvironment) + targetDiamonds, err := utils.GetDiamondAddresses(targetDeploymentsPath, baseDiamonds, verbose) + if err != nil { + log.Error().Err(err).Msgf("Error getting diamond addresses for target environment %s", targetEnvironment) + return err + } + // Create Ethereum client + clients, err := utils.CreateEthereumClients( + baseConfig.BaseRpcUrl, + baseConfig.BaseSepoliaRpcUrl, + originEnvironment, + targetEnvironment, + verbose, + ) + defer func() { + for _, client := range clients { + client.Close() + } + }() + // getCode for all facet addresses over base rpc url and compare with compiled hashes + originFacets := make(map[string][]utils.Facet) + + for diamondName, diamondAddress := range originDiamonds { + if verbose { + log.Info(). + Str("diamondName", fmt.Sprintf("%s", diamondName)). + Str("diamondAddress", diamondAddress). + Msg("Origin Diamond Address") + } + facets, err := utils.ReadAllFacets(clients[originEnvironment], diamondAddress, baseConfig.BasescanAPIKey) + if err != nil { + log.Error().Err(err).Msgf("Error reading all facets for origin diamond %s", diamondName) + return err + } + err = utils.AddContractCodeHashes(clients[originEnvironment], facets) + if err != nil { + log.Error().Err(err).Msgf("Error adding contract code hashes for origin diamond %s", diamondName) + return err + } + originFacets[string(diamondName)] = facets + } + + targetFacets := make(map[string][]utils.Facet) + for diamondName, diamondAddress := range targetDiamonds { + facets, err := utils.ReadAllFacets(clients[targetEnvironment], diamondAddress, baseConfig.BasescanAPIKey) + if err != nil { + log.Error().Err(err).Msgf("Error reading all facets for target diamond %s", diamondName) + return err + } + err = utils.AddContractCodeHashes(clients[targetEnvironment], facets) + if err != nil { + log.Error().Err(err).Msgf("Error adding contract code hashes for target diamond %s", diamondName) + return err + } + targetFacets[string(diamondName)] = facets + } + if verbose { + for diamondName, facets := range originFacets { + log.Info().Str("diamondName", diamondName).Msg("Origin Facets for Diamond contract") + for _, facet := range facets { + log.Info(). + Str("facetAddress", facet.FacetAddress.Hex()). + Str("contractName", facet.ContractName). + Interface("selectors", facet.SelectorsHex). + Msg("Facet") + } + } + for diamondName, facets := range targetFacets { + log.Info().Str("diamondName", diamondName).Msg("Target Facets for Diamond contract") + for _, facet := range facets { + log.Info(). + Str("facetAddress", facet.FacetAddress.Hex()). + Str("contractName", facet.ContractName). + Interface("selectors", facet.SelectorsHex). + Msg("Facet") + } + } + } + + // compare facets and create report + differences := utils.CompareFacets(originFacets, targetFacets) + if verbose { + for diamondName, facets := range differences { + log.Info().Str("diamondName", diamondName).Msg("Differences for Diamond contract") + for _, facet := range facets { + log.Info(). + Str("facetAddress", facet.OriginContractAddress.Hex()). + Str("originContractName", facet.OriginContractName). + Msg("Origin Facet") + log.Info(). + Interface("selectorDiff", facet.SelectorsDiff). + Msg("Selector Diff") + + } + } + } + + // create report + log.Info().Str("reportOutDir", reportOutDir).Msg("Generating YAML report") + err = utils.GenerateYAMLReport(originEnvironment, targetEnvironment, differences, reportOutDir) + if err != nil { + log.Error().Err(err).Msg("Error generating YAML report") + return err + } + return nil +} diff --git a/scripts/bytecode-diff/cmd/run_util.go b/scripts/bytecode-diff/cmd/run_util.go new file mode 100644 index 000000000..cb6211048 --- /dev/null +++ b/scripts/bytecode-diff/cmd/run_util.go @@ -0,0 +1,175 @@ +package cmd + +import ( + "bytecode-diff/utils" + "fmt" + "html/template" + "os" + "path/filepath" + "strings" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" +) + +var htmlRender bool + +func getIncrementedFileName(basePath string, extension string) string { + dir := filepath.Dir(basePath) + fileName := filepath.Base(basePath[:len(basePath)-len(filepath.Ext(basePath))]) + + for i := 1; ; i++ { + newFileName := fmt.Sprintf("%s_hashed_%d%s", fileName, i, extension) + fullPath := filepath.Join(dir, newFileName) + if _, err := os.Stat(fullPath); os.IsNotExist(err) { + return fullPath + } + } +} + +var AddHashesCmd = &cobra.Command{ + Use: "add-hashes [environment] [yaml_file_path]", + Short: "Add bytecode hashes and render yaml, html reports", + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + environment := args[0] + yamlFilePath := args[1] + + supportedEnvironments := []string{"alpha", "gamma", "omega"} + if !utils.Contains(supportedEnvironments, environment) { + log.Fatal(). + Str("environment", environment). + Msg("Environment not supported. Environment can be one of alpha, gamma, or omega.") + } + + // todo: just require 1 rpc url based on env + if baseRpcUrl == "" { + log.Fatal(). + Msg("Base RPC URL not provided. Set it using --base-rpc-url flag or BASE_RPC_URL environment variable") + } + + if baseSepoliaRpcUrl == "" { + log.Fatal(). + Msg("Base Sepolia RPC URL not provided. Set it using --base-sepolia-rpc-url flag or BASE_SEPOLIA_RPC_URL environment variable") + } + + // Create Ethereum client + clients, err := utils.CreateEthereumClients( + baseRpcUrl, + baseSepoliaRpcUrl, + environment, + "", + false, + ) + if err != nil { + log.Fatal().Err(err).Msg("Failed to create Ethereum client") + } + defer clients[environment].Close() + + // Read YAML file + yamlData, err := os.ReadFile(yamlFilePath) + if err != nil { + log.Fatal().Err(err).Str("file", yamlFilePath).Msg("Failed to read YAML file") + } + + var data map[string]interface{} + err = yaml.Unmarshal(yamlData, &data) + if err != nil { + log.Fatal().Err(err).Msg("Failed to unmarshal YAML data") + } + + // Process deployments + deployments, ok := data["deployments"].(map[string]interface{}) + if !ok { + log.Fatal().Msg("Invalid YAML structure: 'deployments' field not found or not a map") + } + + for name, deployment := range deployments { + deploymentMap, ok := deployment.(map[string]interface{}) + if !ok { + log.Warn().Str("name", name).Msg("Skipping invalid deployment entry") + continue + } + + address, ok := deploymentMap["address"].(string) + if !ok { + log.Warn().Str("name", name).Msg("Skipping deployment without valid address") + continue + } + + addressBytes := common.HexToAddress(address) + hash, err := utils.GetContractCodeHash(clients[environment], addressBytes) + if err != nil { + log.Error().Err(err).Str("name", name).Str("address", address).Msg("Failed to get contract code hash") + continue + } + + deploymentMap["bytecodeHash"] = hash + } + + // Write updated YAML file + outputPath := getIncrementedFileName(yamlFilePath, ".yaml") + + updatedYAML, err := yaml.Marshal(data) + if err != nil { + log.Fatal().Err(err).Msg("Failed to marshal updated YAML data") + } + + err = os.WriteFile(outputPath, updatedYAML, 0644) + if err != nil { + log.Fatal().Err(err).Str("file", outputPath).Msg("Failed to write updated YAML file") + } + + // After writing the updated YAML file + if htmlRender { + htmlContent, err := renderYAMLToHTML(updatedYAML, environment) + if err != nil { + log.Error().Err(err).Msg("Failed to render YAML to HTML") + } else { + htmlOutputPath := getIncrementedFileName(yamlFilePath, ".html") + err = os.WriteFile(htmlOutputPath, []byte(htmlContent), 0644) + if err != nil { + log.Error().Err(err).Str("file", htmlOutputPath).Msg("Failed to write HTML file") + } else { + log.Info().Str("file", htmlOutputPath).Msg("Successfully wrote HTML file with bytecode hashes") + } + } + } + + log.Info().Str("file", outputPath).Msg("Successfully wrote updated YAML file with bytecode hashes") + }, +} + +func renderYAMLToHTML(yamlData []byte, environment string) (string, error) { + var data map[string]interface{} + err := yaml.Unmarshal(yamlData, &data) + if err != nil { + return "", fmt.Errorf("failed to unmarshal YAML data: %w", err) + } + + data["environment"] = environment + data["reportTime"] = time.Now().UTC().Format(time.RFC3339) + + t, err := template.ParseFiles("templates/report.html") + if err != nil { + return "", fmt.Errorf("failed to parse HTML template: %w", err) + } + + var buf strings.Builder + err = t.Execute(&buf, data) + if err != nil { + return "", fmt.Errorf("failed to execute HTML template: %w", err) + } + + return buf.String(), nil +} + +func init() { + AddHashesCmd.Flags().StringVar(&baseRpcUrl, "base-rpc-url", os.Getenv("BASE_RPC_URL"), "Base RPC URL") + AddHashesCmd.Flags(). + StringVar(&baseSepoliaRpcUrl, "base-sepolia-rpc-url", os.Getenv("BASE_SEPOLIA_RPC_URL"), "Base Sepolia RPC URL") + AddHashesCmd.Flags().BoolVar(&htmlRender, "html-render", true, "Render output as HTML") +} diff --git a/scripts/bytecode-diff/go.mod b/scripts/bytecode-diff/go.mod index f5cf6599b..af385086a 100644 --- a/scripts/bytecode-diff/go.mod +++ b/scripts/bytecode-diff/go.mod @@ -8,6 +8,7 @@ require ( github.com/rs/zerolog v1.33.0 github.com/spf13/cobra v1.8.1 gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( diff --git a/scripts/bytecode-diff/main.go b/scripts/bytecode-diff/main.go index a3619bc30..35d7a803c 100644 --- a/scripts/bytecode-diff/main.go +++ b/scripts/bytecode-diff/main.go @@ -1,359 +1,7 @@ package main -import ( - "bytecode-diff/utils" - "fmt" - "os" - "path/filepath" - - "github.com/joho/godotenv" - "github.com/rs/zerolog" - "github.com/spf13/cobra" -) - -var log zerolog.Logger - -func init() { - log = zerolog.New(os.Stderr).With().Timestamp().Logger() - utils.SetLogger(log) -} - -func setLogLevel(level string) { - switch level { - case "debug": - zerolog.SetGlobalLevel(zerolog.DebugLevel) - case "info": - zerolog.SetGlobalLevel(zerolog.InfoLevel) - case "warn": - zerolog.SetGlobalLevel(zerolog.WarnLevel) - case "error": - zerolog.SetGlobalLevel(zerolog.ErrorLevel) - default: - zerolog.SetGlobalLevel(zerolog.InfoLevel) - } -} +import "bytecode-diff/cmd" func main() { - if err := godotenv.Load(); err != nil { - log.Warn().Msg("No .env file found") - } - - supportedEnvironments := []string{"alpha", "gamma", "omega"} - var baseRpcUrl string - var facetSourcePath string - var compiledFacetsPath string - var sourceDiffDir string - var sourceDiff bool - var reportOutDir string - var originEnvironment, targetEnvironment string - var deploymentsPath string - var baseSepoliaRpcUrl string - var logLevel string - - rootCmd := &cobra.Command{ - Use: "bytecode-diff [origin_environment] [target_environment]", - Short: "A tool to retrieve and display contract bytecode diff for Base", - PersistentPreRun: func(cmd *cobra.Command, args []string) { - setLogLevel(logLevel) - }, - Args: func(cmd *cobra.Command, args []string) error { - if sourceDiff { - if len(args) != 0 { - return fmt.Errorf("no positional arguments expected when --source-diff-only is set") - } - } else { - if len(args) < 2 { - return fmt.Errorf("at least two arguments required when --source-diff-only is not set, [origin_environment], [target_environment]") - } - } - return nil - }, - PreRun: func(cmd *cobra.Command, args []string) { - if sourceDiff { - envSourceDiffDir := os.Getenv("SOURCE_DIFF_DIR") - if envSourceDiffDir != "" { - sourceDiffDir = envSourceDiffDir - } - - if sourceDiffDir == "" { - sourceDiffDir = cmd.Flag("source-diff-log").Value.String() - } - - facetSourcePath = os.Getenv("FACET_SOURCE_PATH") - if facetSourcePath == "" { - facetSourcePath = cmd.Flag("facets").Value.String() - } - if facetSourcePath == "" { - log.Fatal(). - Msg("Facet source path is missing. Set it using --facets flag or FACET_SOURCE_PATH environment variable") - } - - compiledFacetsPath = os.Getenv("COMPILED_FACETS_PATH") - log.Debug().Str("compiledFacetsPath", compiledFacetsPath).Msg("Compiled facets path from environment") - if compiledFacetsPath == "" { - compiledFacetsPath = cmd.Flag("compiled-facets").Value.String() - log.Debug().Str("compiledFacetsPath", compiledFacetsPath).Msg("Compiled facets path from flag") - } - if compiledFacetsPath == "" { - log.Fatal(). - Msg("Compiled facets path is missing. Set it using --compiled-facets flag or COMPILED_FACETS_PATH environment variable") - } - - envReportOutDir := os.Getenv("REPORT_OUT_DIR") - if envReportOutDir != "" { - reportOutDir = envReportOutDir - } - if reportOutDir == "" { - reportOutDir = cmd.Flag("report-out-dir").Value.String() - } - if reportOutDir == "" { - log.Fatal(). - Msg("Report out directory is missing. Set it using --report-out-dir flag or REPORT_OUT_DIR environment variable") - } - return - } - - envDeploymentsPath := os.Getenv("DEPLOYMENTS_PATH") - if envDeploymentsPath != "" { - deploymentsPath = envDeploymentsPath - } - if deploymentsPath == "" { - deploymentsPath = cmd.Flag("deployments").Value.String() - } - if deploymentsPath == "" { - log.Fatal(). - Msg("Deployments path is missing. Set it using --deployments flag or DEPLOYMENTS_PATH environment variable") - } - }, - Run: func(cmd *cobra.Command, args []string) { - verbose, _ := cmd.Flags().GetBool("verbose") - if sourceDiff { - - log.Info(). - Str("facetSourcePath", facetSourcePath). - Str("compiledFacetsPath", compiledFacetsPath). - Msg("Running diff for facet path recursively only compiled facet contracts") - - if err := executeSourceDiff(verbose, facetSourcePath, compiledFacetsPath, sourceDiffDir); err != nil { - log.Fatal().Err(err).Msg("Error executing source diff") - return - } - } else { - - originEnvironment, targetEnvironment = args[0], args[1] - for _, environment := range []string{originEnvironment, targetEnvironment} { - if !utils.Contains(supportedEnvironments, environment) { - log.Fatal().Str("environment", environment).Msg("Environment not supported. Environment can be one of alpha, gamma, or omega.") - } - } - - log.Info().Str("originEnvironment", originEnvironment).Str("targetEnvironment", targetEnvironment).Msg("Environment") - - if baseRpcUrl == "" { - baseRpcUrl = os.Getenv("BASE_RPC_URL") - if baseRpcUrl == "" { - log.Fatal().Msg("Base RPC URL not provided. Set it using --base-rpc flag or BASE_RPC_URL environment variable") - } - } - - if baseSepoliaRpcUrl == "" { - baseSepoliaRpcUrl = os.Getenv("BASE_SEPOLIA_RPC_URL") - if baseSepoliaRpcUrl == "" { - log.Fatal().Msg("Base Sepolia RPC URL not provided. Set it using --base-sepolia-rpc flag or BASE_SEPOLIA_RPC_URL environment variable") - } - } - - basescanAPIKey := os.Getenv("BASESCAN_API_KEY") - if basescanAPIKey == "" { - log.Fatal().Msg("BaseScan API key not provided. Set it using BASESCAN_API_KEY environment variable") - } - - log.Info().Str("originEnvironment", originEnvironment).Str("targetEnvironment", targetEnvironment).Msg("Running diff for environment") - // Create BaseConfig struct - baseConfig := utils.BaseConfig{ - BaseRpcUrl: baseRpcUrl, - BaseSepoliaRpcUrl: baseSepoliaRpcUrl, - BasescanAPIKey: basescanAPIKey, - } - - if err := executeEnvrionmentDiff(verbose, baseConfig, deploymentsPath, originEnvironment, targetEnvironment, reportOutDir); err != nil { - log.Fatal().Err(err).Msg("Error executing environment diff") - } - } - }, - } - rootCmd.Flags().StringVarP(&baseRpcUrl, "base-rpc", "b", "", "Base RPC provider URL") - rootCmd.Flags().StringVarP(&baseSepoliaRpcUrl, "base-sepolia-rpc", "", "", "Base Sepolia RPC provider URL") - rootCmd.Flags().BoolVarP(&sourceDiff, "source-diff-only", "s", false, "Run source code diff") - rootCmd.Flags().StringVar(&sourceDiffDir, "source-diff-log", "source-diffs", "Path to diff log file") - rootCmd.Flags().StringVar(&compiledFacetsPath, "compiled-facets", "../../contracts/out", "Path to compiled facets") - rootCmd.Flags().StringVar(&facetSourcePath, "facets", "", "Path to facet source files") - rootCmd.Flags().BoolP("verbose", "v", false, "Enable verbose output") - rootCmd.Flags().StringVar(&reportOutDir, "report-out-dir", "deployed-diffs", "Path to report output directory") - rootCmd.Flags(). - StringVar(&deploymentsPath, "deployments", "../../contracts/deployments", "Path to deployments directory") - rootCmd.PersistentFlags(). - StringVar(&logLevel, "log-level", "info", "Set the logging level (debug, info, warn, error)") - - if err := rootCmd.Execute(); err != nil { - log.Error().Err(err).Msg("Error executing root command") - os.Exit(1) - } -} - -func executeSourceDiff(verbose bool, facetSourcePath, compiledFacetsPath string, reportOutDir string) error { - facetFiles, err := utils.GetFacetFiles(facetSourcePath) - if err != nil { - log.Error(). - Str("facetSourcePath", facetSourcePath). - Str("compiledFacetsPath", compiledFacetsPath). - Err(err). - Msg("Error getting facet files") - return err - } - log.Debug().Int("facetFilesCount", len(facetFiles)).Msg("Facet files length") - - compiledHashes, err := utils.GetCompiledFacetHashes(compiledFacetsPath, facetFiles) - if err != nil { - log.Error(). - Err(err). - Str("compiledFacetsPath", compiledFacetsPath). - Msg("Error getting compiled facet hashes") - return err - } - - if verbose { - log.Info().Int("compiledHashesCount", len(compiledHashes)).Msg("Compiled Facet Hashes") - for file, hash := range compiledHashes { - log.Info().Str("file", file).Str("hash", hash).Msg("Compiled Facet Hash") - } - } - - err = utils.CreateFacetHashesReport(compiledFacetsPath, compiledHashes, reportOutDir, verbose) - if err != nil { - log.Error().Err(err).Msg("Error creating facet hashes report") - return err - } - - return nil -} - -func executeEnvrionmentDiff( - verbose bool, - baseConfig utils.BaseConfig, - deploymentsPath, originEnvironment, targetEnvironment string, - reportOutDir string, -) error { - // walk environment diamonds and get all facet addresses from DiamondLoupe facet view - baseDiamonds := []utils.Diamond{ - utils.BaseRegistry, - utils.Space, - utils.SpaceFactory, - utils.SpaceOwner, - } - originDeploymentsPath := filepath.Join(deploymentsPath, originEnvironment) - originDiamonds, err := utils.GetDiamondAddresses(originDeploymentsPath, baseDiamonds, verbose) - if err != nil { - log.Error().Err(err).Msgf("Error getting diamond addresses for origin environment %s", originEnvironment) - return err - } - targetDeploymentsPath := filepath.Join(deploymentsPath, targetEnvironment) - targetDiamonds, err := utils.GetDiamondAddresses(targetDeploymentsPath, baseDiamonds, verbose) - if err != nil { - log.Error().Err(err).Msgf("Error getting diamond addresses for target environment %s", targetEnvironment) - return err - } - // Create Ethereum client - clients, err := utils.CreateEthereumClients( - baseConfig.BaseRpcUrl, - baseConfig.BaseSepoliaRpcUrl, - originEnvironment, - targetEnvironment, - verbose, - ) - defer func() { - for _, client := range clients { - client.Close() - } - }() - // getCode for all facet addresses over base rpc url and compare with compiled hashes - originFacets := make(map[string][]utils.Facet) - - for diamondName, diamondAddress := range originDiamonds { - facets, err := utils.ReadAllFacets(clients[originEnvironment], diamondAddress, baseConfig.BasescanAPIKey) - if err != nil { - log.Error().Err(err).Msgf("Error reading all facets for origin diamond %s", diamondName) - return err - } - err = utils.AddContractCodeHashes(clients[originEnvironment], facets) - if err != nil { - log.Error().Err(err).Msgf("Error adding contract code hashes for origin diamond %s", diamondName) - return err - } - originFacets[string(diamondName)] = facets - } - - targetFacets := make(map[string][]utils.Facet) - for diamondName, diamondAddress := range targetDiamonds { - facets, err := utils.ReadAllFacets(clients[targetEnvironment], diamondAddress, baseConfig.BasescanAPIKey) - if err != nil { - log.Error().Err(err).Msgf("Error reading all facets for target diamond %s", diamondName) - return err - } - err = utils.AddContractCodeHashes(clients[targetEnvironment], facets) - if err != nil { - log.Error().Err(err).Msgf("Error adding contract code hashes for target diamond %s", diamondName) - return err - } - targetFacets[string(diamondName)] = facets - } - if verbose { - for diamondName, facets := range originFacets { - log.Info().Str("diamondName", diamondName).Msg("Origin Facets for Diamond contract") - for _, facet := range facets { - log.Info(). - Str("facetAddress", facet.FacetAddress.Hex()). - Str("contractName", facet.ContractName). - Interface("selectors", facet.SelectorsHex). - Msg("Facet") - } - } - for diamondName, facets := range targetFacets { - log.Info().Str("diamondName", diamondName).Msg("Target Facets for Diamond contract") - for _, facet := range facets { - log.Info(). - Str("facetAddress", facet.FacetAddress.Hex()). - Str("contractName", facet.ContractName). - Interface("selectors", facet.SelectorsHex). - Msg("Facet") - } - } - } - - // compare facets and create report - differences := utils.CompareFacets(originFacets, targetFacets) - if verbose { - for diamondName, facets := range differences { - log.Info().Str("diamondName", diamondName).Msg("Differences for Diamond contract") - for _, facet := range facets { - log.Info(). - Str("facetAddress", facet.OriginContractAddress.Hex()). - Str("originContractName", facet.OriginContractName). - Msg("Origin Facet") - log.Info(). - Interface("selectorDiff", facet.SelectorsDiff). - Msg("Selector Diff") - - } - } - } - - // create report - log.Info().Str("reportOutDir", reportOutDir).Msg("Generating YAML report") - err = utils.GenerateYAMLReport(originEnvironment, targetEnvironment, differences, reportOutDir) - if err != nil { - log.Error().Err(err).Msg("Error generating YAML report") - return err - } - return nil + cmd.Execute() } diff --git a/scripts/bytecode-diff/scripts/upgrade-facets.sh b/scripts/bytecode-diff/scripts/upgrade-facets.sh new file mode 100755 index 000000000..f523a5902 --- /dev/null +++ b/scripts/bytecode-diff/scripts/upgrade-facets.sh @@ -0,0 +1,155 @@ +#!/bin/bash +cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" +cd .. + +# Function to find the most recent file +find_most_recent_file() { + local dir="$1" + find "$dir" -name "facet_diff_*.yaml" | sort -r | head -n 1 +} + +# Check if the correct number of arguments is provided +if [ $# -lt 1 ] || [ $# -gt 3 ]; then + echo "Usage: $0 [diff_directory] [file_name]" + echo " : Must be either 'gamma' or 'omega'" + exit 1 +fi + +# Set the network and validate it +network="$1" +if [ "$network" != "gamma" ] && [ "$network" != "omega" ]; then + echo "Error: Network must be either 'gamma' or 'omega'" + exit 1 +fi + +# Set default values and parse additional arguments +diff_dir="deployed-diffs" +file_name="" + +if [ $# -eq 2 ]; then + diff_dir="$2" +elif [ $# -eq 3 ]; then + diff_dir="$2" + file_name="$3" +fi + +# If file_name is not provided, find the most recent file +if [ -z "$file_name" ]; then + most_recent_file=$(find_most_recent_file "$diff_dir") + if [ -n "$most_recent_file" ]; then + file_name=$(basename "$most_recent_file") + echo "Using most recent file: $file_name" + else + echo "No matching files found in $diff_dir" + exit 1 + fi +fi + +# Function to process a single YAML file +process_file() { + local file="$1" + echo "Processing file: $file" + + # Extract originContractNames into an array, strip "Facet" suffix, and remove duplicates + contract_names=($(yq e '.diamonds[].facets[].originContractName' "$file" | sed 's/Facet$//' | sort -u)) + + # Determine which make command to use + if [[ "$network" == "omega" ]]; then + chain_id=8453 + context="omega" + make_command="make deploy-base" + elif [[ "$network" == "gamma" ]]; then + chain_id=84532 + context="gamma" + make_command="make deploy-base-sepolia" + else + echo "Error: Unknown file type $file. Cannot determine chain ID and context." + exit 1 + fi + + # Loop through each contract name and call the appropriate make command + if [ ${#contract_names[@]} -eq 0 ]; then + echo "No contracts to deploy." + exit 0 + else + current_dir=$(pwd) + cd ../../contracts + for contract in "${contract_names[@]}"; do + deploy_file=$(find ./scripts -name "Deploy${contract}.s.sol" -o -name "Deploy${contract}Facet.s.sol" | head -n1) + + if [ -n "$deploy_file" ]; then + deploy_contract=$(basename "$deploy_file" .s.sol) + echo "Deploying contract: $contract using $deploy_contract to chain $chain_id with context $context" + OVERRIDE_DEPLOYMENTS=1 $make_command context="$context" type=facets contract="$deploy_contract" + + if [ $? -ne 0 ]; then + echo "Error deploying $contract" + fi + else + echo "Error: Deploy file not found for $contract. Skipping." + fi + done + cd "$current_dir" + fi + + # Call process_deployments after processing the file + process_deployments "$chain_id" "$file" "${contract_names[@]}" +} + +# Function to process deployments and create a new YAML file +process_deployments() { + local chain_id="$1" + local input_file="$2" + shift 2 + local contract_names=("$@") + + # Append a new deployments section to the input file + echo -e "\ndeployments:" >> "$input_file" + + for contract in "${contract_names[@]}"; do + local json_file="../../broadcast/Deploy${contract}.s.sol/${chain_id}/run-latest.json" + if [[ -f "$json_file" ]]; then + local contract_name=$(jq -r '.transactions[0].contractName' "$json_file") + local contract_address=$(jq -r '.transactions[0].contractAddress' "$json_file") + local tx_hash=$(jq -r '.transactions[0].hash' "$json_file") + local deployment_date=$(date -r "$json_file" -u +"%Y-%m-%d %H:%M") + + # Determine the baseScanLink based on the chain_id + if [ "$chain_id" == "84532" ]; then + local base_scan_link="https://sepolia.basescan.org/tx/$tx_hash" + elif [ "$chain_id" == "8453" ]; then + local base_scan_link="https://basescan.org/tx/$tx_hash" + else + local base_scan_link="" + fi + + # Append deployment information for each contract + echo " $contract_name:" >> "$input_file" + echo " address: $contract_address" >> "$input_file" + echo " transactionHash: $tx_hash" >> "$input_file" + echo " deploymentDate: $deployment_date" >> "$input_file" + echo " bytecodeHash: " >> "$input_file" + if [ -n "$base_scan_link" ]; then + echo " baseScanLink: $base_scan_link" >> "$input_file" + fi + fi + done + + echo "Deployment information appended to $input_file" +} + +# Main script +if [ -n "$file_name" ]; then + if [[ -f "$diff_dir/$file_name" ]]; then + process_file "$diff_dir/$file_name" + else + echo "Error: Specified file $diff_dir/$file_name not found." + exit 1 + fi +else + for file in "$diff_dir"/diff_*.yaml; do + if [[ -f "$file" ]]; then + process_file "$file" + fi + done +fi \ No newline at end of file diff --git a/scripts/bytecode-diff/templates/report.html b/scripts/bytecode-diff/templates/report.html new file mode 100644 index 000000000..acd743623 --- /dev/null +++ b/scripts/bytecode-diff/templates/report.html @@ -0,0 +1,118 @@ + + + + + + Base Facets Bytecode Diff Report + + + +

{{.environment}}: Base Facets Bytecode Diff Report

+

Generated on: {{.reportTime}}

+ +

Deployments

+ + + + + + + + + {{range $name, $deployment := .deployments}} + + + + + + + + {{end}} +
NameAddressBytecode HashBasescan LinkDeployment Date
{{$name}} + {{if eq $.environment "gamma"}} + {{$deployment.address}} + {{else if eq $.environment "omega"}} + {{$deployment.address}} + {{else}} + {{$deployment.address}} + {{end}} + {{$deployment.bytecodeHash}}View on Basescan{{$deployment.deploymentDate}}
+ +

Diamonds

+ {{if .diamonds}} + {{range $name, $diamond := .diamonds}} +

Diamond: {{$diamond.name}}

+

Facets

+ {{if $diamond.facets}} + + + + + + + + + + + + + {{range $facet := $diamond.facets}} + + + + + + + + + + + {{end}} +
Contract NameOrigin AddressOrigin Bytecode HashTarget AddressesTarget Bytecode HashesSelectors Missing on {{$.environment}}Origin VerifiedTarget Verified
{{$facet.originContractName}} + {{if eq $.environment "gamma"}} + {{$facet.originFacetAddress}} + {{else if eq $.environment "omega"}} + {{$facet.originFacetAddress}} + {{else}} + {{$facet.originFacetAddress}} + {{end}} + {{$facet.originBytecodeHash}} + {{range $addr := $facet.targetContractAddresses}} + {{if eq $.environment "gamma"}} + {{$addr}}
+ {{else if eq $.environment "omega"}} + {{$addr}}
+ {{else}} + {{$addr}}
+ {{end}} + {{end}} +
+ {{range $hash := $facet.targetBytecodeHashes}} + {{$hash}}
+ {{end}} +
{{$facet.selectorsDiff}}{{$facet.originVerified}}{{$facet.targetVerified}}
+ {{else}} +

No facets found for this diamond.

+ {{end}} + {{end}} + {{else}} +

No diamonds found in the YAML data.

+ {{end}} + + {{range $key, $value := .}} + {{if and (ne $key "deployments") (ne $key "diamonds")}} +

{{$key}}

+
{{$value | printf "%#v"}}
+ {{end}} + {{end}} + + \ No newline at end of file diff --git a/scripts/bytecode-diff/utils/ethereum.go b/scripts/bytecode-diff/utils/ethereum.go index a717dc731..225cd5e23 100644 --- a/scripts/bytecode-diff/utils/ethereum.go +++ b/scripts/bytecode-diff/utils/ethereum.go @@ -7,6 +7,7 @@ import ( "io" "net/http" "strings" + "time" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" @@ -89,10 +90,16 @@ func ReadAllFacets(client *ethclient.Client, contractAddress string, basescanAPI } for i, facet := range facets { + // Throttle API calls to 2 per second to avoid being rate limited + time.Sleep(500 * time.Millisecond) + // read contract name from basescan source code api contractName, err := GetContractNameFromBasescan(basescanUrl, facet.FacetAddress.Hex(), basescanAPIKey) if err != nil { - return nil, fmt.Errorf("failed to get contract name from Basescan: %w", err) + return nil, fmt.Errorf( + "failed to get contract name from Basescan: %w", + err, + ) } facets[i].ContractName = contractName @@ -127,7 +134,13 @@ func ReadAllFacets(client *ethclient.Client, contractAddress string, basescanAPI return facets, nil } -func CreateEthereumClients(baseRpcUrl, baseSepoliaRpcUrl, originEnvironment, targetEnvironment string, verbose bool) (map[string]*ethclient.Client, error) { +func CreateEthereumClients( + baseRpcUrl string, + baseSepoliaRpcUrl string, + originEnvironment string, + targetEnvironment string, + verbose bool, +) (map[string]*ethclient.Client, error) { clients := make(map[string]*ethclient.Client) for _, env := range []string{originEnvironment, targetEnvironment} { @@ -180,11 +193,17 @@ func GetContractNameFromBasescan(baseURL, address, apiKey string) (string, error } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("Basescan API returned non-200 status code: %d", resp.StatusCode) + } + body, err := io.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("failed to read response body: %w", err) } + Log.Debug().Msgf("Raw Basescan JSON response: %s", string(body)) + var result struct { Status string `json:"status"` Message string `json:"message"` @@ -209,20 +228,26 @@ func GetContractNameFromBasescan(baseURL, address, apiKey string) (string, error return result.Result[0].ContractName, nil } +// GetContractCodeHash fetches the deployed code and calculates its keccak256 hash +func GetContractCodeHash(client *ethclient.Client, address common.Address) (string, error) { + code, err := client.CodeAt(context.Background(), address, nil) + if err != nil { + return "", fmt.Errorf("failed to read contract code for address %s: %w", address.Hex(), err) + } + + hash := crypto.Keccak256Hash(code) + return hash.Hex(), nil +} + // AddContractCodeHashes reads the contract code for each facet and adds its keccak256 hash to the Facet struct func AddContractCodeHashes(client *ethclient.Client, facets []Facet) error { for i, facet := range facets { - // Read the contract code - code, err := client.CodeAt(context.Background(), facet.FacetAddress, nil) + hash, err := GetContractCodeHash(client, facet.FacetAddress) if err != nil { - return fmt.Errorf("failed to read contract code for address %s: %w", facet.FacetAddress.Hex(), err) + return err } - // Hash the code using Keccak256Hash - hash := crypto.Keccak256Hash(code) - - // Store the hash hex string in the Facet struct - facets[i].BytecodeHash = hash.Hex() + facets[i].BytecodeHash = hash } return nil