Skip to content

Conversation

@0xLaz3r
Copy link
Contributor

@0xLaz3r 0xLaz3r commented Sep 1, 2025

Enhanced Contract Verification with Retry Mechanisms and Multi-Explorer Support

🎯 Summary

Enhances scripts/verify.py to eliminate single points of failure when verifying Sky Protocol spells. Adds robust retry mechanisms and support for multiple block explorers (Etherscan + Sourcify) with automatic fallback.

🔧 Key Changes

Retry Mechanisms

  • Exponential backoff with jitter for all API calls and subprocess operations
  • Configurable retry parameters: 3 attempts, 2s base delay, 60s max delay
  • Smart error handling with detailed logging

Multi-Explorer Support

  • Etherscan: Primary verifier (requires API key)
  • Sourcify: Backup verifier (no API key required)
  • Automatic fallback: If one fails, tries the next

Enhanced Error Handling

  • Graceful degradation when verifiers are unavailable
  • Comprehensive logging of all attempts and failures
  • Better error messages with clear failure reasons

📝 Files Changed

  • scripts/verify.py - Enhanced with retry mechanisms and multi-verifier support
  • scripts/verification/README.md - Updated documentation
  • scripts/verification/test_retry.py - Simple test for the retry mechanism

⚙️ Usage (Unchanged)

make verify addr=0x1234567890123456789012345678901234567890

Benefits

  1. Reliability: Multiple verifiers eliminate single points of failure
  2. Resilience: Handles temporary network issues gracefully
  3. Transparency: Detailed logging shows exactly what's happening
  4. Compatibility: Existing workflows work unchanged

🧪 Testing

  • ✅ Python syntax validation
  • ✅ Retry mechanism verification
  • ✅ Argument validation
  • ✅ Environment variable validation

🎯 Addresses Requirements

  • ✅ Verify on at least 2 block explorers
  • ✅ Proper retry mechanisms with sane defaults
  • ✅ Maintain existing Makefile command
  • ✅ Eliminate single point of failure

No breaking changes - pure enhancement with full backward compatibility.

@DaiFoundation-DevOps
Copy link

DaiFoundation-DevOps commented Sep 1, 2025

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link

coderabbitai bot commented Sep 1, 2025

Important

Review skipped

Auto reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@amusingaxl amusingaxl left a comment

Choose a reason for hiding this comment

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

Hey there, I left some comments and suggestions.

@0xLaz3r 0xLaz3r marked this pull request as draft September 3, 2025 18:48
"""
Etherscan block explorer verifier implementation.
"""
import os
Copy link
Contributor

Choose a reason for hiding this comment

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

seems like this is unused

