Skip to content

op-deployer: add command to verify contracts on etherscan#14359

Closed
bitwiseguy wants to merge 12 commits intodevelopfrom
ss/deployer-verify-contracts
Closed

op-deployer: add command to verify contracts on etherscan#14359
bitwiseguy wants to merge 12 commits intodevelopfrom
ss/deployer-verify-contracts

Conversation

@bitwiseguy
Copy link
Contributor

@bitwiseguy bitwiseguy commented Feb 13, 2025

Overview

Closes #13544

Provides a user-friendly command to verify L1 contracts on etherscan. The following information is used for verification:

  • contract addresses: pulled from the state.json file
  • contract artifact info (source code, compile settings): pulled from artifacts specified by l1ContractLocator field in the state.json.
  • constructor args: encode by pulling data from deployed contracts and state.json file

Usage

  1. Verify all contracts:
go run ./cmd/op-deployer verify \
  --l1-rpc-url "xxx" \
  --etherscan-api-key xxx
  1. Verify one of the contract bundles (i.e. superchain, implementations, opchain)
go run ./cmd/op-deployer verify \
  --l1-rpc-url "xxx" \
  --etherscan-api-key xxx
  --contract-bundle superchain
  1. Verify a single contract
go run ./cmd/op-deployer verify \
  --l1-rpc-url "xxx" \
  --etherscan-api-key xxx
  --contract-bundle superchain
  --contract-name ProxyAdminAddress

Testing

Manually tested by running the following sequence:

  1. Modified the implementation source code for ProxyAdmin, SuperchainConfig, and ProtocolVersions by just adding an extra function to each one called fakeFunction so that the bytecode was unique and the contract was not automatically verified by etherscan.
  2. just build - compile the contracts
  3. Ran the following command to deploy the superchain contract bundle to sepolia
go run ./cmd/op-deployer bootstrap superchain \
  --l1-rpc-url="xxx" \
  --private-key="xxx" \
  --artifacts-locator="file:///Users/<username>/repos/op-labs/optimism/packages/contracts-bedrock/forge-artifacts" \
  --required-protocol-version="0x33a0e51eCD7188d2F13769D7B4feE4390e970C7D000000000000000000000000" \
  --recommended-protocol-version="0x33a0e51eCD7188d2F13769D7B4feE4390e970C7D000000000000000000000000" \
  --superchain-proxy-admin-owner="0x83D4eB702690413a0A1E5819ECc48b62160dFb42" \
  --protocol-versions-owner="0x83D4eB702690413a0A1E5819ECc48b62160dFb42" \
  --guardian="0x83D4eB702690413a0A1E5819ECc48b62160dFb42" \
  --paused=true
  1. Ran the following command to verify that contract bundle on sepolia:
go run ./cmd/op-deployer verify \
  --l1-rpc-url "xxx" \
  --etherscan-api-key xxx \
  --contract-bundle superchain

Contract addresses used for this test:

  "superchainDeployment": {
    "proxyAdminAddress": "0x9513783d80312af0e50f37849039738571d6b413",
    "superchainConfigProxyAddress": "0x96b69609c739e66a2be0803cd0e14037d0ea3bb7",
    "superchainConfigImplAddress": "0x77fa21851150a174d00addc5b2cd2f9321904474",
    "protocolVersionsProxyAddress": "0xed4278224700e847a24414fdea46dad6339b0252",
    "protocolVersionsImplAddress": "0x01206c8e4654faf1387c8eb334fa56723b1f791c"
  }

@codecov
Copy link

codecov bot commented Feb 13, 2025

Codecov Report

Attention: Patch coverage is 0% with 17 lines in your changes missing coverage. Please review.

Project coverage is 90.56%. Comparing base (2e75688) to head (b9226fe).
Report is 65 commits behind head on develop.

Files with missing lines Patch % Lines
op-chain-ops/foundry/artifact.go 0.00% 17 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           develop   #14359       +/-   ##
============================================
+ Coverage    46.45%   90.56%   +44.10%     
============================================
  Files         1032      117      -915     
  Lines        88560     5543    -83017     
============================================
- Hits         41143     5020    -36123     
+ Misses       44346      518    -43828     
+ Partials      3071        5     -3066     
Flag Coverage Δ
cannon-go-tests-32 ?
cannon-go-tests-64 ?
contracts-bedrock-tests 94.58% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
op-chain-ops/foundry/artifact.go 45.76% <0.00%> (-18.53%) ⬇️

... and 918 files with indirect coverage changes

🚀 New features to boost your workflow:
  • Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

Choose a reason for hiding this comment

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

This change made it easier to construct the etherscan verification request


optimizer = true
optimizer_runs = 999999
use_literal_content = true
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This forced just build to include the source code in the artifacts, which we need for contract verification

Copy link
Collaborator

Choose a reason for hiding this comment

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

@mds1 is this an OK change to make?

Copy link
Contributor

Choose a reason for hiding this comment

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

It feels like a smell that we need to make this change, but it should be ok. An alternative is to create a [verify] profile here that adds this setting. If we keep this, we should add a comment explaining this so contract devs know why it's here

But we are already compiling the code with forge so should be able to just get the required info more easily. For example forge build --build-info --build-info-path cache generates a JSON file in the cache dir containing all build info including the source code, and forge verify-contract --show-standard-json-input spits out the standard JSON file that you can submit directly to etherscan

