Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/regression.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,19 @@ jobs:
--allow-source-diff 0x6F5c0A5a824773E8f8285bC5aA59ea0Aab2A6400
--allow-source-diff 0xaf35A63a4114B7481589fDD9FDB3e35Fd65fAed7
--allow-source-diff 0x647DeE7bF33e44829e6430F7A08F63b3319694f0
- config: config_samples/ethereum/mainnet/gualGovernance/dual_governance_mainnet_config.json
hardhat: hardhat_configs/mainnet_hardhat_config.ts
network: mainnet
flags: >
--allow-bytecode-diff 0xd6A67636c05BeB5B4a5c90D408b03A63c4e39426
- config: config_samples/ethereum/mainnet/gualGovernance/dual_governance_mainnet_voting_config.json
hardhat: hardhat_configs/mainnet_hardhat_config.ts
network: mainnet
flags: ""
- config: config_samples/ethereum/mainnet/gualGovernance/dual_governance_mainnet_upgrade_voting_config.json
hardhat: hardhat_configs/mainnet_hardhat_config.ts
network: mainnet
flags: ""
- config: config_samples/ethereum/mainnet/tw/tw_config.json
hardhat: hardhat_configs/mainnet_hardhat_config.ts
network: mainnet
Expand All @@ -67,6 +80,9 @@ jobs:
--allow-bytecode-diff 0x7E99eE3C66636DE415D2d7C880938F2f40f94De4
--allow-bytecode-diff 0x2F0303F20E0795E6CCd17BD5efE791A586f28E03
--allow-bytecode-diff 0x6d1a9bBFF97f7565e9532FEB7b499982848E5e07
--allow-bytecode-diff 0x933b84D2C01B04C2f53cD2FB1b7055241E122C83
--allow-bytecode-diff 0xE22486EA7cE77daE718fFa7B7114fD50CF73Cbac
--allow-bytecode-diff 0xd253b0ca059343e70474e685Beb2974F10CCFa67
- config: config_samples/ethereum/hoodi/vaults/hoodi_vaults_easy_track_config.json
hardhat: hardhat_configs/hoodi_hardhat_config.ts
network: hoodi
Expand All @@ -83,6 +99,7 @@ jobs:
--allow-source-diff 0xc5dCd2A9642ceA9B71A632BF5b8ff52424Ea1B40
env:
ETHERSCAN_EXPLORER_TOKEN: ${{ secrets.ETHERSCAN_EXPLORER_TOKEN }}
ETHERSCAN_TOKEN: ${{ secrets.ETHERSCAN_EXPLORER_TOKEN }}
GITHUB_API_TOKEN: ${{ github.token }}
LOCAL_RPC_URL: "http://127.0.0.1:7545"
REMOTE_RPC_URL: ${{ matrix.network == 'mainnet' && secrets['REMOTE_RPC_URL_MAINNET'] || secrets['REMOTE_RPC_URL_HOODI'] }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -382,15 +382,27 @@
"0xdc79D1751D1435fEc9204c03ca3D64ceEB73A7df",
"0x7b2B6EA1e53B2039a493cA587805183883Cb8B88",
"0x2291496c76CC2e9368DbE9d4977ED2623cbDfb32",
"0xBC2bb8310730F3D2b514Cb26f7e0A8776De879Ac",
"0xA48DF029Fd2e5FCECB3886c5c2F60e3625A1E87d",
"0x0534aA41907c9631fae990960bCC72d75fA7cfeD",
"0xd3545AC0286A94970BacC41D3AF676b89606204F",
"0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8",
"0x49B3512c44891bef83F8967d075121Bd1b07a01B",
"0x9CAaCCc62c66d817CC59c44780D1b722359795bF",
"0x78780e70Eae33e2935814a327f7dB6c01136cc62"
]
"0x78780e70Eae33e2935814a327f7dB6c01136cc62",
"0x284D91a7D47850d21A6DEaaC6E538AC7E5E6fc2a",
"0xBC2bb8310730F3D2b514Cb26f7e0A8776De879Ac",
"0xF21f98cac0Ba38f02b4d5be1667cc345929E8877",
"0xfEF8B796Fea42b3C68E342364Adcf88F1d6145a6",
"0x4DF806111AC58e93d90E6D2fBE8522a76be6F499",
"0x56Ff87F41a8CF795764E15E496124240Ac17695b",
"0xc3FA83D65a900303e1d99cDBBF762c6630562c04",
"0x351426775c75aB5127de860Cdcaf1953F1D622a2",
"0x83DfE5Fe8ac8b7DB38c020F4F54BF09b65D92c63",
"0xc5dCd2A9642ceA9B71A632BF5b8ff52424Ea1B40",
"0xa11906bBBBaC5207b8FDA4F7F294d7EcB8dcc758"
],
1765324800,
300
],
"0x3e144aEd003b5AE6953A99B78dD34154CF3F8c76": [
"0xb3e6a8B6A752d3bb905A1B3Ef12bbdeE77E8160e",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
"0x553337946F2FAb8911774b20025fa776B76a7CcE": "TimelockedGovernance"
},
"explorer_hostname": "api.etherscan.io",
"explorer_token_env_var": "ETHERSCAN_EXPLORER_TOKEN",
"explorer_chain_id": 1,
"github_repo": {
"url": "https://github.com/lidofinance/dual-governance",
"commit": "14b59968c98992bf51a6f1340457127ddc0cc6c2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"0x67988077f29FbA661911d9567E05cc52C51ca1B0": "DGUpgradeOmnibusMainnet"
},
"explorer_hostname": "api.etherscan.io",
"explorer_token_env_var": "ETHERSCAN_EXPLORER_TOKEN",
"explorer_chain_id": 1,
"github_repo": {
"url": "https://github.com/lidofinance/dual-governance",
"commit": "959e1766a1a27eeb5ca57dc05e4f5cae1e025f47",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"0x2a30F5aC03187674553024296bed35Aa49749DDa": "TimeConstraints"
},
"explorer_hostname": "api.etherscan.io",
"explorer_token_env_var": "ETHERSCAN_EXPLORER_TOKEN",
"explorer_chain_id": 1,
"github_repo": {
"url": "https://github.com/lidofinance/dual-governance",
"commit": "f3f26383ee130d6a8bb0d1f420151e4a311018eb",
Expand Down
130 changes: 101 additions & 29 deletions diffyscan/diffyscan.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,61 @@
ExceptionHandler,
BaseCustomException,
BinVerifierError,
CompileError,
)

