Commit 35f29894 authored by Mark Tyneway's avatar Mark Tyneway Committed by GitHub

op-bindings: remove bindgen (#10301)

Removes the bindgen cli tool from the repo since it is not owned
by anybody and also not used to generate bindings anymore since
we are moving away from auto generated bindings.
parent 9b630492
![BindGen Header Image](./bindgen_header.png)
A CLI for generating Go bindings from Forge artifacts and API clients such as Etherscan's
- [Dependencies](#dependencies)
- [Running BindGen](#running-bindgen)
- [Using the Makefile Commands](#using-the-makefile-commands)
- [`bindgen`](#bindgen)
- [Required ENVs](#required-envs)
- [`bindgen-local`](#bindgen-local)
- [`bindgen-remote`](#bindgen-remote)
- [Required ENVs](#required-envs-1)
- [Using the CLI Directly](#using-the-cli-directly)
- [CLI Flags](#cli-flags)
- [Global Flags](#global-flags)
- [Local Flags](#local-flags)
- [Remote Flags](#remote-flags)
- [Using BindGen to Add New Preinstalls to L2 Genesis](#using-bindgen-to-add-new-preinstalls-to-l2-genesis)
- [Anatomy of `artifacts.json`](#anatomy-of-artifactsjson)
- [`"local"` Contracts](#local-contracts)
- [`"remote"` Contracts](#remote-contracts)
- [Adding A New `"remote"` Contract](#adding-a-new-remote-contract)
- [Contracts that Don't Make Good Preinstalls](#contracts-that-dont-make-good-preinstalls)
- [Adding the Contract to L2 Genesis](#adding-the-contract-to-l2-genesis)
# Dependencies
- [Go](https://go.dev/dl/)
- [Foundry](https://getfoundry.sh/)
- [pnpm](https://pnpm.io/installation)
If you're running the CLI inside the Optimism monorepo, please make sure you've executed `pnpm i` and `pnpm build` to install and setup all of the monorepo's dependencies.
# Running BindGen
BindGen can be run in one of two ways:
1. Using the provided [Makefile](../Makefile) which defaults some of the required flags
2. Executing the CLI directly with `go run`, or building a Go binary and executing it
Before executing BindGen, please review the [artifacts.json](../artifacts.json) file which specifies what contracts BindGen should generate Go bindings and metadata files for. More information on how to configure `artifacts.json` can be found [here](#anatomy-of-artifactsjson).
## Using the Makefile Commands
### `bindgen`
```bash
ETHERSCAN_APIKEY_ETH=your_api_key \
ETHERSCAN_APIKEY_OP=your_api_key \
RPC_URL_ETH=your_rpc_url \
RPC_URL_OP=your_rpc_url \
make bindgen
```
This command will run `forge clean` to remove any existing Forge artifacts found in the [contracts-bedrock](../../packages/contracts-bedrock/) directory, re-build the Forge artifacts, then will use BindGen to generate Go bindings and metadata files for the contracts specified in [artifacts.json](../artifacts.json).
#### Required ENVs
- `ETHERSCAN_APIKEY_ETH` An Etherscan API key for querying Ethereum Mainnet.
- [Here's a guide](https://docs.etherscan.io/getting-started/viewing-api-usage-statistics) on how to obtain a key.
- `ETHERSCAN_APIKEY_OP` An Etherscan API key for querying Optimism Mainnet.
- You can follow the above guide to obtain a key, but make sure you're on the [Optimistic Etherscan](https://optimistic.etherscan.io/)
- `RPC_URL_ETH` This is any HTTP URL that can be used to query an Ethereum Mainnet RPC node.
- Expected to use API key authentication.
- `RPC_URL_OP` This is any HTTP URL that can be used to query an Optimism Mainnet RPC node.
- Expected to use API key authentication.
### `bindgen-local`
```bash
make bindgen-local
```
This command will run `forge clean` to remove any existing Forge artifacts found in the [contracts-bedrock](../../packages/contracts-bedrock/) directory, re-build the Forge artifacts, then will use BindGen to generate Go bindings and metadata files for the `"local"` contracts specified in [artifacts.json](../artifacts.json).
### `bindgen-remote`
```bash
ETHERSCAN_APIKEY_ETH=your_api_key \
ETHERSCAN_APIKEY_OP=your_api_key \
RPC_URL_ETH=your_rpc_url \
RPC_URL_OP=your_rpc_url \
make bindgen-remote
```
This command will use BindGen to generate Go bindings and metadata files for the `"remote"` contracts specified in [artifacts.json](../artifacts.json).
#### Required ENVs
- `ETHERSCAN_APIKEY_ETH` An Etherscan API key for querying Ethereum Mainnet.
- [Here's a guide](https://docs.etherscan.io/getting-started/viewing-api-usage-statistics) on how to obtain a key.
- `ETHERSCAN_APIKEY_OP` An Etherscan API key for querying Optimism Mainnet.
- You can follow the above guide to obtain a key, but make sure you're on the [Optimistic Etherscan](https://optimistic.etherscan.io/)
- `RPC_URL_ETH` This is any HTTP URL that can be used to query an Ethereum Mainnet RPC node.
- Expected to use API key authentication.
- `RPC_URL_OP` This is any HTTP URL that can be used to query an Optimism Mainnet RPC node.
- Expected to use API key authentication.
## Using the CLI Directly
Currently the CLI only has one command, `generate`, which expects one of the following sub-commands:
Command | Description | Flags | Usage
-------- | -------------------------------------------------------------------------- | ----------------------------- | ------------------------------------------------------------------
`all` | Generates bindings for both local and remotely sourced contracts. | [Global Flags](#global-flags) | `bindgen generate [global-flags] all [local-flags] [remote-flags]`
`local` | Generates bindings for contracts with locally available Forge artifacts. | [Local Flags](#local-flags) | `bindgen generate [global-flags] local [local-flags]`
`remote` | Generates bindings for contracts whose metadata is sourced from Etherscan. | [Remote Flags](#remote-flags) | `bindgen generate [global-flags] remote [remote-flags]`
The following displays how the CLI can be invoked from the monorepo root:
```bash
go run ./op-bindings/cmd/ <bindgen-command> <flags> <sub-command> <sub-command-flags>
```
## CLI Flags
### Global Flags
These flags are used by all CLI commands
Flag | Type | Description | Required
------------------ | ------ | ------------------------------------------------------------------------------ | --------
`metadata-out` | String | Output directory for Go bindings contract metadata files | Yes
`bindings-package` | String | Go package name used for generated Go bindings | Yes
`contracts-list` | String | Path to the list of `local` and/or `remote` contracts | Yes
`log.level` | String | Log level (`none`, `debug`, `info`, `warn`, `error`, `crit`) (Default: `info`) | No
## Local Flags
These flags are used with `all` and `local` commands
Flag | Type | Description | Required
------------------ | ------ | ------------------------------------------------------------- | --------
`source-maps-list` | String | Comma-separated list of contracts to generate source-maps for | No
`forge-artifacts` | String | Path to the directory with compiled Forge artifacts | Yes
## Remote Flags
These flags are used with `all` and `remote` commands
Flag | Type | Description | Required
---------------------- | ------ | --------------------------------------------------------------------------- | --------
`etherscan.apikey.eth` | String | An Etherscan API key for querying Ethereum Mainnet | Yes
`etherscan.apikey.op` | String | An Etherscan API key for querying Optimism Mainnet | Yes
`rpc.url.eth` | String | This is any HTTP URL that can be used to query an Ethereum Mainnet RPC node | Yes
`rpc.url.op` | String | This is any HTTP URL that can be used to query an Optimism Mainnet RPC node | Yes
# Using BindGen to Add New Preinstalls to L2 Genesis
**Note** While we encourage hacking on the OP stack, we are not actively looking to integrate more contracts to the official OP stack genesis.
BindGen uses the provided `contracts-list` to generate Go bindings and metadata files which are used when building the L2 genesis. The first step in adding a new preinstall to L2 genesis is adding the contract to your `contracts-list` (by default this list is [artifacts.json](../artifacts.json)).
## Anatomy of `artifacts.json`
Below is a condensed version of the default [artifacts.json](../artifacts.json) file for reference:
```json
{
"local": [
"SystemConfig",
"L1CrossDomainMessenger",
...
"StorageSetter",
"SuperchainConfig"
],
"remote": [
{
"name": "MultiCall3",
"verified": true,
"deployments": {
"eth": "0xcA11bde05977b3631167028862bE2a173976CA11",
"op": "0xcA11bde05977b3631167028862bE2a173976CA11"
}
},
...
{
"name": "EntryPoint",
"verified": true,
"deployments": {
"eth": "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789",
"op": "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"
},
"deploymentSalt": "0000000000000000000000000000000000000000000000000000000000000000"
}
]
}
```
### `"local"` Contracts
The first property of this JSON object, `"local"`, specifies the name of the contracts that have locally available Forge artifacts which BindGen will use to generate Go bindings and metadata files. This property specifies an array of strings where each string corresponds to the name of the contract which is used in the name of its corresponding Forge artifact.
For example, the first contract listed in the default contract list is `SystemConfig`. After running `pnpm build` in the [contract-bedrock](../../packages/contracts-bedrock/), you'll have a [forge-artifacts](../../packages/contracts-bedrock/forge-artifacts/) directory where you can find [SystemConfig.sol](../../packages/contracts-bedrock/forge-artifacts/SystemConfig.sol/). Inside is the Forge artifacts BindGen will use to generate the Go bindings and metadata file.
In some cases, such as `Safe`, there will exist multiple versioned Forge artifacts (e.g. [contracts-bedrock/forge-artifacts/Safe.sol/](../../packages/contracts-bedrock/forge-artifacts/Safe.sol/) contains `Safe.0.8.15.json` and `Safe.0.8.19.json`). In this case BindGen will default to using the lesser version (`Safe.0.8.19.json` in this case), and when running BindGen you will see a warning logged to the console to notify you:
```bash
...
WARN [12-22|13:39:19.217] Multiple versions of forge artifacts exist, using lesser version contract=Safe
...
INFO [12-22|13:39:20.253] Generating bindings and metadata for local contract contract=Safe
```
### `"remote"` Contracts
The second property specifies a list of `RemoteContract` objects which contain metadata used to fetch the needed contract info to generate Go bindings from Etherscan; these contracts do **not** have locally available Forge artifacts.
There are a couple different variations of the `RemoteContract` object, but the following is the Go struct for reference:
```go
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"`
}
```
Name | Description
---------------------- | -----------
`name` | The name of the remote contract that will be used for the Go bindings and metadata files
`verified` | Denotes whether the contract is verified on Etherscan
`deployments` | An object that maps a network and the address the contract is deployed to on that network
`deployments.eth` | The address the contract is deployed to on Ethereum Mainnet
`deployments.op` | The address the contract is deployed to on Optimism Mainnet
`deploymentSalt` | If the contract was deployed using CREATE2 or a CREATE2 proxy deployer, here is where you specify the salt that was used for creation
`deployer` | The address used to deploy the contract, used to mimic CREATE2 deployments
`abi` | The ABI of the contract, required if the contract is **not** verified on Etherscan
`initBytecode` | The initialization bytecode for the contract, required if the contract is a part of the initialization of another contract (i.e. the `input` data of the deployment transaction contains initialization bytecode other than what belongs to the specific contract you're adding)
### Adding A New `"remote"` Contract
After adding a `RemoteContract` object to your `contracts-list`, you will need to add the `name` of your contract to the `switch` statement found in the `processContracts` function in [generator_remote.go](./generator_remote.go):
```go
...
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 your contract is verified on Etherscan, doesn't contain any Solidity `immutable`s, and doesn't require any special handling, then you most likely can add your contract's `name` to the first switch case. Then will use the `standardHandler` which:
1. Fetches the required contract metadata from Etherscan (i.e. initialization and deployed bytecode, ABI, deployment transaction hash, etc.)
2. Compares the retrieved deployed bytecode from Etherscan against the response of `eth_codeAt` from an RPC node for each network specified in `RemoteContract.deployments` (this is a sanity check to verify Etherscan is returning correct data)
3. If applicable, removes the provided `RemoteContract.deploymentSalt` from the initialization bytecode
4. Compares the initialization bytecode retrieved from Etherscan on Ethereum Mainnet against the bytecode retrieved from Etherscan on Optimism Mainnet
- This is an important sanity check! If the initialization bytecode from Ethereum differs from Optimism, then there's a big chance the deployment from Ethereum may not behave as expected if preinstalled to an OP stack L2
5. Compares the deployment bytecode retrieved from Etherscan on Ethereum Mainnet against the bytecode retrieved from Etherscan on Optimism Mainnet
- This has the same concern as differing initialization bytecode
6. Lastly, the Go bindings are generated and the metadata file is written to the path provided as `metadata-out` CLI flag
All other default `"remote"` contract have some variation of the above execution flow depending on the nuances of each contract. For example:
- `Create2Deployer`'s initialization and deployed bytecode is expected to differ from its Optimism Mainnet deployment
- `MultiSend_v130` has an `immutable` Solidity variable the resolves to `address(this)`, so we can't use the deployment bytecode from Ethereum Mainnet, we must get its deployment bytecode from Optimism Mainnet
- `SenderCreator` is deployed by `EntryPoint`, so its initialization bytecode is provided in [artifacts.json](../artifacts.json) and not being fetched from Etherscan like other contracts
#### Contracts that Don't Make Good Preinstalls
Not every contract can be added as a preinstall, and some contracts have nuances that make them potentially dangerous or troublesome to preinstall. Below are some examples of contracts that wouldn't make good preinstalls. This is not a comprehensive list, so make sure to use judgment for each contract added as a preinstall.
- Contracts that haven't been audited or stood the test of time
- Once a contract is preinstalled and a network is started, if a vulnerability is discovered for the contract and there is no way to easily disable the contract, the only options to "disable" the vulnerable contract are to either (A) remove it from the L2 genesis and restart the L2 network, (B) Hardfork the network to remove/replace the preinstall, or (C) Warn users not to use the vulnerable preinstall
- Related to above, contracts that may become deprecated/unsupported relatively soon
- As mentioned above, you're limited to options A, B, or C
- Upgradeable Contracts
- While it's certainly feasible to preinstall an upgradeable contract, great care should be taken to minimize security risks to users if the contract is upgraded to a malicious or buggy implementation. Understanding who has the ability to upgrade the contract is key to avoiding this. Additionally, users might be expecting a preinstall to do something and may be caught off guard if the implementation was upgraded without their knowledge
- Contracts with Privileged Roles and Configuration Parameters
- Similar to the upgradeable contracts, simply having an owner or other privileged role with the ability to make configuration changes can present a security risk and result in unexpected different behaviors across chains.
- Contracts that have dependencies
- Dependencies has many definitions, for example:
- Being reliant on specific Oracle contracts that may not be available on your L2
- Specific contract state that's set on L1 but won't be on L2
- Relying on specific values of block and transaction properties (e.g. `block.chainid`, `block.timestamp`, `block.number`, etc.)
- Contract libraries that may not be deployed on L2
### Adding the Contract to L2 Genesis
Once you've configured the `contracts-list` to include the contracts you'd like to add as preinstalls, the next step is utilizing the BindGen outputs to configure the L2 genesis.
1. First we must update the [addresses.go](../predeploys/addresses.go) file to include the address we're preinstalling our contracts to
1. Update the `switch` case found in [layer_two.go](../../op-chain-ops/genesis/layer_two.go) to include the `name` of your contracts
1. Update [immutables.go](../../op-chain-ops/immutables/immutables.go) to include your added contracts
1. Update [Predeploys.sol](../../packages/contracts-bedrock/src/libraries/Predeploys.sol) to include your added contracts at their expected addresses
1. Update [Predeploys.t.sol](../../packages/contracts-bedrock/test/Predeploys.t.sol) to include the `name` of your contracts to avoid being tested for `Predeploys.PROXY_ADMIN`
This source diff could not be displayed because it is too large. You can view the blob instead.
package bindgen
// The Init bytecode used for these tests can either be sourced
// on-chain using the deployment tx of these contracts, or can be
// found in the bindings output from BindGen (../bindings/)
var removeDeploymentSaltTests = []struct {
name string
deploymentData string
deploymentSalt string
expected string
}{
{
"Case #1",
Safe_v130InitBytecode,
"0000000000000000000000000000000000000000000000000000000000000000",
Safe_v130InitBytecodeNoSalt,
},
{
"Case #2",
Permit2InitBytecode,
"0000000000000000000000000000000000000000d3af2663da51c10215000000",
Permit2InitBytecodeNoSalt,
},
{
"Case #3",
EntryPointInitBytecode,
"0000000000000000000000000000000000000000000000000000000000000000",
EntryPointInitBytecodeNoSalt,
},
}
var removeDeploymentSaltTestsFailures = []struct {
name string
deploymentData string
deploymentSalt string
expectedError string
}{
{
"Failure Case #1 Invalid Regex",
"0x1234abc",
"[invalid-regex",
"failed to compile regular expression: error parsing regexp: missing closing ]: `[invalid-regex)`",
},
{
"Failure Case #2 Salt Not Found",
"0x1234abc",
"4567",
"expected salt: 4567 to be at the beginning of the contract initialization code: 0x1234abc, but it wasn't",
},
}
package bindgen
import (
"encoding/json"
"errors"
"fmt"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/ethereum-optimism/optimism/op-bindings/foundry"
)
type BindGenGeneratorLocal struct {
BindGenGeneratorBase
SourceMapsList string
ForgeArtifactsPath string
}
func (generator *BindGenGeneratorLocal) 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.Local) == 0 {
return fmt.Errorf("no contracts parsed from given contract list: %s", generator.ContractsListPath)
}
return generator.processContracts(contracts.Local)
}
func (generator *BindGenGeneratorLocal) processContracts(contracts []string) error {
tempArtifactsDir, err := mkTempArtifactsDir(generator.Logger)
if err != nil {
return err
}
defer func() {
err := os.RemoveAll(tempArtifactsDir)
if err != nil {
generator.Logger.Error("Error removing temporary artifact directory", "path", tempArtifactsDir, "err", err.Error())
} else {
generator.Logger.Debug("Successfully removed temporary artifact directory")
}
}()
sourceMapsList := strings.Split(generator.SourceMapsList, ",")
sourceMapsSet := make(map[string]struct{})
for _, k := range sourceMapsList {
sourceMapsSet[k] = struct{}{}
}
contractArtifactPaths, err := generator.getContractArtifactPaths()
if err != nil {
return err
}
for _, contractName := range contracts {
generator.Logger.Info("Generating bindings and metadata for local contract", "contract", contractName)
forgeArtifact, err := generator.readForgeArtifact(contractName, contractArtifactPaths)
if err != nil {
return err
}
abiFilePath, bytecodeFilePath, err := writeContractArtifacts(generator.Logger, tempArtifactsDir, contractName, forgeArtifact.Abi, []byte(forgeArtifact.Bytecode.Object.String()))
if err != nil {
return err
}
err = genContractBindings(generator.Logger, generator.MonorepoBasePath, abiFilePath, bytecodeFilePath, generator.BindingsPackageName, contractName)
if err != nil {
return err
}
}
return nil
}
func (generator *BindGenGeneratorLocal) getContractArtifactPaths() (map[string]string, error) {
// If some contracts have the same name then the path to their
// artifact depends on their full import path. Scan over all artifacts
// and hold a mapping from the contract name to the contract path.
// Walk walks the directory deterministically, so the earliest instance
// of the contract with the same name will be used
artifactPaths := make(map[string]string)
if err := filepath.Walk(generator.ForgeArtifactsPath,
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if strings.HasSuffix(path, ".json") {
base := filepath.Base(path)
name := strings.TrimSuffix(base, ".json")
// remove the compiler version from the name
re := regexp.MustCompile(`\.\d+\.\d+\.\d+`)
sanitized := re.ReplaceAllString(name, "")
_, ok := artifactPaths[sanitized]
if !ok {
artifactPaths[sanitized] = path
} else {
generator.Logger.Warn("Multiple versions of forge artifacts exist, using lesser version", "contract", sanitized)
}
}
return nil
}); err != nil {
return artifactPaths, err
}
return artifactPaths, nil
}
func (generator *BindGenGeneratorLocal) readForgeArtifact(contractName string, contractArtifactPaths map[string]string) (foundry.Artifact, error) {
var forgeArtifact foundry.Artifact
contractArtifactPath := path.Join(generator.ForgeArtifactsPath, contractName+".sol", contractName+".json")
forgeArtifactRaw, err := os.ReadFile(contractArtifactPath)
if errors.Is(err, os.ErrNotExist) {
generator.Logger.Debug("Cannot find forge-artifact at standard path, trying provided path", "contract", contractName, "standardPath", contractArtifactPath, "providedPath", contractArtifactPaths[contractName])
contractArtifactPath = contractArtifactPaths[contractName]
forgeArtifactRaw, err = os.ReadFile(contractArtifactPath)
if errors.Is(err, os.ErrNotExist) {
return forgeArtifact, fmt.Errorf("cannot find forge-artifact of %q", contractName)
}
}
generator.Logger.Debug("Using forge-artifact", "path", contractArtifactPath)
if err := json.Unmarshal(forgeArtifactRaw, &forgeArtifact); err != nil {
return forgeArtifact, fmt.Errorf("failed to parse forge artifact of %q: %w", contractName, err)
}
return forgeArtifact, nil
}
package bindgen
import (
"context"
"fmt"
"os"
"github.com/ethereum-optimism/optimism/op-bindings/etherscan"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
)
type BindGenGeneratorRemote struct {
BindGenGeneratorBase
ContractDataClients struct {
Eth contractDataClient
Op contractDataClient
}
RpcClients struct {
Eth *ethclient.Client
Op *ethclient.Client
}
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.Transaction, 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
}
package bindgen
import (
"bytes"
"context"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"text/template"
"github.com/ethereum-optimism/optimism/op-bindings/etherscan"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
)
type ContractData struct {
Abi string
DeployedBin string
DeploymentTx etherscan.Transaction
}
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 err = generator.CompareDeployedBytecodeWithRpc(contractMetadata, "eth"); err != nil {
return err
}
if err = generator.CompareDeployedBytecodeWithRpc(contractMetadata, "op"); err != nil {
return err
}
// 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.CompareInitBytecodeWithOp(contractMetadata, true); err != nil {
return fmt.Errorf("%s: %w", contractMetadata.Name, err)
}
if err := generator.CompareDeployedBytecodeWithOp(contractMetadata, true); err != nil {
return fmt.Errorf("%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 expecting the initialization bytecode for Create2Deployer to not match the init code on OP,
// because the deployment on OP has been overwritten by the Canyon hardfork, and the init code
// Etherscan returns for the OP deployment is from the initial outdated deployment.
// For context: https://github.com/ethereum-optimism/op-geth/pull/126
if err := generator.CompareInitBytecodeWithOp(contractMetadata, false); err != nil {
return fmt.Errorf("%s: %w", contractMetadata.Name, err)
}
if err := generator.CompareDeployedBytecodeWithOp(contractMetadata, true); err != nil {
return fmt.Errorf("%s: %w", contractMetadata.Name, err)
}
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 err = generator.CompareDeployedBytecodeWithRpc(contractMetadata, "op"); err != nil {
return err
}
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)
}
if err = generator.CompareDeployedBytecodeWithRpc(contractMetadata, "eth"); err != nil {
return err
}
if err = generator.CompareDeployedBytecodeWithRpc(contractMetadata, "op"); err != nil {
return 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.CompareInitBytecodeWithOp(contractMetadata, false); err != nil {
return fmt.Errorf("%s: %w", contractMetadata.Name, err)
}
if err := generator.CompareDeployedBytecodeWithOp(contractMetadata, true); err != nil {
return fmt.Errorf("%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
contractMetadata.DeployedBin = fetchedData.DeployedBin
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,
)
}
if err := generator.CompareInitBytecodeWithOp(contractMetadata, true); err != nil {
return fmt.Errorf("%s: %w", contractMetadata.Name, err)
}
// We're asserting the deployed bytecode doesn't match, because Permit2 has immutable Solidity variables that
// are dependent on block.chainid
if err := generator.CompareDeployedBytecodeWithOp(contractMetadata, false); err != nil {
return fmt.Errorf("%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) CompareInitBytecodeWithOp(contractMetadataEth *RemoteContractMetadata, initCodeShouldMatch bool) error {
if contractMetadataEth.InitBin == "" {
return fmt.Errorf("no initialization bytecode provided for ETH deployment for comparison")
}
var zeroAddress common.Address
if contractMetadataEth.Deployments.Op == zeroAddress {
return fmt.Errorf("no deployment address on Optimism provided for %s", contractMetadataEth.Name)
}
// 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 opContractData.DeploymentTx.Input, err = generator.removeDeploymentSalt(opContractData.DeploymentTx.Input, contractMetadataEth.DeploymentSalt); err != nil {
return err
}
initCodeComparison := strings.EqualFold(contractMetadataEth.InitBin, opContractData.DeploymentTx.Input)
if initCodeShouldMatch && !initCodeComparison {
return fmt.Errorf(
"expected initialization bytecode to match on Ethereum and Optimism, but it doesn't. contract=%s bytecodeEth=%s bytecodeOp=%s",
contractMetadataEth.Name,
contractMetadataEth.InitBin,
opContractData.DeploymentTx.Input,
)
} else if !initCodeShouldMatch && initCodeComparison {
return fmt.Errorf(
"expected initialization bytecode on Ethereum to not match on Optimism, but it did. contract=%s bytecodeEth=%s bytecodeOp=%s",
contractMetadataEth.Name,
contractMetadataEth.InitBin,
opContractData.DeploymentTx.Input,
)
}
return nil
}
func (generator *BindGenGeneratorRemote) CompareDeployedBytecodeWithOp(contractMetadataEth *RemoteContractMetadata, deployedCodeShouldMatch bool) error {
if contractMetadataEth.DeployedBin == "" {
return fmt.Errorf("no deployed bytecode provided for ETH deployment for comparison")
}
var zeroAddress common.Address
if contractMetadataEth.Deployments.Op == zeroAddress {
return fmt.Errorf("no deployment address on Optimism provided for %s", contractMetadataEth.Name)
}
// 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
}
deployedCodeComparison := strings.EqualFold(contractMetadataEth.DeployedBin, opContractData.DeployedBin)
if deployedCodeShouldMatch && !deployedCodeComparison {
return fmt.Errorf(
"expected deployed bytecode to match on Ethereum and Optimism, but it doesn't. contract=%s bytecodeEth=%s bytecodeOp=%s",
contractMetadataEth.Name,
contractMetadataEth.DeployedBin,
opContractData.DeployedBin,
)
} else if !deployedCodeShouldMatch && deployedCodeComparison {
return fmt.Errorf(
"expected deployed bytecode on Ethereum to not match on Optimism, but it does. contract=%s bytecodeEth=%s bytecodeOp=%s",
contractMetadataEth.Name,
contractMetadataEth.DeployedBin,
opContractData.DeployedBin,
)
}
return nil
}
func (generator *BindGenGeneratorRemote) CompareDeployedBytecodeWithRpc(contractMetadata *RemoteContractMetadata, chain string) error {
var client *ethclient.Client
switch chain {
case "eth":
client = generator.RpcClients.Eth
case "op":
client = generator.RpcClients.Op
default:
return fmt.Errorf("unknown chain: %s, unable to retrieve a RPC client", chain)
}
var deployment common.Address
switch chain {
case "eth":
deployment = contractMetadata.Deployments.Eth
case "op":
deployment = contractMetadata.Deployments.Op
default:
generator.Logger.Warn("Unable to compare bytecode from Etherscan against RPC client, no deployment address provided for chain", "chain", chain)
}
if deployment != (common.Address{}) {
bytecode, err := client.CodeAt(context.Background(), common.HexToAddress(deployment.Hex()), nil)
if err != nil {
return fmt.Errorf("error getting deployed bytecode from RPC on chain: %s err: %w", chain, err)
}
bytecodeHex := common.Bytes2Hex(bytecode)
if !strings.EqualFold(strings.TrimPrefix(contractMetadata.DeployedBin, "0x"), bytecodeHex) {
return fmt.Errorf("%s deployment bytecode from RPC doesn't match bytecode from Etherscan. rpcBytecode: %s etherscanBytecode: %s", contractMetadata.Name, bytecodeHex, contractMetadata.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, generator.MonorepoBasePath, 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")
var existingOutput []byte
if _, err := os.Stat(metadataFilePath); err == nil {
existingOutput, err = os.ReadFile(metadataFilePath)
if err != nil {
return fmt.Errorf("error reading existing metadata output file, metadataFilePath: %s err: %w", metadataFilePath, err)
}
}
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)
}
if len(existingOutput) != 0 {
var newOutput []byte
newOutput, err = os.ReadFile(metadataFilePath)
if err != nil {
return fmt.Errorf("error reading new file: %w", err)
}
if bytes.Equal(existingOutput, newOutput) {
generator.Logger.Debug("No changes detected in the contract metadata", "contract", contractMetadata.Name)
} else {
generator.Logger.Warn("Changes detected in the contract metadata, old metadata has been overwritten", "contract", contractMetadata.Name)
}
} else {
generator.Logger.Debug("No existing contract metadata found, skipping comparison", "contract", contractMetadata.Name)
}
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
}
`
package bindgen
import (
"testing"
"github.com/stretchr/testify/require"
)
var generator BindGenGeneratorRemote = BindGenGeneratorRemote{}
func TestRemoveDeploymentSalt(t *testing.T) {
for _, tt := range removeDeploymentSaltTests {
t.Run(tt.name, func(t *testing.T) {
got, _ := generator.removeDeploymentSalt(tt.deploymentData, tt.deploymentSalt)
require.Equal(t, tt.expected, got)
})
}
}
func TestRemoveDeploymentSaltFailures(t *testing.T) {
for _, tt := range removeDeploymentSaltTestsFailures {
t.Run(tt.name, func(t *testing.T) {
_, err := generator.removeDeploymentSalt(tt.deploymentData, tt.deploymentSalt)
require.Equal(t, err.Error(), tt.expectedError)
})
}
}
package bindgen
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"path"
"strings"
"github.com/ethereum/go-ethereum/log"
)
type BindGenGeneratorBase struct {
MetadataOut string
BindingsPackageName string
MonorepoBasePath string
ContractsListPath string
Logger log.Logger
}
type contractsList struct {
Local []string `json:"local"`
Remote []RemoteContract `json:"remote"`
}
// readContractList reads a JSON file from the given `filePath` and unmarshals
// its content into the provided result interface. It logs the path of the file
// being read.
//
// Parameters:
// - logger: An instance of go-ethereum/log
// - filePath: The path to the JSON file to be read.
// - result: A pointer to the structure where the JSON data will be unmarshaled.
//
// Returns:
// - An error if reading the file or unmarshaling fails, nil otherwise.
func readContractList(logger log.Logger, filePath string) (contractsList, error) {
logger.Debug("Reading contract list", "filePath", filePath)
var contracts contractsList
contractData, err := os.ReadFile(filePath)
if err != nil {
return contracts, err
}
return contracts, json.Unmarshal(contractData, &contracts)
}
// mkTempArtifactsDir creates a temporary directory with a "op-bindings" prefix
// for holding contract artifacts. The path to the created directory is logged.
//
// Parameters:
// - logger: An instance of go-ethereum/log
//
// Returns:
// - The path to the created temporary directory.
// - An error if the directory creation fails, nil otherwise.
func mkTempArtifactsDir(logger log.Logger) (string, error) {
dir, err := os.MkdirTemp("", "op-bindings")
if err != nil {
return "", err
}
logger.Debug("Created temporary artifacts directory", "dir", dir)
return dir, nil
}
// writeContractArtifacts writes the provided ABI and bytecode data to respective
// files in the specified temporary directory. The naming convention for these
// files is based on the provided contract name. The ABI data is written to a file
// with a ".abi" extension, and the bytecode data is written to a file with a ".bin"
// extension.
//
// Parameters:
// - logger: An instance of go-ethereum/log
// - tempDirPath: The directory path where the ABI and bytecode files will be written.
// - contractName: The name of the contract, used to create the filenames.
// - abi: The ABI data of the contract.
// - bytecode: The bytecode of the contract.
//
// Returns:
// - The full path to the written ABI file.
// - The full path to the written bytecode file.
// - An error if writing either file fails, nil otherwise.
func writeContractArtifacts(logger log.Logger, tempDirPath, contractName string, abi, bytecode []byte) (string, string, error) {
logger.Debug("Writing ABI and bytecode to temporary artifacts directory", "contractName", contractName, "tempDirPath", tempDirPath)
abiFilePath := path.Join(tempDirPath, contractName+".abi")
if err := os.WriteFile(abiFilePath, abi, 0o600); err != nil {
return "", "", fmt.Errorf("error writing %s's ABI file: %w", contractName, err)
}
bytecodeFilePath := path.Join(tempDirPath, contractName+".bin")
if err := os.WriteFile(bytecodeFilePath, bytecode, 0o600); err != nil {
return "", "", fmt.Errorf("error writing %s's bytecode file: %w", contractName, err)
}
return abiFilePath, bytecodeFilePath, nil
}
// genContractBindings generates Go bindings for an Ethereum contract using
// the provided ABI and bytecode files. The bindings are generated using the
// `abigen` tool and are written to the specified Go package directory. The
// generated file's name is based on the provided contract name and will have
// a ".go" extension. The generated bindings will be part of the provided Go
// package.
//
// Parameters:
// - logger: An instance of go-ethereum/log
// - abiFilePath: The path to the ABI file for the contract.
// - bytecodeFilePath: The path to the bytecode file for the contract.
// - goPackageName: The name of the Go package where the bindings will be written.
// - contractName: The name of the contract, used for naming the output file and
// defining the type in the generated bindings.
//
// Returns:
// - An error if there's an issue during any step of the binding generation process,
// nil otherwise.
//
// Note: This function relies on the external `abigen` tool, which should be
// installed and available in the system's PATH.
func genContractBindings(logger log.Logger, monorepoRootPath, abiFilePath, bytecodeFilePath, goPackageName, contractName string) error {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("error getting cwd: %w", err)
}
outFilePath := path.Join(cwd, goPackageName, strings.ToLower(contractName)+".go")
var existingOutput []byte
if _, err := os.Stat(outFilePath); err == nil {
existingOutput, err = os.ReadFile(outFilePath)
if err != nil {
return fmt.Errorf("error reading existing bindings output file, outFilePath: %s err: %w", outFilePath, err)
}
}
if monorepoRootPath != "" {
logger.Debug("Checking abigen version")
// Fetch installed abigen version (format: abigen version X.Y.Z-<stable/nightly>-<commit_sha>)
cmd := exec.Command("abigen", "--version")
var versionBuf bytes.Buffer
cmd.Stdout = bufio.NewWriter(&versionBuf)
if err := cmd.Run(); err != nil {
return fmt.Errorf("error fetching abigen version: %w", err)
}
abigenVersion := bytes.Trim(versionBuf.Bytes(), "\n")
// Fetch expected abigen version (format: vX.Y.Z)
expectedAbigenVersion, err := readExpectedAbigenVersion(monorepoRootPath)
if err != nil {
return fmt.Errorf("error fetching the expected abigen version: %w", err)
}
if !bytes.Contains(abigenVersion, []byte(expectedAbigenVersion)) {
return fmt.Errorf("abigen version mismatch, expected %s, got %s. Please run `pnpm install:abigen` in the monorepo root", expectedAbigenVersion, abigenVersion)
}
} else {
logger.Debug("No monorepo root path provided, skipping abigen version check")
}
logger.Debug("Generating contract bindings", "contractName", contractName, "outFilePath", outFilePath)
cmd := exec.Command("abigen", "--abi", abiFilePath, "--bin", bytecodeFilePath, "--pkg", goPackageName, "--type", contractName, "--out", outFilePath)
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
return fmt.Errorf("error running abigen for %s: %w", contractName, err)
}
if len(existingOutput) != 0 {
newOutput, err := os.ReadFile(outFilePath)
if err != nil {
return fmt.Errorf("error reading new file: %w", err)
}
if bytes.Equal(existingOutput, newOutput) {
logger.Debug("No changes detected in the contract bindings", "contractName", contractName)
} else {
logger.Warn("Changes detected in the contract bindings, old bindings have been overwritten", "contractName", contractName)
}
} else {
logger.Debug("No existing contract bindings found, skipping comparison", "contractName", contractName)
}
return nil
}
// Versions is a struct for holding the versions of the tools used in the monorepo
type Versions struct {
Abigen string `json:"abigen"`
Foundry string `json:"foundry"`
Geth string `json:"geth"`
Nvm string `json:"nvm"`
Slither string `json:"slither"`
Kontrol string `json:"kontrol"`
}
// readExpectedAbigenVersion reads the expected abigen version from the monorepo's
// versions.json file. This function will remove the 'v' prefix from the version
// string.
//
// Parameters:
// - monorepoRootPath: The path to the monorepo's root directory.
//
// Returns:
// - The expected abigen version.
// - An error if the versions.json file cannot be read or parsed, nil otherwise.
func readExpectedAbigenVersion(monorepoRootPath string) (string, error) {
// Open the version control file
jsonFile, err := os.Open(path.Join(monorepoRootPath, "versions.json"))
if err != nil {
return "", fmt.Errorf("error reading versions.json file: %w", err)
}
defer jsonFile.Close()
// Parse the version control file
byteValue, _ := io.ReadAll(jsonFile)
var versions Versions
if err := json.Unmarshal(byteValue, &versions); err != nil {
return "", fmt.Errorf("error parsing versions.json file: %w", err)
}
// Trim the 'v' prefix from the version string
return strings.Trim(versions.Abigen, "v"), nil
}
package bindgen
import (
"encoding/json"
"os"
"path"
"testing"
"github.com/stretchr/testify/require"
)
func TestReadExpectedAbigenVersion(t *testing.T) {
// Create a temporary directory for the version control file.
tmpDir := path.Join(os.TempDir(), "version-tests")
defer os.RemoveAll(tmpDir)
require.NoError(t, os.MkdirAll(tmpDir, 0755))
// Create a temporary version control file.
versionFile := path.Join(tmpDir, "versions.json")
versions := Versions{Abigen: "v1.2.3"}
// Marshal the versions to JSON.
versionsJSON, err := json.Marshal(versions)
require.NoError(t, err)
// Write the JSON to the version control file.
require.NoError(t, os.WriteFile(versionFile, versionsJSON, 0644))
// Read the expected version from the version control file.
// The read version should not have a "v" prefix.
expectedVersion, err := readExpectedAbigenVersion(tmpDir)
require.NoError(t, err)
require.Equal(t, expectedVersion, "1.2.3")
}
package main
import (
"fmt"
"os"
"github.com/ethereum-optimism/optimism/op-bindings/bindgen"
"github.com/ethereum-optimism/optimism/op-bindings/etherscan"
op_service "github.com/ethereum-optimism/optimism/op-service"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/urfave/cli/v2"
)
const (
// Base Flags
MetadataOutFlagName = "metadata-out"
BindingsPackageNameFlagName = "bindings-package"
ContractsListFlagName = "contracts-list"
// 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() {
oplog.SetupDefaults()
app := &cli.App{
Name: "BindGen",
Usage: "Generate contract bindings using Foundry artifacts and/or remotely sourced contract data",
Commands: []*cli.Command{
{
Name: "generate",
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,
},
},
},
},
}
if err := app.Run(os.Args); err != nil {
log.Crit("BindGen error", "error", err.Error())
}
}
func setupLogger(c *cli.Context) log.Logger {
logger := oplog.NewLogger(oplog.AppOut(c), oplog.ReadCLIConfig(c))
oplog.SetGlobalLogHandler(logger.Handler())
return logger
}
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 {
return err
}
if err := localBindingsGenerator.GenerateBindings(); err != nil {
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)
}
}
func parseConfigBase(logger log.Logger, c *cli.Context) (bindgen.BindGenGeneratorBase, error) {
cwd, err := os.Getwd()
if err != nil {
return bindgen.BindGenGeneratorBase{}, err
}
monoRepoPath, err := op_service.FindMonorepoRoot(cwd)
if err != nil {
return bindgen.BindGenGeneratorBase{}, err
}
return bindgen.BindGenGeneratorBase{
MetadataOut: c.String(MetadataOutFlagName),
BindingsPackageName: c.String(BindingsPackageNameFlagName),
MonorepoBasePath: monoRepoPath,
ContractsListPath: c.String(ContractsListFlagName),
Logger: logger,
}, nil
}
func parseConfigLocal(logger log.Logger, c *cli.Context) (bindgen.BindGenGeneratorLocal, error) {
baseConfig, err := parseConfigBase(logger, c)
if err != nil {
return bindgen.BindGenGeneratorLocal{}, err
}
return bindgen.BindGenGeneratorLocal{
BindGenGeneratorBase: baseConfig,
SourceMapsList: c.String(SourceMapsListFlagName),
ForgeArtifactsPath: c.String(ForgeArtifactsFlagName),
}, nil
}
func parseConfigRemote(logger log.Logger, c *cli.Context) (bindgen.BindGenGeneratorRemote, error) {
baseConfig, err := parseConfigBase(logger, c)
if err != nil {
return bindgen.BindGenGeneratorRemote{}, err
}
generator := bindgen.BindGenGeneratorRemote{
BindGenGeneratorBase: baseConfig,
}
generator.ContractDataClients.Eth = etherscan.NewEthereumClient(c.String(EtherscanApiKeyEthFlagName))
generator.ContractDataClients.Op = etherscan.NewOptimismClient(c.String(EtherscanApiKeyOpFlagName))
if generator.RpcClients.Eth, err = ethclient.Dial(c.String(RpcUrlEthFlagName)); err != nil {
return bindgen.BindGenGeneratorRemote{}, fmt.Errorf("error initializing Ethereum client: %w", err)
}
if generator.RpcClients.Op, err = ethclient.Dial(c.String(RpcUrlOpFlagName)); err != nil {
return bindgen.BindGenGeneratorRemote{}, fmt.Errorf("error initializing Optimism client: %w", err)
}
return generator, nil
}
func baseFlags() []cli.Flag {
baseFlags := []cli.Flag{
&cli.StringFlag{
Name: MetadataOutFlagName,
Usage: "Output directory to put contract metadata files in",
Required: true,
},
&cli.StringFlag{
Name: BindingsPackageNameFlagName,
Usage: "Go package name given to generated bindings",
Required: true,
},
&cli.StringFlag{
Name: ContractsListFlagName,
Usage: "Path to file containing list of contract names to generate bindings for",
Required: true,
},
}
return append(baseFlags, oplog.CLIFlags("bindgen")...)
}
func localFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: SourceMapsListFlagName,
Usage: "Comma-separated list of contracts to generate source-maps for",
},
&cli.StringFlag{
Name: ForgeArtifactsFlagName,
Usage: "Path to forge-artifacts directory, containing compiled contract artifacts",
Required: true,
},
}
}
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,
},
&cli.StringFlag{
Name: RpcUrlEthFlagName,
Usage: "RPC URL (with API key if required) to query Ethereum",
Required: true,
},
&cli.StringFlag{
Name: RpcUrlOpFlagName,
Usage: "RPC URL (with API key if required) to query Optimism",
Required: true,
},
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment