From 725ad1a8e2d8a49b8aa27d52b34a35f38661be86 Mon Sep 17 00:00:00 2001 From: Wyatt Barnes Date: Fri, 17 Nov 2023 15:20:44 -1000 Subject: [PATCH] Init remote contract generation --- op-bindings/Makefile | 28 +++ op-bindings/artifacts.json | 99 ++++++++ op-bindings/bindgen/generator_remote.go | 124 ++++++++++ op-bindings/bindgen/main.go | 75 ++++++ op-bindings/bindgen/remote_handlers.go | 310 ++++++++++++++++++++++++ op-bindings/bindgen/utils.go | 3 +- op-bindings/etherscan/client.go | 5 +- 7 files changed, 639 insertions(+), 5 deletions(-) create mode 100644 op-bindings/bindgen/generator_remote.go create mode 100644 op-bindings/bindgen/remote_handlers.go diff --git a/op-bindings/Makefile b/op-bindings/Makefile index 906fc23de1e5a..7392f37e53a02 100644 --- a/op-bindings/Makefile +++ b/op-bindings/Makefile @@ -5,6 +5,8 @@ monorepo-base := $(shell dirname $(realpath .)) contracts-dir := $(monorepo-base)/packages/contracts-bedrock contracts-list := ./artifacts.json log-level := info +ETHERSCAN_APIKEY_ETH ?= +ETHERSCAN_APIKEY_OP ?= all: version mkdir bindings @@ -21,6 +23,21 @@ bindings: bindgen-local bindings-build: bindgen-generate-local +bindgen: compile bindgen-generate-all + +bindgen-generate-all: + go run ./bindgen/ \ + generate \ + --metadata-out ./$(pkg) \ + --bindings-package $(pkg) \ + --contracts-list $(contracts-list) \ + --log.level $(log-level) \ + all \ + --source-maps-list MIPS,PreimageOracle \ + --forge-artifacts $(contracts-dir)/forge-artifacts \ + --etherscan.apikey.eth $(ETHERSCAN_APIKEY_ETH) \ + --etherscan.apikey.op $(ETHERSCAN_APIKEY_OP) + bindgen-local: compile bindgen-generate-local bindgen-generate-local: @@ -34,6 +51,17 @@ bindgen-generate-local: --source-maps-list MIPS,PreimageOracle \ --forge-artifacts $(contracts-dir)/forge-artifacts +bindgen-remote: + go run ./bindgen/ \ + generate \ + --metadata-out ./$(pkg) \ + --bindings-package $(pkg) \ + --contracts-list $(contracts-list) \ + --log.level $(log-level) \ + remote \ + --etherscan.apikey.eth $(ETHERSCAN_APIKEY_ETH) \ + --etherscan.apikey.op $(ETHERSCAN_APIKEY_OP) + mkdir: mkdir -p $(pkg) diff --git a/op-bindings/artifacts.json b/op-bindings/artifacts.json index f216885aab145..c6ec7cd372535 100644 --- a/op-bindings/artifacts.json +++ b/op-bindings/artifacts.json @@ -46,5 +46,104 @@ "ISemver", "StorageSetter", "SuperchainConfig" + ], + "remote": [ + { + "name": "MultiCall3", + "verified": true, + "deployments": { + "eth": "0xcA11bde05977b3631167028862bE2a173976CA11", + "op": "0xcA11bde05977b3631167028862bE2a173976CA11" + } + }, + { + "name": "Create2Deployer", + "verified": true, + "deployments": { + "eth": "0xF49600926c7109BD66Ab97a2c036bf696e58Dbc2" + } + }, + { + "name": "Safe_v130", + "verified": true, + "deployments": { + "eth": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", + "op": "0x69f4D1788e39c87893C980c06EdF4b7f686e2938" + }, + "deploymentSalt": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "name": "SafeL2_v130", + "verified": true, + "deployments": { + "eth": "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", + "op": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA" + }, + "deploymentSalt": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "name": "MultiSendCallOnly_v130", + "verified": true, + "deployments": { + "eth": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", + "op": "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B" + }, + "deploymentSalt": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "name": "SafeSingletonFactory", + "verified": false, + "deployments": { + "eth": "0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7", + "op": "0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7" + }, + "abi": "[{\"payable\":true,\"stateMutability\":\"payable\",\"type\":\"fallback\",\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"salt\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"creationCode\",\"type\":\"bytes\"}]}]" + }, + { + "name": "DeterministicDeploymentProxy", + "verified": false, + "deployments": { + "eth": "0x4e59b44847b379578588920cA78FbF26c0B4956C", + "op": "0x4e59b44847b379578588920cA78FbF26c0B4956C" + }, + "abi": "[{\"payable\":true,\"stateMutability\":\"payable\",\"type\":\"fallback\",\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"salt\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"creationCode\",\"type\":\"bytes\"}]}]" + }, + { + "name": "MultiSend_v130", + "verified": true, + "deployments": { + "op": "0x998739BFdAAdde7C933B942a68053933098f9EDa" + }, + "deploymentSalt": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "name": "Permit2", + "verified": true, + "deployments": { + "eth": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "op": "0x000000000022D473030F116dDEE9F6B43aC78BA3" + }, + "deploymentSalt": "0000000000000000000000000000000000000000d3af2663da51c10215000000", + "deployer": "0x4e59b44847b379578588920cA78FbF26c0B4956C" + }, + { + "name": "SenderCreator", + "verified": false, + "deployments": { + "eth": "0x7fc98430eaedbb6070b35b39d798725049088348", + "op": "0x7fc98430eaedbb6070b35b39d798725049088348" + }, + "initBytecode": "0x6080806040523461001657610210908161001c8239f35b600080fdfe6080604052600436101561001257600080fd5b6000803560e01c63570e1a361461002857600080fd5b346100c95760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100c95760043567ffffffffffffffff918282116100c957366023830112156100c95781600401359283116100c95736602484840101116100c9576100c561009e84602485016100fc565b60405173ffffffffffffffffffffffffffffffffffffffff90911681529081906020820190565b0390f35b80fd5b507f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b90806014116101bb5767ffffffffffffffff917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffec82018381116101cd575b604051937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0603f81600b8701160116850190858210908211176101c0575b604052808452602084019036848401116101bb576020946000600c819682946014880187378301015251923560601c5af19060005191156101b557565b60009150565b600080fd5b6101c86100cc565b610178565b6101d56100cc565b61013a56fea26469706673582212201927e80b76ab9b71c952137dd676621a9fdf520c25928815636594036eb1c40364736f6c63430008110033", + "abi": "[{\"inputs\": [{\"internalType\": \"bytes\",\"name\": \"initCode\",\"type\": \"bytes\"}],\"name\": \"createSender\",\"outputs\": [{\"internalType\": \"address\",\"name\": \"sender\",\"type\": \"address\"}],\"stateMutability\": \"nonpayable\",\"type\": \"function\"}]" + }, + { + "name": "EntryPoint", + "verified": true, + "deployments": { + "eth": "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", + "op": "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789" + }, + "deploymentSalt": "0000000000000000000000000000000000000000000000000000000000000000" + } ] } diff --git a/op-bindings/bindgen/generator_remote.go b/op-bindings/bindgen/generator_remote.go new file mode 100644 index 0000000000000..e059e7428fbb8 --- /dev/null +++ b/op-bindings/bindgen/generator_remote.go @@ -0,0 +1,124 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/ethereum-optimism/optimism/op-bindings/etherscan" + "github.com/ethereum/go-ethereum/common" +) + +type bindGenGeneratorRemote struct { + bindGenGeneratorBase + contractDataClients struct { + eth contractDataClient + op contractDataClient + } + tempArtifactsDir string +} + +type contractDataClient interface { + FetchAbi(ctx context.Context, address string) (string, error) + FetchDeployedBytecode(ctx context.Context, address string) (string, error) + FetchDeploymentTxHash(ctx context.Context, address string) (string, error) + FetchDeploymentTx(ctx context.Context, txHash string) (etherscan.TxInfo, error) +} + +type deployments struct { + Eth common.Address `json:"eth"` + Op common.Address `json:"op"` +} + +type remoteContract struct { + Name string `json:"name"` + Verified bool `json:"verified"` + Deployments deployments `json:"deployments"` + DeploymentSalt string `json:"deploymentSalt"` + Deployer common.Address `json:"deployer"` + ABI string `json:"abi"` + InitBytecode string `json:"initBytecode"` +} + +type remoteContractMetadata struct { + remoteContract + Package string + InitBin string + DeployedBin string +} + +func (generator *bindGenGeneratorRemote) generateBindings() error { + contracts, err := readContractList(generator.logger, generator.contractsListPath) + if err != nil { + return fmt.Errorf("error reading contract list %s: %w", generator.contractsListPath, err) + } + if len(contracts.Remote) == 0 { + return fmt.Errorf("no contracts parsed from given contract list: %s", generator.contractsListPath) + } + + return generator.processContracts(contracts.Remote) +} + +func (generator *bindGenGeneratorRemote) processContracts(contracts []remoteContract) error { + var err error + generator.tempArtifactsDir, err = mkTempArtifactsDir(generator.logger) + if err != nil { + return err + } + defer func() { + err := os.RemoveAll(generator.tempArtifactsDir) + if err != nil { + generator.logger.Error("Error removing temporary artifact directory", "path", generator.tempArtifactsDir, "err", err.Error()) + } else { + generator.logger.Debug("Successfully removed temporary artifact directory") + } + }() + + for _, contract := range contracts { + generator.logger.Info("Generating bindings and metadata for remote contract", "contract", contract.Name) + + contractMetadata := remoteContractMetadata{ + remoteContract: remoteContract{ + Name: contract.Name, + Deployments: contract.Deployments, + DeploymentSalt: contract.DeploymentSalt, + ABI: contract.ABI, + Verified: contract.Verified, + }, + Package: generator.bindingsPackageName, + } + + var err error + switch contract.Name { + case "MultiCall3", "Safe_v130", "SafeL2_v130", "MultiSendCallOnly_v130", + "EntryPoint", "SafeSingletonFactory", "DeterministicDeploymentProxy": + err = generator.standardHandler(&contractMetadata) + case "Create2Deployer": + err = generator.create2DeployerHandler(&contractMetadata) + case "MultiSend_v130": + err = generator.multiSendHandler(&contractMetadata) + case "SenderCreator": + // The SenderCreator contract is deployed by EntryPoint, so the transaction data + // from the deployment transaction is for the entire EntryPoint deployment. + // So, we're manually providing the initialization bytecode + contractMetadata.InitBin = contract.InitBytecode + err = generator.senderCreatorHandler(&contractMetadata) + case "Permit2": + // Permit2 has an immutable Solidity variable that resolves to block.chainid, + // so we can't use the deployed bytecode, and instead must generate it + // at some later point not handled by BindGen. + // DeployerAddress is intended to be used to help deploy Permit2 at it's deterministic address + // to a chain set with the required id to be able to obtain a diff minimized deployed bytecode + contractMetadata.Deployer = contract.Deployer + err = generator.permit2Handler(&contractMetadata) + default: + err = fmt.Errorf("unknown contract: %s, don't know how to handle it", contract.Name) + } + + if err != nil { + return err + } + } + + return nil +} diff --git a/op-bindings/bindgen/main.go b/op-bindings/bindgen/main.go index 020633d088b1d..eb88e571193bd 100644 --- a/op-bindings/bindgen/main.go +++ b/op-bindings/bindgen/main.go @@ -4,6 +4,7 @@ import ( "fmt" "os" + "github.com/ethereum-optimism/optimism/op-bindings/etherscan" "github.com/ethereum-optimism/optimism/op-e2e/config" oplog "github.com/ethereum-optimism/optimism/op-service/log" "github.com/ethereum/go-ethereum/log" @@ -27,6 +28,12 @@ const ( // Local Contracts Flags SourceMapsListFlagName = "source-maps-list" ForgeArtifactsFlagName = "forge-artifacts" + + // Remote Contracts Flags + EtherscanApiKeyEthFlagName = "etherscan.apikey.eth" + EtherscanApiKeyOpFlagName = "etherscan.apikey.op" + RpcUrlEthFlagName = "rpc.url.eth" + RpcUrlOpFlagName = "rpc.url.op" ) func main() { @@ -41,12 +48,24 @@ func main() { Usage: "Generate contract bindings", Flags: baseFlags(), Subcommands: []*cli.Command{ + { + Name: "all", + Usage: "Generate bindings for local and remote contracts", + Flags: append(localFlags(), remoteFlags()...), + Action: generateBindings, + }, { Name: "local", Usage: "Generate bindings for locally sourced contracts", Flags: localFlags(), Action: generateBindings, }, + { + Name: "remote", + Usage: "Generate bindings for remotely sourced contracts", + Flags: remoteFlags(), + Action: generateBindings, + }, }, }, }, @@ -67,6 +86,24 @@ func generateBindings(c *cli.Context) error { logger := setupLogger(c) switch c.Command.Name { + case "all": + localBindingsGenerator, err := parseConfigLocal(logger, c) + if err != nil { + return err + } + if err := localBindingsGenerator.generateBindings(); err != nil { + return fmt.Errorf("error generating local bindings: %w", err) + } + + remoteBindingsGenerator, err := parseConfigRemote(logger, c) + if err != nil { + return err + } + if err := remoteBindingsGenerator.generateBindings(); err != nil { + return fmt.Errorf("error generating remote bindings: %w", err) + } + + return nil case "local": localBindingsGenerator, err := parseConfigLocal(logger, c) if err != nil { @@ -76,6 +113,15 @@ func generateBindings(c *cli.Context) error { return fmt.Errorf("error generating local bindings: %w", err) } return nil + case "remote": + remoteBindingsGenerator, err := parseConfigRemote(logger, c) + if err != nil { + return err + } + if err := remoteBindingsGenerator.generateBindings(); err != nil { + return fmt.Errorf("error generating remote bindings: %w", err) + } + return nil default: return fmt.Errorf("unknown command: %s", c.Command.Name) } @@ -113,6 +159,20 @@ func parseConfigLocal(logger log.Logger, c *cli.Context) (bindGenGeneratorLocal, }, nil } +func parseConfigRemote(logger log.Logger, c *cli.Context) (bindGenGeneratorRemote, error) { + baseConfig, err := parseConfigBase(logger, c) + if err != nil { + return bindGenGeneratorRemote{}, err + } + generator := bindGenGeneratorRemote{ + bindGenGeneratorBase: baseConfig, + } + + generator.contractDataClients.eth = etherscan.NewEthereumClient(c.String(EtherscanApiKeyEthFlagName)) + generator.contractDataClients.op = etherscan.NewOptimismClient(c.String(EtherscanApiKeyOpFlagName)) + return generator, nil +} + func baseFlags() []cli.Flag { baseFlags := []cli.Flag{ &cli.StringFlag{ @@ -148,3 +208,18 @@ func localFlags() []cli.Flag { }, } } + +func remoteFlags() []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Name: EtherscanApiKeyEthFlagName, + Usage: "API key to make queries to Etherscan for Ethereum", + Required: true, + }, + &cli.StringFlag{ + Name: EtherscanApiKeyOpFlagName, + Usage: "API key to make queries to Etherscan for Optimism", + Required: true, + }, + } +} diff --git a/op-bindings/bindgen/remote_handlers.go b/op-bindings/bindgen/remote_handlers.go new file mode 100644 index 0000000000000..74a44d98bd462 --- /dev/null +++ b/op-bindings/bindgen/remote_handlers.go @@ -0,0 +1,310 @@ +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "text/template" + + "github.com/ethereum-optimism/optimism/op-bindings/etherscan" +) + +type contractData struct { + abi string + deployedBin string + deploymentTx etherscan.TxInfo +} + +func (generator *bindGenGeneratorRemote) standardHandler(contractMetadata *remoteContractMetadata) error { + fetchedData, err := generator.fetchContractData(contractMetadata.Verified, "eth", contractMetadata.Deployments.Eth.Hex()) + if err != nil { + return err + } + + contractMetadata.DeployedBin = fetchedData.deployedBin + + // If ABI was explicitly provided by config, don't overwrite + if contractMetadata.ABI == "" { + contractMetadata.ABI = fetchedData.abi + } else if fetchedData.abi != "" && contractMetadata.ABI != fetchedData.abi { + generator.logger.Debug("ABIs", "given", contractMetadata.ABI, "fetched", fetchedData.abi) + return fmt.Errorf("the given ABI for %s differs from what was fetched from Etherscan", contractMetadata.Name) + } + + if contractMetadata.InitBin, err = generator.removeDeploymentSalt(fetchedData.deploymentTx.Input, contractMetadata.DeploymentSalt); err != nil { + return err + } + + if err := generator.compareBytecodeWithOp(contractMetadata, true, true); err != nil { + return fmt.Errorf("error comparing contract bytecode for %s: %w", contractMetadata.Name, err) + } + + return generator.writeAllOutputs(contractMetadata, remoteContractMetadataTemplate) +} + +func (generator *bindGenGeneratorRemote) create2DeployerHandler(contractMetadata *remoteContractMetadata) error { + fetchedData, err := generator.fetchContractData(contractMetadata.Verified, "eth", contractMetadata.Deployments.Eth.Hex()) + if err != nil { + return err + } + + contractMetadata.ABI = fetchedData.abi + contractMetadata.DeployedBin = fetchedData.deployedBin + if contractMetadata.InitBin, err = generator.removeDeploymentSalt(fetchedData.deploymentTx.Input, contractMetadata.DeploymentSalt); err != nil { + return err + } + + // We're not comparing the bytecode for Create2Deployer with deployment on OP, + // because we're predeploying a modified version of Create2Deployer that has not yet been + // deployed to OP. + // For context: https://github.com/ethereum-optimism/op-geth/pull/126 + + return generator.writeAllOutputs(contractMetadata, remoteContractMetadataTemplate) +} + +func (generator *bindGenGeneratorRemote) multiSendHandler(contractMetadata *remoteContractMetadata) error { + // MultiSend has an immutable that resolves to this(address). + // Because we're predeploying MultiSend to the same address as on OP, + // we can use the deployed bytecode directly for the predeploy + fetchedData, err := generator.fetchContractData(contractMetadata.Verified, "op", contractMetadata.Deployments.Op.Hex()) + if err != nil { + return err + } + + contractMetadata.ABI = fetchedData.abi + contractMetadata.DeployedBin = fetchedData.deployedBin + if contractMetadata.InitBin, err = generator.removeDeploymentSalt(fetchedData.deploymentTx.Input, contractMetadata.DeploymentSalt); err != nil { + return err + } + + return generator.writeAllOutputs(contractMetadata, remoteContractMetadataTemplate) +} + +func (generator *bindGenGeneratorRemote) senderCreatorHandler(contractMetadata *remoteContractMetadata) error { + var err error + contractMetadata.DeployedBin, err = generator.contractDataClients.eth.FetchDeployedBytecode(context.Background(), contractMetadata.Deployments.Eth.Hex()) + if err != nil { + return fmt.Errorf("error fetching deployed bytecode: %w", err) + } + + // The SenderCreator contract is deployed by EntryPoint, so the transaction data + // from the deployment transaction is for the entire EntryPoint deployment. + // So, we're manually providing the initialization bytecode and therefore it isn't being compared here + if err := generator.compareBytecodeWithOp(contractMetadata, false, true); err != nil { + return fmt.Errorf("error comparing contract bytecode for %s: %w", contractMetadata.Name, err) + } + + return generator.writeAllOutputs(contractMetadata, remoteContractMetadataTemplate) +} + +func (generator *bindGenGeneratorRemote) permit2Handler(contractMetadata *remoteContractMetadata) error { + fetchedData, err := generator.fetchContractData(contractMetadata.Verified, "eth", contractMetadata.Deployments.Eth.Hex()) + if err != nil { + return err + } + + contractMetadata.ABI = fetchedData.abi + if contractMetadata.InitBin, err = generator.removeDeploymentSalt(fetchedData.deploymentTx.Input, contractMetadata.DeploymentSalt); err != nil { + return err + } + + if !strings.EqualFold(contractMetadata.Deployer.Hex(), fetchedData.deploymentTx.To) { + return fmt.Errorf( + "expected deployer address: %s doesn't match the to address: %s for Permit2's proxy deployment transaction", + contractMetadata.Deployer.Hex(), + fetchedData.deploymentTx.To, + ) + } + + // We're not comparing deployed bytecode because Permit2 has immutable Solidity variables that + // are dependent on block.chainid + if err := generator.compareBytecodeWithOp(contractMetadata, true, false); err != nil { + return fmt.Errorf("error comparing contract bytecode for %s: %w", contractMetadata.Name, err) + } + + return generator.writeAllOutputs(contractMetadata, permit2MetadataTemplate) +} + +func (generator *bindGenGeneratorRemote) fetchContractData(contractVerified bool, chain, deploymentAddress string) (contractData, error) { + var data contractData + var err error + + var client contractDataClient + switch chain { + case "eth": + client = generator.contractDataClients.eth + case "op": + client = generator.contractDataClients.op + default: + return data, fmt.Errorf("unknown chain, unable to retrieve a contract data client for chain: %s", chain) + } + + if contractVerified { + data.abi, err = client.FetchAbi(context.Background(), deploymentAddress) + if err != nil { + return contractData{}, fmt.Errorf("error fetching ABI: %w", err) + } + } + + data.deployedBin, err = client.FetchDeployedBytecode(context.Background(), deploymentAddress) + if err != nil { + return contractData{}, fmt.Errorf("error fetching deployed bytecode: %w", err) + } + + deploymentTxHash, err := client.FetchDeploymentTxHash(context.Background(), deploymentAddress) + if err != nil { + return contractData{}, fmt.Errorf("error fetching deployment transaction hash: %w", err) + } + + data.deploymentTx, err = client.FetchDeploymentTx(context.Background(), deploymentTxHash) + if err != nil { + return contractData{}, fmt.Errorf("error fetching deployment transaction data: %w", err) + } + + return data, nil +} + +func (generator *bindGenGeneratorRemote) removeDeploymentSalt(deploymentData, deploymentSalt string) (string, error) { + if deploymentSalt == "" { + return deploymentData, nil + } + + re, err := regexp.Compile(fmt.Sprintf("^0x(%s)", deploymentSalt)) + if err != nil { + return "", fmt.Errorf("failed to compile regular expression: %w", err) + } + if !re.MatchString(deploymentData) { + return "", fmt.Errorf( + "expected salt: %s to be at the beginning of the contract initialization code: %s, but it wasn't", + deploymentSalt, deploymentData, + ) + } + return re.ReplaceAllString(deploymentData, ""), nil +} + +func (generator *bindGenGeneratorRemote) compareBytecodeWithOp(contractMetadataEth *remoteContractMetadata, compareInitialization, compareDeployment bool) error { + // Passing false here, because true will retrieve contract's ABI, but we don't need it for bytecode comparison + opContractData, err := generator.fetchContractData(false, "op", contractMetadataEth.Deployments.Op.Hex()) + if err != nil { + return err + } + + if compareInitialization { + if opContractData.deploymentTx.Input, err = generator.removeDeploymentSalt(opContractData.deploymentTx.Input, contractMetadataEth.DeploymentSalt); err != nil { + return err + } + + if !strings.EqualFold(contractMetadataEth.InitBin, opContractData.deploymentTx.Input) { + return fmt.Errorf( + "initialization bytecode on Ethereum doesn't match bytecode on Optimism. contract=%s bytecodeEth=%s bytecodeOp=%s", + contractMetadataEth.Name, + contractMetadataEth.InitBin, + opContractData.deploymentTx.Input, + ) + } + } + + if compareDeployment { + if !strings.EqualFold(contractMetadataEth.DeployedBin, opContractData.deployedBin) { + return fmt.Errorf( + "deployed bytecode on Ethereum doesn't match bytecode on Optimism. contract=%s bytecodeEth=%s bytecodeOp=%s", + contractMetadataEth.Name, + contractMetadataEth.DeployedBin, + opContractData.deployedBin, + ) + } + } + + return nil +} + +func (generator *bindGenGeneratorRemote) writeAllOutputs(contractMetadata *remoteContractMetadata, fileTemplate string) error { + abiFilePath, bytecodeFilePath, err := writeContractArtifacts( + generator.logger, generator.tempArtifactsDir, contractMetadata.Name, + []byte(contractMetadata.ABI), []byte(contractMetadata.InitBin), + ) + if err != nil { + return err + } + + err = genContractBindings(generator.logger, abiFilePath, bytecodeFilePath, generator.bindingsPackageName, contractMetadata.Name) + if err != nil { + return err + } + + return generator.writeContractMetadata( + contractMetadata, + template.Must(template.New("remoteContractMetadata").Parse(fileTemplate)), + ) +} + +func (generator *bindGenGeneratorRemote) writeContractMetadata(contractMetadata *remoteContractMetadata, fileTemplate *template.Template) error { + metadataFilePath := filepath.Join(generator.metadataOut, strings.ToLower(contractMetadata.Name)+"_more.go") + metadataFile, err := os.OpenFile( + metadataFilePath, + os.O_RDWR|os.O_CREATE|os.O_TRUNC, + 0o600, + ) + if err != nil { + return fmt.Errorf("error opening %s's metadata file at %s: %w", contractMetadata.Name, metadataFilePath, err) + } + defer metadataFile.Close() + + if err := fileTemplate.Execute(metadataFile, contractMetadata); err != nil { + return fmt.Errorf("error writing %s's contract metadata at %s: %w", contractMetadata.Name, metadataFilePath, err) + } + + generator.logger.Debug("Successfully wrote contract metadata", "contract", contractMetadata.Name, "path", metadataFilePath) + return nil +} + +// remoteContractMetadataTemplate is a Go text template for generating the metadata +// associated with a remotely sourced contracts. +// +// The template expects the following data to be provided: +// - .Package: the name of the Go package. +// - .Name: the name of the contract. +// - .DeployedBin: the binary (hex-encoded) of the deployed contract. +var remoteContractMetadataTemplate = `// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package {{.Package}} + +var {{.Name}}DeployedBin = "{{.DeployedBin}}" +func init() { + deployedBytecodes["{{.Name}}"] = {{.Name}}DeployedBin +} +` + +// permit2MetadataTemplate is a Go text template used to generate metadata +// for remotely sourced Permit2 contract. Because Permit2 has an immutable +// Solidity variables that depends on block.chainid, we can't use the deployed +// bytecode, but instead need to generate it specifically for each chain. +// To help with this, the metadata contains the initialization bytecode, the +// deployer address, and the CREATE2 salt, so that deployment can be +// replicated as closely as possible. +// +// The template expects the following data to be provided: +// - .Package: the name of the Go package. +// - .Name: the name of the contract. +// - .InitBin: the binary (hex-encoded) of the contract's initialization code. +// - .DeploymentSalt: the salt used during the contract's deployment. +// - .Deployer: the Ethereum address of the contract's deployer. +var permit2MetadataTemplate = `// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package {{.Package}} + +var {{.Name}}InitBin = "{{.InitBin}}" +var {{.Name}}DeploymentSalt = "{{.DeploymentSalt}}" +var {{.Name}}Deployer = "{{.Deployer}}" + +func init() { + initBytecodes["{{.Name}}"] = {{.Name}}InitBin + deploymentSalts["{{.Name}}"] = {{.Name}}DeploymentSalt + deployers["{{.Name}}"] = {{.Name}}Deployer +} +` diff --git a/op-bindings/bindgen/utils.go b/op-bindings/bindgen/utils.go index 444b9305efcdf..8465e34af3c4e 100644 --- a/op-bindings/bindgen/utils.go +++ b/op-bindings/bindgen/utils.go @@ -12,7 +12,8 @@ import ( ) type contractsList struct { - Local []string `json:"local"` + Local []string `json:"local"` + Remote []remoteContract `json:"remote"` } // readContractList reads a JSON file from the given `filePath` and unmarshals diff --git a/op-bindings/etherscan/client.go b/op-bindings/etherscan/client.go index 21231c8c133fb..0349912030cfe 100644 --- a/op-bindings/etherscan/client.go +++ b/op-bindings/etherscan/client.go @@ -120,10 +120,7 @@ func (c *client) fetchEtherscanRpc(ctx context.Context, url string) (rpcResponse } var resultString string - err = json.Unmarshal(response.Result, &resultString) - if err != nil { - return rpcResponse{}, fmt.Errorf("failed to unmarshal response.Result as a string: %w", err) - } + _ = json.Unmarshal(response.Result, &resultString) if resultString == errRateLimited { return rpcResponse{}, errors.New(errRateLimited) }