-
Notifications
You must be signed in to change notification settings - Fork 59
Retry mechanism for verification in multiple explorers #487
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 16 commits
493e14e
850b03f
7c3ec30
547d643
fd22657
4e1d4c4
408a1e6
b4a1ac2
eea1b68
3fc520f
6a49e3a
458e249
64f0aab
d1eeef3
219a333
456e91c
86e5688
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| # Contract Verification | ||
|
|
||
| Minimal verification wrapper that shells out to `forge verify-contract` per explorer, using Foundry's built-in retries and delays. | ||
|
|
||
| ## Usage | ||
|
|
||
| ```bash | ||
| # from repo root | ||
| export ETH_RPC_URL="https://..." | ||
| # optional for Etherscan | ||
| export ETHERSCAN_API_KEY="..." | ||
|
|
||
| # optional overrides (defaults: 5) | ||
| export VERIFY_RETRIES=5 | ||
| export VERIFY_DELAY=5 | ||
|
|
||
| python -m scripts.verification.verify DssSpell 0xYourSpellAddress [constructorArgs] | ||
| ``` | ||
|
|
||
| This verifies: | ||
| - The Spell contract you pass (e.g., `DssSpell`) | ||
| - The associated `DssSpellAction` via `action()` lookup | ||
|
|
||
| ## Explorers | ||
| - Sourcify: used on mainnet; no API key needed | ||
| - Etherscan: used on mainnet when `ETHERSCAN_API_KEY` is set | ||
|
|
||
| ## Notes | ||
| - Libraries: if `DssExecLib` is configured in `foundry.toml`, it is linked automatically via `--libraries`. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The CLI no longer accepts constructor args, as we're relying on |
||
| - Retries & delay: handled by `forge verify-contract` flags (`--retries`, `--delay`) per Foundry docs ([forge verify-contract](https://getfoundry.sh/forge/reference/verify-contract#forge-verify-contract)). | ||
|
|
||
| ## Examples | ||
| ```bash | ||
| # Mainnet spell, with Etherscan | ||
| ETHERSCAN_API_KEY=... python -m scripts.verification.verify DssSpell 0xabc...def | ||
|
|
||
| # Custom retries/delay | ||
| VERIFY_RETRIES=10 VERIFY_DELAY=8 python -m scripts.verification.verify DssSpell 0xabc...def | ||
| ``` | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| #!/usr/bin/env python3 | ||
| """ | ||
| Verification package for Sky Protocol spells. | ||
| """ | ||
|
|
||
| from .contract_data import get_action_address, get_chain_id, get_library_address | ||
|
|
||
| __all__ = [ | ||
| "get_chain_id", | ||
| "get_library_address", | ||
| "get_action_address", | ||
| ] |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,72 @@ | ||||||||
| #!/usr/bin/env python3 | ||||||||
| """ | ||||||||
| Contract data utilities for Sky Protocol spells verification. | ||||||||
| This module handles contract metadata extraction, source code flattening, | ||||||||
| and other contract-related data operations. | ||||||||
| """ | ||||||||
| import os | ||||||||
| import re | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unused
Suggested change
|
||||||||
| import subprocess | ||||||||
| import sys | ||||||||
| from typing import Optional | ||||||||
|
|
||||||||
| # Constants | ||||||||
| SOURCE_FILE_PATH = "src/DssSpell.sol" | ||||||||
| LIBRARY_NAME = "DssExecLib" | ||||||||
|
Comment on lines
+13
to
+15
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both unused in this file:
Suggested change
|
||||||||
|
|
||||||||
|
|
||||||||
| def get_chain_id() -> str: | ||||||||
| """Get the current chain ID.""" | ||||||||
| print("Obtaining chain ID... ") | ||||||||
| result = subprocess.run( | ||||||||
| ["cast", "chain-id"], capture_output=True, text=True, check=True | ||||||||
| ) | ||||||||
| chain_id = result.stdout.strip() | ||||||||
| print(f"CHAIN_ID: {chain_id}") | ||||||||
| return chain_id | ||||||||
|
|
||||||||
|
|
||||||||
| def get_library_address() -> str: | ||||||||
|
||||||||
| """Find the DssExecLib address from foundry.toml.""" | ||||||||
| library_address = "" | ||||||||
|
|
||||||||
| # Try to read from foundry.toml libraries | ||||||||
| if os.path.exists("foundry.toml"): | ||||||||
| try: | ||||||||
| with open("foundry.toml", "r") as f: | ||||||||
| config = f.read() | ||||||||
|
|
||||||||
| result = re.search(r":DssExecLib:(0x[0-9a-fA-F]{40})", config) | ||||||||
| if result: | ||||||||
| library_address = result.group(1) | ||||||||
| print(f"Using library {LIBRARY_NAME} at address {library_address}") | ||||||||
| return library_address | ||||||||
| else: | ||||||||
| print("No DssExecLib configured in foundry.toml", file=sys.stderr) | ||||||||
| except Exception as e: | ||||||||
| print(f"Error reading foundry.toml: {str(e)}", file=sys.stderr) | ||||||||
| else: | ||||||||
| print("No foundry.toml found", file=sys.stderr) | ||||||||
|
|
||||||||
| # If we get here, no library address was found | ||||||||
| print("WARNING: Assuming this contract uses no libraries", file=sys.stderr) | ||||||||
|
||||||||
| return "" | ||||||||
|
|
||||||||
|
|
||||||||
| def get_action_address(spell_address: str) -> Optional[str]: | ||||||||
| """Get the action contract address from the spell contract.""" | ||||||||
| try: | ||||||||
| result = subprocess.run( | ||||||||
| ["cast", "call", spell_address, "action()(address)"], | ||||||||
| capture_output=True, | ||||||||
| text=True, | ||||||||
| check=True, | ||||||||
| env=os.environ | {"ETH_GAS_PRICE": "0", "ETH_PRIO_FEE": "0"}, | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question - why do
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a bug in |
||||||||
| ) | ||||||||
| return result.stdout.strip() | ||||||||
| except subprocess.CalledProcessError as e: | ||||||||
| print(f"Error getting action address: {str(e)}", file=sys.stderr) | ||||||||
| return None | ||||||||
| except Exception as e: | ||||||||
| print(f"Unexpected error getting action address: {str(e)}", file=sys.stderr) | ||||||||
| return None | ||||||||
amusingaxl marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,234 @@ | ||||||||||||||||
| #!/usr/bin/env python3 | ||||||||||||||||
| """ | ||||||||||||||||
| Enhanced contract verification script for Sky Protocol spells on mainnet. | ||||||||||||||||
| This script verifies both the DssSpell and DssSpellAction contracts on multiple block explorers | ||||||||||||||||
| using forge verify-contract --flatten with robust retry mechanisms and fallback options. | ||||||||||||||||
| """ | ||||||||||||||||
| import os | ||||||||||||||||
| import sys | ||||||||||||||||
| import subprocess | ||||||||||||||||
| from typing import Tuple, List | ||||||||||||||||
|
|
||||||||||||||||
| from . import get_chain_id, get_library_address, get_action_address | ||||||||||||||||
|
|
||||||||||||||||
| # Constants | ||||||||||||||||
| SOURCE_FILE_PATH = 'src/DssSpell.sol' | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| def get_env_var(var_name: str, error_message: str) -> str: | ||||||||||||||||
| """Get environment variable with error handling.""" | ||||||||||||||||
| try: | ||||||||||||||||
| return os.environ[var_name] | ||||||||||||||||
| except KeyError: | ||||||||||||||||
| print(f" {error_message}", file=sys.stderr) | ||||||||||||||||
| sys.exit(1) | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| def parse_command_line_args() -> Tuple[str, str, str]: | ||||||||||||||||
| """Parse command line arguments.""" | ||||||||||||||||
| if len(sys.argv) not in [3, 4]: | ||||||||||||||||
| print("""usage: | ||||||||||||||||
| ./verify.py <contractname> <address> [constructorArgs] | ||||||||||||||||
| """, file=sys.stderr) | ||||||||||||||||
| sys.exit(1) | ||||||||||||||||
|
|
||||||||||||||||
| contract_name = sys.argv[1] | ||||||||||||||||
| contract_address = sys.argv[2] | ||||||||||||||||
|
|
||||||||||||||||
| if len(contract_address) != 42: | ||||||||||||||||
| sys.exit('Malformed address') | ||||||||||||||||
|
Comment on lines
+38
to
+39
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: this can check against a regular expression which is safer for address detection since it also checks hex-format
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this check is necessary. |
||||||||||||||||
|
|
||||||||||||||||
| constructor_args = '' | ||||||||||||||||
| if len(sys.argv) == 4: | ||||||||||||||||
| constructor_args = sys.argv[3] | ||||||||||||||||
|
|
||||||||||||||||
| return contract_name, contract_address, constructor_args | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| def build_forge_cmd( | ||||||||||||||||
| verifier: str, | ||||||||||||||||
| address: str, | ||||||||||||||||
| contract_name: str, | ||||||||||||||||
| constructor_args: str, | ||||||||||||||||
| library_address: str, | ||||||||||||||||
| retries: int, | ||||||||||||||||
| delay: int, | ||||||||||||||||
| etherscan_api_key: str = "", | ||||||||||||||||
| ) -> List[str]: | ||||||||||||||||
| cmd: List[str] = [ | ||||||||||||||||
| "forge", | ||||||||||||||||
| "verify-contract", | ||||||||||||||||
| address, | ||||||||||||||||
| f"{SOURCE_FILE_PATH}:{contract_name}", | ||||||||||||||||
| "--verifier", | ||||||||||||||||
| verifier, | ||||||||||||||||
| "--flatten", | ||||||||||||||||
| "--watch", | ||||||||||||||||
| "--retries", | ||||||||||||||||
| str(retries), | ||||||||||||||||
| "--delay", | ||||||||||||||||
| str(delay), | ||||||||||||||||
| ] | ||||||||||||||||
|
|
||||||||||||||||
| if constructor_args: | ||||||||||||||||
| cmd.extend(["--constructor-args", constructor_args]) | ||||||||||||||||
|
||||||||||||||||
|
|
||||||||||||||||
| if library_address: | ||||||||||||||||
| cmd.extend(["--libraries", f"src/DssExecLib.sol:DssExecLib:{library_address}"]) | ||||||||||||||||
|
||||||||||||||||
|
|
||||||||||||||||
| if verifier == "etherscan" and etherscan_api_key: | ||||||||||||||||
| cmd.extend(["--etherscan-api-key", etherscan_api_key]) | ||||||||||||||||
|
|
||||||||||||||||
| return cmd | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| def verify_once_on( | ||||||||||||||||
| verifier: str, | ||||||||||||||||
| address: str, | ||||||||||||||||
| contract_name: str, | ||||||||||||||||
| constructor_args: str, | ||||||||||||||||
| library_address: str, | ||||||||||||||||
| retries: int, | ||||||||||||||||
| delay: int, | ||||||||||||||||
| etherscan_api_key: str = "", | ||||||||||||||||
| ) -> bool: | ||||||||||||||||
| cmd = build_forge_cmd( | ||||||||||||||||
| verifier=verifier, | ||||||||||||||||
| address=address, | ||||||||||||||||
| contract_name=contract_name, | ||||||||||||||||
| constructor_args=constructor_args, | ||||||||||||||||
| library_address=library_address, | ||||||||||||||||
| retries=retries, | ||||||||||||||||
| delay=delay, | ||||||||||||||||
| etherscan_api_key=etherscan_api_key, | ||||||||||||||||
| ) | ||||||||||||||||
|
|
||||||||||||||||
| print(f"\nVerifying {contract_name} at {address} on {verifier}...") | ||||||||||||||||
| try: | ||||||||||||||||
| result = subprocess.run( | ||||||||||||||||
| cmd, | ||||||||||||||||
| capture_output=True, | ||||||||||||||||
| text=True, | ||||||||||||||||
| check=True, | ||||||||||||||||
| ) | ||||||||||||||||
| # forge prints useful info; surface stdout | ||||||||||||||||
| if result.stdout: | ||||||||||||||||
| print(result.stdout.strip()) | ||||||||||||||||
| print(f"β {verifier} verification OK") | ||||||||||||||||
| return True | ||||||||||||||||
| except subprocess.CalledProcessError as e: | ||||||||||||||||
| combined = (e.stdout or "") + "\n" + (e.stderr or "") | ||||||||||||||||
| if "already verified" in combined.lower(): | ||||||||||||||||
| print(f"β {verifier}: already verified") | ||||||||||||||||
| return True | ||||||||||||||||
| print(f"β {verifier} verification failed\n{combined}", file=sys.stderr) | ||||||||||||||||
| return False | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| def verify_contract_with_verifiers( | ||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably it's best to inject both
Suggested change
|
||||||||||||||||
| contract_name: str, | ||||||||||||||||
| contract_address: str, | ||||||||||||||||
| constructor_args: str, | ||||||||||||||||
| library_address: str | ||||||||||||||||
| ) -> bool: | ||||||||||||||||
| """Verify contract by issuing forge commands per explorer.""" | ||||||||||||||||
| # Configure retries/delay via env or defaults | ||||||||||||||||
| retries = int(os.environ.get("VERIFY_RETRIES", "5")) | ||||||||||||||||
| delay = int(os.environ.get("VERIFY_DELAY", "5")) | ||||||||||||||||
|
Comment on lines
+118
to
+119
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function has one too many levels of abstraction going on. |
||||||||||||||||
|
|
||||||||||||||||
| chain_id = get_chain_id() | ||||||||||||||||
| etherscan_api_key = os.environ.get("ETHERSCAN_API_KEY", "") | ||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you please check for the presence of a bug caused by the presence of a non-empty This only happens when:
This combination causes forge to never verify on Sourcify. It is caused by this line which doesn't use sourcify when |
||||||||||||||||
|
|
||||||||||||||||
| successes = 0 | ||||||||||||||||
|
|
||||||||||||||||
| # Sourcify (works without API key); blockscout pulls from it | ||||||||||||||||
| if chain_id == "1": | ||||||||||||||||
| if verify_once_on( | ||||||||||||||||
| verifier="sourcify", | ||||||||||||||||
| address=contract_address, | ||||||||||||||||
| contract_name=contract_name, | ||||||||||||||||
| constructor_args=constructor_args, | ||||||||||||||||
| library_address=library_address, | ||||||||||||||||
| retries=retries, | ||||||||||||||||
| delay=delay, | ||||||||||||||||
| ): | ||||||||||||||||
| successes += 1 | ||||||||||||||||
| else: | ||||||||||||||||
| print(f"Sourcify not configured for CHAIN_ID {chain_id}, skipping.") | ||||||||||||||||
|
|
||||||||||||||||
| # Etherscan (requires API key) | ||||||||||||||||
| if chain_id == "1" and etherscan_api_key: | ||||||||||||||||
|
Comment on lines
+139
to
+140
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Awareness question - would hardcoding From my understanding, testnets created from Mainnet will report chain ID of 1, so
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, as long as we don't change the chain ID for Tenderly Virtual Testnets, it's fine. |
||||||||||||||||
| if verify_once_on( | ||||||||||||||||
| verifier="etherscan", | ||||||||||||||||
| address=contract_address, | ||||||||||||||||
| contract_name=contract_name, | ||||||||||||||||
| constructor_args=constructor_args, | ||||||||||||||||
| library_address=library_address, | ||||||||||||||||
| retries=retries, | ||||||||||||||||
| delay=delay, | ||||||||||||||||
| etherscan_api_key=etherscan_api_key, | ||||||||||||||||
| ): | ||||||||||||||||
| successes += 1 | ||||||||||||||||
| elif chain_id == "1": | ||||||||||||||||
| print("ETHERSCAN_API_KEY not set; skipping Etherscan.") | ||||||||||||||||
|
|
||||||||||||||||
| return successes > 0 | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| def main(): | ||||||||||||||||
| """Main entry point for the enhanced verification script.""" | ||||||||||||||||
| try: | ||||||||||||||||
| # Get environment variables | ||||||||||||||||
| get_env_var( | ||||||||||||||||
| 'ETH_RPC_URL', | ||||||||||||||||
| "You need a valid ETH_RPC_URL.\n" | ||||||||||||||||
| "Get a public one at https://chainlist.org/ or provide your own\n" | ||||||||||||||||
| "Then export it with `export ETH_RPC_URL=https://....'" | ||||||||||||||||
| ) | ||||||||||||||||
|
|
||||||||||||||||
| # Parse command line arguments | ||||||||||||||||
| spell_name, spell_address, constructor_args = parse_command_line_args() | ||||||||||||||||
|
|
||||||||||||||||
| # Get library address | ||||||||||||||||
| library_address = get_library_address() | ||||||||||||||||
|
|
||||||||||||||||
| # Verify spell contract | ||||||||||||||||
| spell_success = verify_contract_with_verifiers( | ||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
| contract_name=spell_name, | ||||||||||||||||
| contract_address=spell_address, | ||||||||||||||||
| constructor_args=constructor_args, | ||||||||||||||||
| library_address=library_address | ||||||||||||||||
| ) | ||||||||||||||||
|
|
||||||||||||||||
| if not spell_success: | ||||||||||||||||
| print("Failed to verify spell contract", file=sys.stderr) | ||||||||||||||||
| sys.exit(1) | ||||||||||||||||
|
|
||||||||||||||||
| # Get and verify action contract | ||||||||||||||||
| action_address = get_action_address(spell_address) | ||||||||||||||||
| if not action_address: | ||||||||||||||||
| print('Could not determine action contract address', file=sys.stderr) | ||||||||||||||||
| sys.exit(1) | ||||||||||||||||
|
|
||||||||||||||||
| action_success = verify_contract_with_verifiers( | ||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
| contract_name="DssSpellAction", | ||||||||||||||||
| contract_address=action_address, | ||||||||||||||||
| constructor_args=constructor_args, | ||||||||||||||||
| library_address=library_address | ||||||||||||||||
| ) | ||||||||||||||||
|
|
||||||||||||||||
| if not action_success: | ||||||||||||||||
| print("Failed to verify action contract", file=sys.stderr) | ||||||||||||||||
| sys.exit(1) | ||||||||||||||||
|
|
||||||||||||||||
| print('\nπ All verifications complete!') | ||||||||||||||||
|
|
||||||||||||||||
| except Exception as e: | ||||||||||||||||
| print(f'\nError: {str(e)}', file=sys.stderr) | ||||||||||||||||
| sys.exit(1) | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| if __name__ == "__main__": | ||||||||||||||||
| main() | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Constructor args are not supported anymore.