Skip to content
Draft
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
46 changes: 46 additions & 0 deletions tests/test_known_addresses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Tests for utils/known_addresses and its address-resolver integration."""

import unittest
from unittest.mock import patch

from utils import known_addresses
from utils.address_resolver import resolve_address_label


class TestKnownAddressesLookup(unittest.TestCase):
def test_chain_agnostic_burn_address(self) -> None:
label = known_addresses.lookup(1, "0x000000000000000000000000000000000000dEaD")
self.assertIn("Burn", label)

def test_case_insensitive(self) -> None:
self.assertEqual(
known_addresses.lookup(1, "0x000000000000000000000000000000000000DEAD"),
known_addresses.lookup(8453, "0x000000000000000000000000000000000000dead"),
)

def test_unknown_returns_empty(self) -> None:
self.assertEqual(known_addresses.lookup(1, "0x" + "ab" * 20), "")

def test_empty_address(self) -> None:
self.assertEqual(known_addresses.lookup(1, ""), "")

def test_chain_specific_takes_precedence(self) -> None:
addr = "0x" + "11" * 20
with patch.dict(known_addresses._BY_CHAIN, {(1, addr): "Yearn yChad"}, clear=False):
self.assertEqual(known_addresses.lookup(1, addr), "Yearn yChad")
# Different chain → no chain-specific entry, falls through to "".
self.assertEqual(known_addresses.lookup(8453, addr), "")


class TestResolverIntegration(unittest.TestCase):
def test_known_address_backend_wins(self) -> None:
addr = "0x" + "22" * 20
# Registry hit should short-circuit before any network backend runs.
with patch.dict(known_addresses._BY_CHAIN, {(1, addr): "Yearn dev multisig"}, clear=False):
with patch("utils.address_resolver._etherscan_backend", return_value="ShouldNotWin") as etherscan:
self.assertEqual(resolve_address_label(1, addr), "Yearn dev multisig")
etherscan.assert_not_called()


if __name__ == "__main__":
unittest.main()
44 changes: 43 additions & 1 deletion tests/test_risk_anchors.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import unittest

from utils.risk_anchors import RiskAnchor, format_anchors_block, lookup
from eth_utils import function_signature_to_4byte_selector

from utils.risk_anchors import _ANCHORS, RiskAnchor, format_anchors_block, lookup


class TestLookup(unittest.TestCase):
Expand Down Expand Up @@ -49,5 +51,45 @@ def test_renders_multiple_anchors(self) -> None:
self.assertIn("transferOwnership(address)", block)


class TestAnchorRegistryIntegrity(unittest.TestCase):
"""Guards over the whole _ANCHORS table."""

_VALID_LEVELS = {"LOW", "MEDIUM", "HIGH", "CRITICAL"}

def test_all_keys_wellformed(self) -> None:
for selector in _ANCHORS:
self.assertTrue(selector.startswith("0x") and len(selector) == 10, f"bad selector {selector}")
self.assertEqual(selector, selector.lower(), f"selector not lowercase: {selector}")

def test_all_levels_valid(self) -> None:
for selector, anchor in _ANCHORS.items():
self.assertIn(anchor.level, self._VALID_LEVELS, f"{selector} has invalid level {anchor.level}")
self.assertTrue(anchor.rationale, f"{selector} missing rationale")

def test_selectors_match_signatures(self) -> None:
# Recompute the selector for each anchored signature to catch typos.
expected = {
"acceptOwnership()": "0x79ba5097",
"mint(address,uint256)": "0x40c10f19",
"addOwnerWithThreshold(address,uint256)": "0x0d582f13",
"removeOwner(address,address,uint256)": "0xf8dc5dd9",
"swapOwner(address,address,address)": "0xe318b52b",
"changeThreshold(uint256)": "0x694e80c3",
"enableModule(address)": "0x610b5925",
"disableModule(address,address)": "0xe009cfde",
"setGuard(address)": "0xe19a9dd9",
"setFallbackHandler(address)": "0xf08a0323",
}
for sig, selector in expected.items():
self.assertEqual("0x" + function_signature_to_4byte_selector(sig).hex(), selector, sig)
self.assertIn(selector, _ANCHORS, f"{sig} ({selector}) not registered")

def test_safe_module_is_critical(self) -> None:
# enableModule can move funds with no owner signatures — highest band.
anchor = lookup("0x610b5925")
assert anchor is not None
self.assertEqual(anchor.level, "CRITICAL")


if __name__ == "__main__":
unittest.main()
8 changes: 8 additions & 0 deletions utils/address_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@
Backend = Callable[[int, str], str]


def _known_address_backend(chain_id: int, address: str) -> str:
"""Curated registry of multisigs / EOAs / burn addresses. No IO."""
from utils.known_addresses import lookup

return lookup(chain_id, address)


def _safe_utility_backend(chain_id: int, address: str) -> str:
"""Canonical Safe utilities (MultiSendCallOnly, SignMessageLib, …). No IO."""
from safe.multisend import safe_utility_label
Expand Down Expand Up @@ -66,6 +73,7 @@ def _etherscan_backend(chain_id: int, address: str) -> str:
# actually swaps what the chain calls. Also lets callers `register_backend` a
# new function attached to this module and have it resolve correctly.
_BACKEND_NAMES: list[str] = [
"_known_address_backend",
"_safe_utility_backend",
"_swiss_knife_backend",
"_etherscan_backend",
Expand Down
38 changes: 38 additions & 0 deletions utils/known_addresses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Curated address → label registry.

The highest-priority label source, ahead of Etherscan / swiss-knife, for
addresses those backends don't name usefully — governance multisigs, known
EOAs, and canonical burn addresses. A correct label here lets the LLM reason
about *who* an address is (e.g. ``grantRole`` to a known multisig reads very
differently from ``grantRole`` to an unknown EOA).

Keys are lowercase hex. Labels are either chain-agnostic (``_CHAIN_AGNOSTIC``,
e.g. burn addresses that mean the same everywhere) or chain-specific
(``_BY_CHAIN``, keyed by ``(chain_id, address)``).

Populate ``_BY_CHAIN`` per deployment with the multisigs/EOAs you care about —
only add an address you have independently verified, since a wrong label is
worse than none.
"""