Are we strongly against shelling out to forge instead here? A few thoughts about this PR:

  • I worry that this is a lot of code to maintain and we are duplicating functionality that the foundry team has spent a lot of effort on to make robust, so this is likely to become flaky
  • We also need to basically duplicate etherscan.go for each block explorer we want to support, blockscout being the other main one. They do all have very similar APIs though so we can reuse a lot of code
  • Forge can infer constructor args based on the initcode it fetches from the block explorer, which is usually correct and avoids having to implement and maintain constructors.go

Copy link
Contributor Author

@bitwiseguy bitwiseguy Feb 17, 2025

Choose a reason for hiding this comment

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

Are we strongly against shelling out to forge instead here?

I was attempting to preserve the "stand-alone binary without external dependencies" property of op-deployer. I don't feel strongly about that, but was trying to follow the existing pattern. Seems like there was a lot of effort put into implementing other forge functionality in go. Is there any difference in the forge verify-contract feature compared to the other forge features that are implemented in go, or are you more asking about the op-deployer design as a whole and why it doesn't shell out to forge in all cases?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Forge can infer constructor args based on the initcode it fetches from the block explorer, which is usually correct and avoids having to implement and maintain constructors.go

I imagine we can do the same in go and make constructors.go much simpler. I wasn't aware that constructor args could be derived that way, therefore I went with the existing complex solution. If we stick with a go implementation for contract verification, perhaps the entire constructors.go file could just be:

func inferConstructorArgs(deployedAddress common.Address, compiledBytecode string) (string, error) {
    // Get deployed bytecode from chain
    deployedCode, err := client.CodeAt(context.Background(), deployedAddress, nil)
    if err != nil {
        return "", err
    }

    // Remove "0x" prefix if present
    compiledBytecode = strings.TrimPrefix(compiledBytecode, "0x")
    deployedHex := hex.EncodeToString(deployedCode)

    // Find where compiled bytecode ends in deployed bytecode
    if !strings.Contains(deployedHex, compiledBytecode) {
        return "", fmt.Errorf("compiled bytecode not found in deployed bytecode")
    }

    // Extract constructor args (everything after the compiled bytecode)
    constructorArgs := strings.TrimPrefix(deployedHex, compiledBytecode)
    return constructorArgs, nil
}

Copy link
Contributor

@mds1 mds1 Feb 17, 2025

Choose a reason for hiding this comment

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

Is there any difference in the forge verify-contract feature compared to the other forge features that are implemented in go, or are you more asking about the op-deployer design as a whole and why it doesn't shell out to forge in all cases?

Just asking about the verify-contract feature, not op-deployer as a whole. The rationale here is that contract verification has been a pain point for foundry users for a long time (see how many foundry issues there are for unable to verify), so I worry that over time we will encounter similar issues if we try reimplementing verification. However if you have an approach that works, and we can simplify some of it as discussed in the below paragraph, then I'm ok starting with this approach to avoid needing forge as a dep, and we can always change if it ends up being problematic

Re constructors.go, it's a bit more complex than your snippet because you need to fetch the initcode (creation code) not the deployed code. You can see forge's implementation here, notably the part where they fetch creation code using this Get Contract Creator and Creation Tx Hash Etherscan API endpoint. (Fetching it yourself gets tricky because you have you have to binary search over all blocks then trace the transaction where it was deployed, so I recommend using the etherscan endpoint)

But yes we can definitely simplify constructors.go significantly using that approach!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mds1 - I spoke with @mslipper yesterday. I am going to refactor this pr so that we shell out to forge verify-contract as you suggested instead of reimplementing that functionality in go (and having to deal with the continued maintenance for that feature). I will reach back out when the refactor is complete

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok sounds great, thank you for the update!

Copy link
Contributor Author

@bitwiseguy bitwiseguy Feb 15, 2025

Choose a reason for hiding this comment

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

This is the most useful file to unit test. It will help us detect drift with the smart contract source code and the solidity function calls made here.

It would also be nice to detect updates in smart contract constructor args compared to what is expected here, but not sure how to do that yet. Perhaps when the contracts are deployed we store the encoded constructor args in the state.json file (could be a mapping of address --> string encoded args). That way we don't have to reconstruct the args during op-deployer verify runtime. Would make the verification less error-prone

@bitwiseguy bitwiseguy force-pushed the ss/deployer-verify-contracts branch from 6caa487 to a67339c Compare February 15, 2025 16:35
@@ -0,0 +1,323 @@
package verify
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's chat about this live next week. I see why you need to do this but it's a lot of complexity that I fear will become very brittle as the codebase evolves.

Copy link
Contributor Author

@bitwiseguy bitwiseguy Feb 17, 2025

Choose a reason for hiding this comment

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

Yea agreed on the complexity and brittleness as downsides. See this comment for an alternative approach where we always store constructor args at the time the contract is deployed, and therefore don't have to regenerate them later. For that solution, I think we'd have to update the solidity scripts to return the constructor args for each contract as script outputs

@bitwiseguy bitwiseguy force-pushed the ss/deployer-verify-contracts branch from 6f7f3d0 to b9226fe Compare February 25, 2025 20:51
@mslipper
Copy link
Collaborator

How we looking here @bitwiseguy?

@bitwiseguy
Copy link
Contributor Author

Closing in favor of #14633

@bitwiseguy bitwiseguy closed this Mar 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Automatically verify L1 contracts on etherscan

3 participants

Comments