"""
import sys
import json
import time
Copy link
Contributor

Choose a reason for hiding this comment

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

This also seems unused

Comment on lines 6 to 11
import random
import unittest
from unittest.mock import patch, MagicMock
from typing import Tuple, Callable

from retry import retry_with_backoff, DEFAULT_MAX_RETRIES, DEFAULT_BASE_DELAY
Copy link
Contributor

Choose a reason for hiding this comment

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

random, MagicMock, Tuple, Callable, DEFAULT_MAX_RETRIES e DEFAULT_BASE_DELAY all seem unused

data['libraryaddress1'] = library_address

# Submit verification request with retry
max_retries = 3
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this hardcoded value be here? Isn't it also set somewhere else?

@0xLaz3r 0xLaz3r marked this pull request as ready for review September 24, 2025 10:30
amusingaxl
amusingaxl previously approved these changes Sep 25, 2025
Copy link
Contributor

@amusingaxl amusingaxl left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Contributor

@amusingaxl amusingaxl left a comment

Choose a reason for hiding this comment

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

Left some suggestions.

@0xLaz3r 0xLaz3r requested a review from amusingaxl October 27, 2025 19:03
Comment on lines 11 to 12
import json
from typing import Dict, Any, Optional
Copy link
Contributor

Choose a reason for hiding this comment

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

Json, Dict and Any seems to be unused.

Comment on lines 110 to 111
# ✅ Correct way - run as module
python3 -m scripts.verification.test_retry
Copy link
Contributor

Choose a reason for hiding this comment

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

The test name needs updating here.

@0xLaz3r 0xLaz3r requested a review from 0xBasset October 28, 2025 09:20
Comment on lines 76 to 77
if library_address:
cmd.extend(["--libraries", f"src/DssExecLib.sol:DssExecLib:{library_address}"])
Copy link
Contributor

Choose a reason for hiding this comment

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

Since library is present in foundry.toml, can we get rid of all library parsing/passing inside the scripts?

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed

Comment on lines +159 to +160
# Etherscan (requires API key)
if chain_id == "1" and etherscan_api_key:
Copy link
Contributor

Choose a reason for hiding this comment

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

Awareness question - would hardcoding 1 allow for verification against Tenderly Testnets?
https://docs.tenderly.co/virtual-testnets/smart-contract-frameworks/foundry#verify-existing-contracts

From my understanding, testnets created from Mainnet will report chain ID of 1, so verify.py should work with them

Copy link
Contributor

Choose a reason for hiding this comment

The 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.
I don't think we have a use case to change it regardless.

capture_output=True,
text=True,
check=True,
env=os.environ | {"ETH_GAS_PRICE": "0", "ETH_PRIO_FEE": "0"},
Copy link
Contributor

Choose a reason for hiding this comment

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

Question - why do ETH_GAS_PRICE and ETH_PRIO_FEE need to be set?

Copy link
Contributor

Choose a reason for hiding this comment

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

There was a bug in cast call that caused it to fail to execute if the sender didn't have enough gas to fund the call, even though it's supposed to be a read-only function.
Usually this command is executed in the same shell that executed the deployment, so those variables would most likely be set.
Not sure if they fixed it, but explicitly setting the gas price and the prio fee to zero was the workaround.

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)
Copy link
Contributor

Choose a reason for hiding this comment

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

Question - does it ever make sense to continue with verification when no libraries are present?

Copy link
Contributor

Choose a reason for hiding this comment

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

  1. Question - what role does this index file play?

  2. Please adjust Makefile to a correct file since currently Make cannot find ./scripts/verify.py

return chain_id


def get_library_address() -> str:
Copy link
Contributor

Choose a reason for hiding this comment

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

As mentioned in the other place, I believe that libraries should be handled by forge itself, and not parsed/provided to it

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed.

Comment on lines +38 to +39
if len(contract_address) != 42:
sys.exit('Malformed address')
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this check is necessary.
If it's not a valid address, it will fail anyway.
This is more a guard against forgetting to set the address, or setting it so something different because of bad copy-pasta.

delay = int(os.environ.get("VERIFY_DELAY", "5"))

chain_id = get_chain_id()
etherscan_api_key = os.environ.get("ETHERSCAN_API_KEY", "")
Copy link
Contributor

Choose a reason for hiding this comment

The 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 ETHERSCAN_API_KEY environment variable which causes Forge to always verify against etherscan even when other verifier has been specified.

This only happens when:

  • ETHERSCAN_API_KEY was set to a non-empty value;
  • --verifier sourcify.

This combination causes forge to never verify on Sourcify.

It is caused by this line which doesn't use sourcify when ETHERSCAN_API_KEY is there.

Comment on lines 73 to 74
if constructor_args:
cmd.extend(["--constructor-args", constructor_args])
Copy link
Contributor

Choose a reason for hiding this comment

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

Spells are not supposed to have constructor args. We can safely remove this.

Copy link
Contributor

@amusingaxl amusingaxl left a comment

Choose a reason for hiding this comment

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

Left some notes.

- 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`.
Copy link
Contributor

Choose a reason for hiding this comment

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

The CLI no longer accepts constructor args, as we're relying on foundry.toml. Please update the README do reflect that.

export VERIFY_RETRIES=5
export VERIFY_DELAY=5

python -m scripts.verification.verify DssSpell 0xYourSpellAddress [constructorArgs]
Copy link
Contributor

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.

Suggested change
python -m scripts.verification.verify DssSpell 0xYourSpellAddress [constructorArgs]
python -m scripts.verification.verify DssSpell 0x{spell_address}

and other contract-related data operations.
"""
import os
import re
Copy link
Contributor

Choose a reason for hiding this comment

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

Unused

Suggested change
import re

Comment on lines +13 to +15
# Constants
SOURCE_FILE_PATH = "src/DssSpell.sol"
LIBRARY_NAME = "DssExecLib"
Copy link
Contributor

Choose a reason for hiding this comment

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

Both unused in this file:

Suggested change
# Constants
SOURCE_FILE_PATH = "src/DssSpell.sol"
LIBRARY_NAME = "DssExecLib"

return False


def verify_contract_with_verifiers(
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably it's best to inject both chain_id and etherscan_api_key as parameters than to fetch them locally

Suggested change
def verify_contract_with_verifiers(
def verify_contract_with_verifiers(
contract_name: str,
contract_address: str,
chain_id: str,
etherscan_api_key: str,
) -> bool:

)

# Parse command line arguments
spell_name, spell_address = parse_command_line_args()
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
spell_name, spell_address = parse_command_line_args()
spell_name, spell_address = parse_command_line_args()
chain_id = get_chain_id()
etherscan_api_key = os.environ.get("ETHERSCAN_API_KEY", "")

spell_name, spell_address = parse_command_line_args()

# Verify spell contract
spell_success = verify_contract_with_verifiers(
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
spell_success = verify_contract_with_verifiers(
spell_success = verify_contract_with_verifiers(
contract_name=spell_name,
contract_address=spell_address,
chain_id=chain_id,
etherscan_api_key=etherscan_api_key,
)

print('Could not determine action contract address', file=sys.stderr)
sys.exit(1)

action_success = verify_contract_with_verifiers(
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
action_success = verify_contract_with_verifiers(
action_success = verify_contract_with_verifiers(
contract_name="DssSpellAction",
contract_address=action_address,
chain_id=chain_id,
etherscan_api_key=etherscan_api_key,
)

Comment on lines +118 to +119
retries = int(os.environ.get("VERIFY_RETRIES", "5"))
delay = int(os.environ.get("VERIFY_DELAY", "5"))
Copy link
Contributor

Choose a reason for hiding this comment

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

This function has one too many levels of abstraction going on.
Parsing environment variables and getting their default values shouldn't be part of its responsibilities.
You most likely want to inject those already parsed into the function as parameters and let the top level call deal with those.

Copy link
Contributor

Choose a reason for hiding this comment

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

Needs to resolve the conflict.

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.

6 participants