# Same meaning on every chain.
_CHAIN_AGNOSTIC: dict[str, str] = {
"0x0000000000000000000000000000000000000000": "Null address (0x0)",
"0x000000000000000000000000000000000000dead": "Burn address (0x…dEaD)",
}

# (chain_id, lowercase address) → label. Curate per deployment, e.g.:
# (1, "0x....."): "Yearn yChad (main multisig)",
# (1, "0x....."): "Yearn dev multisig (ySafe)",
_BY_CHAIN: dict[tuple[int, str], str] = {}


def lookup(chain_id: int, address: str) -> str:
"""Return a curated label for ``address``, or "" if none is registered.

Chain-specific entries take precedence over chain-agnostic ones.
"""
if not address:
return ""
addr = address.lower()
return _BY_CHAIN.get((chain_id, addr)) or _CHAIN_AGNOSTIC.get(addr, "")
12 changes: 12 additions & 0 deletions utils/risk_anchors.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ class RiskAnchor:
# Ownership — irreversible authority change
"0xf2fde38b": RiskAnchor("HIGH", "transferOwnership(): hands over full admin control"),
"0x715018a6": RiskAnchor("HIGH", "renounceOwnership(): irrevocably abandons admin"),
"0x79ba5097": RiskAnchor("HIGH", "acceptOwnership(): completes an Ownable2Step handover"),
# Token supply
"0x40c10f19": RiskAnchor("MEDIUM", "mint(address,uint256): new supply — elevate to HIGH if large or unbacked"),
# Proxy upgrades — replaces all code; impl-diff section should drive the verdict
"0x3659cfe6": RiskAnchor("HIGH", "upgradeTo(): replaces all implementation code"),
"0x4f1ef286": RiskAnchor("HIGH", "upgradeToAndCall(): replaces code AND runs initializer"),
Expand All @@ -50,6 +53,15 @@ class RiskAnchor:
"0x0e18b681": RiskAnchor("HIGH", "acceptAdmin(): completes an admin handover"),
# Diamond / facet operations
"0x1f931c1c": RiskAnchor("HIGH", "diamondCut(): replaces/adds/removes selectors — bytecode-level change"),
# Gnosis Safe self-administration — changes who/what can move the multisig's funds
"0x0d582f13": RiskAnchor("HIGH", "addOwnerWithThreshold(): adds a Safe signer"),
"0xf8dc5dd9": RiskAnchor("HIGH", "removeOwner(): removes a Safe signer"),
"0xe318b52b": RiskAnchor("HIGH", "swapOwner(): replaces a Safe signer"),
"0x694e80c3": RiskAnchor("HIGH", "changeThreshold(): changes signatures required to execute"),
"0x610b5925": RiskAnchor("CRITICAL", "enableModule(): a module can move funds with NO owner signatures"),
"0xe009cfde": RiskAnchor("MEDIUM", "disableModule(): removes a module — usually defensive"),
"0xe19a9dd9": RiskAnchor("HIGH", "setGuard(): a guard can permit or block every Safe transaction"),
"0xf08a0323": RiskAnchor("MEDIUM", "setFallbackHandler(): changes the Safe's fallback behavior"),
}


Expand Down