__version__ = "0.0.0"


def _build_github_solc_input(
contract_source_code,
config,
github_api_token,
recursive_parsing,
cache_github,
):
solc_input = contract_source_code["solcInput"]
sources = solc_input.get("sources", solc_input)
updated_sources = {}
missing = []

for path_to_file, source in sources.items():
repo, dep_name = resolve_dep(path_to_file, config)
if not dep_name:
repo = config["github_repo"]

if recursive_parsing:
github_file = get_file_from_github_recursive(
github_api_token, repo, path_to_file, dep_name, cache_github
)
else:
github_file = get_file_from_github(
github_api_token, repo, path_to_file, dep_name, cache_github
)

if not github_file:
missing.append(path_to_file)
continue

updated_sources[path_to_file] = {"content": github_file}

if "sources" in solc_input:
github_solc_input = dict(solc_input)
github_solc_input["sources"] = updated_sources
else:
github_solc_input = {"sources": updated_sources}

return github_solc_input, missing


def run_bytecode_diff(
contract_address_from_config,
contract_name_from_config,
contract_source_code,
config,
github_api_token,
recursive_parsing,
cache_github,
deployer_account,
local_rpc_url,
remote_rpc_url,
Expand All @@ -66,9 +111,29 @@ def run_bytecode_diff(

# Get libraries from config if they exist
libraries = config.get("bytecode_comparison", {}).get("libraries", None)
github_solc_input, missing_sources = _build_github_solc_input(
contract_source_code,
config,
github_api_token,
recursive_parsing,
cache_github,
)
if missing_sources:
missing_preview = ", ".join(missing_sources[:5])
more = ""
if len(missing_sources) > 5:
more = f" (and {len(missing_sources) - 5} more)"
raise CompileError(
"missing GitHub sources for bytecode compilation; "
f"count={len(missing_sources)}; first={missing_preview}{more}"
)

github_contract_source = dict(contract_source_code)
github_contract_source["solcInput"] = github_solc_input
target_compiled_contract = compile_contract_from_explorer(
contract_source_code, libraries
github_contract_source, libraries
)
logger.okay("Compiled contract for bytecode comparison using GitHub sources")

contract_creation_code, local_compiled_bytecode, immutables = (
parse_compiled_contract(target_compiled_contract)
Expand Down Expand Up @@ -170,7 +235,10 @@ def run_source_diff(
logger.okay("Files", files_count)

if not skip_user_input:
input("Press Enter to proceed...")
if sys.stdin.isatty():
input("Press Enter to proceed...")
else:
logger.info("Skipping prompt (non-interactive stdin).")
logger.divider()

logger.info("Diffing...")
Expand Down Expand Up @@ -439,36 +507,40 @@ def process_config(
source_stats.append(source_result)

if enable_binary_comparison:
bytecode_match = run_bytecode_diff(
contract_address,
contract_name,
contract_code,
config,
deployer_account,
local_rpc_url,
remote_rpc_url,
)
bytecode_stats.append(
{
"contract_address": contract_address,
"contract_name": contract_name,
"match": bytecode_match,
}
)
try:
bytecode_match = run_bytecode_diff(
contract_address,
contract_name,
contract_code,
config,
github_api_token,
recursive_parsing,
cache_github,
deployer_account,
local_rpc_url,
remote_rpc_url,
)
bytecode_stats.append(
{
"contract_address": contract_address,
"contract_name": contract_name,
"match": bytecode_match,
}
)
except BaseCustomException as exc:
# Treat bytecode comparison errors as reportable diffs; final
# allowlist handling happens after all contracts are processed.
logger.error(str(exc))
bytecode_stats.append(
{
"contract_address": contract_address,
"contract_name": contract_name,
"match": False,
}
)
except BaseCustomException as custom_exc:
ExceptionHandler.raise_exception_or_log(custom_exc)
traceback.print_exc()
# Track failed bytecode comparison if it was a BinVerifierError
if enable_binary_comparison and isinstance(
custom_exc, BinVerifierError
):
bytecode_stats.append(
{
"contract_address": contract_address,
"contract_name": contract_name,
"match": False,
}
)
except KeyboardInterrupt:
logger.info("Keyboard interrupt by user")

Expand Down
31 changes: 24 additions & 7 deletions diffyscan/utils/binary_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,14 +143,15 @@ def _get_checkpoints_for_display(
"""
checkpoints = {0, *mismatches}

# Include last lines if instructions differ in count
if actual_instructions:
checkpoints.add(len(actual_instructions) - 1)
if expected_instructions:
checkpoints.add(len(expected_instructions) - 1)
# Bound checkpoints to the shared instruction range
max_idx = min(len(actual_instructions), len(expected_instructions)) - 1
if max_idx < 0:
return [0]

# Include last comparable line for context
checkpoints.add(max_idx)

# Expand around mismatches
max_idx = min(len(actual_instructions), len(expected_instructions)) - 1
for idx in list(checkpoints):
start_idx = max(0, idx - context_lines)
end_idx = min(idx + context_lines, max_idx)
Expand Down Expand Up @@ -198,6 +199,9 @@ def _format_instruction_diff(actual, expected, immutables):
params = bgRed(actual_params) + " " + bgGreen(expected_params)
is_immutable_only = False

if not same_opcode:
is_immutable_only = False

return (opcode, opname, params), is_immutable_only


Expand Down Expand Up @@ -272,7 +276,8 @@ def deep_match_bytecode(
logger.warn(f"Detected unknown opcodes: {unknown_opcodes}")

# Check length differences
if len(actual_instructions) != len(expected_instructions):
length_mismatch = len(actual_instructions) != len(expected_instructions)
if length_mismatch:
logger.warn("Codes have a different length")

# Validate string literals
Expand All @@ -291,6 +296,12 @@ def deep_match_bytecode(
logger.okay("Bytecodes match (after trimming metadata and string literals)")
return True

# If one side has no instructions, avoid diff rendering/index errors
if length_mismatch and (not actual_instructions or not expected_instructions):
raise BinVerifierError(
"Bytecodes have different length after trimming metadata and string literals"
)

# Display diff with context
checkpoints = _get_checkpoints_for_display(
mismatches, actual_instructions, expected_instructions
Expand All @@ -302,6 +313,12 @@ def deep_match_bytecode(
zipped_instructions, checkpoints, immutables
)

# If lengths differ, this is a bytecode mismatch (not just immutables)
if length_mismatch:
raise BinVerifierError(
"Bytecodes have different length after trimming metadata and string literals"
)

# If we found any mismatch outside immutables => fail
if not is_matched_with_excluded_immutables:
raise BinVerifierError(
Expand Down
27 changes: 27 additions & 0 deletions tests/test_binary_verifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import pytest

from diffyscan.utils.binary_verifier import deep_match_bytecode
from diffyscan.utils.custom_exceptions import BinVerifierError


def test_length_mismatch_raises():
actual = "0x6001600055fe"
expected = "0x6001600055fe6001"

with pytest.raises(BinVerifierError, match="different length"):
deep_match_bytecode(actual, expected, immutables={})


def test_immutable_only_diff_returns_false():
actual = "0x6001fe"
expected = "0x6002fe"

assert deep_match_bytecode(actual, expected, immutables={1: 1}) is False


def test_non_immutable_diff_raises():
actual = "0x6001fe"
expected = "0x6001fd"

with pytest.raises(BinVerifierError, match="differences not on the immutable"):
deep_match_bytecode(actual, expected, immutables={})
Loading