From 355b662b9dc610bc47bc1c2e210af34b276de99c Mon Sep 17 00:00:00 2001 From: spalen0 Date: Fri, 23 Oct 2026 12:10:35 +0000 Subject: [PATCH 1/3] fix(llm): rate governance votes on what they execute Strategist Multisig nonce 3376 only called Voter.vote_yea for proposals 2-5, moving no funds and enacting nothing (execution is a separate Voting.execute), yet was tagged MEDIUM because the proposal contents were unverified. - risk anchors: vote_yea/vote_nay (Yearn Voter) and Governor castVote/castVoteWithReason anchor at LOW - system prompt: missing context about effects the transaction does not execute goes under "Not verified" and does not raise the tag; unverified targets and undecoded payloads still do Co-Authored-By: Claude Opus 5.5 --- tests/test_risk_anchors.py | 12 ++++++++++++ utils/llm/ai_explainer.py | 5 +++++ utils/risk_anchors.py | 14 ++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/tests/test_risk_anchors.py b/tests/test_risk_anchors.py index f9bae60b..ac84af2c 100644 --- a/tests/test_risk_anchors.py +++ b/tests/test_risk_anchors.py @@ -18,6 +18,18 @@ def test_known_low_risk_selector(self) -> None: assert anchor is not None self.assertEqual(anchor.level, "LOW") + def test_governance_votes_are_low(self) -> None: + for sig in ( + "vote_yea(address,uint256)", + "vote_nay(address,uint256)", + "castVote(uint256,uint8)", + "castVoteWithReason(uint256,uint8,string)", + ): + with self.subTest(sig): + anchor = lookup("0x" + function_signature_to_4byte_selector(sig).hex()) + assert anchor is not None + self.assertEqual(anchor.level, "LOW") + def test_case_insensitive(self) -> None: self.assertIsNotNone(lookup("0xF2FDE38B")) diff --git a/utils/llm/ai_explainer.py b/utils/llm/ai_explainer.py index b1402a58..5d0a62d4 100644 --- a/utils/llm/ai_explainer.py +++ b/utils/llm/ai_explainer.py @@ -181,6 +181,11 @@ only by a privileged manual update_debt (outside the default queue and the DebtAllocator), limits depositors' withdrawals — say so in the TLDR and rate it at least MEDIUM, even when no value leaves the protocol. +- Base the risk tag on what this transaction executes. Missing context about something it + does not execute — what a voted-on proposal would do, how a recipient later uses funds — + goes under `### Not verified` and does not raise the tag. A vote that moves no funds and + enacts nothing is LOW even when the proposal contents are unknown. This does not soften + the rules for unverified targets or undecoded payloads, which do execute here. - When a Risk Anchors section is provided, treat it as a typical floor/ceiling, not a verdict. Adjust up or down based on the specific parameters (e.g. grantRole of a minor role can be LOW; an upgrade to fresh-bytecode code can be CRITICAL). diff --git a/utils/risk_anchors.py b/utils/risk_anchors.py index a4042dba..9704aa11 100644 --- a/utils/risk_anchors.py +++ b/utils/risk_anchors.py @@ -73,6 +73,20 @@ class RiskAnchor: "diamondCut((address,uint8,bytes4[])[],address,bytes)": RiskAnchor( "HIGH", "diamondCut(): replaces/adds/removes selectors — bytecode-level change" ), + # Governance votes — record a ballot only; a passed proposal still needs its own execute() + # call, which is judged on its actions when it is queued. + "vote_yea(address,uint256)": RiskAnchor( + "LOW", "vote_yea() via Yearn Voter: records a yes vote; moves no funds and executes nothing" + ), + "vote_nay(address,uint256)": RiskAnchor( + "LOW", "vote_nay() via Yearn Voter: records a no vote; moves no funds and executes nothing" + ), + "castVote(uint256,uint8)": RiskAnchor( + "LOW", "castVote() on a Governor: records a vote; moves no funds and executes nothing" + ), + "castVoteWithReason(uint256,uint8,string)": RiskAnchor( + "LOW", "castVoteWithReason() on a Governor: records a vote; moves no funds and executes nothing" + ), # Gnosis Safe self-administration — changes who/what can move the multisig's funds "addOwnerWithThreshold(address,uint256)": RiskAnchor("HIGH", "addOwnerWithThreshold(): adds a Safe signer"), "removeOwner(address,address,uint256)": RiskAnchor("HIGH", "removeOwner(): removes a Safe signer"), From fc49f9b52f1c467a8d8bfb21bbd6c6ed7f2c30f1 Mon Sep 17 00:00:00 2001 From: spalen0 Date: Fri, 23 Oct 2026 12:21:30 +0000 Subject: [PATCH 2/3] fix(llm): anchor common governance vote signatures at LOW Generalize the vote anchors beyond Yearn's Voter: OpenZeppelin Governor / Compound Bravo castVote* (incl. BySig), GovernorAlpha castVote(uint256,bool), Aave v2 submitVote and Curve-style vote_for_gauge_weights all anchor LOW. Aragon-style vote(uint256,bool,bool) / votePct(...,bool) run the vote's script in the same tx when `_executesIfDecided` is set and the vote decides it, so they are LOW only with the flag off and MEDIUM ("judge that script") with it on. delegate(address) stays unanchored: it hands voting power to someone else. Co-Authored-By: Claude Opus 5.5 --- tests/test_risk_anchors.py | 44 ++++++++++++++++++-- utils/risk_anchors.py | 82 +++++++++++++++++++++++++++++++------- 2 files changed, 109 insertions(+), 17 deletions(-) diff --git a/tests/test_risk_anchors.py b/tests/test_risk_anchors.py index ac84af2c..651d57f5 100644 --- a/tests/test_risk_anchors.py +++ b/tests/test_risk_anchors.py @@ -7,6 +7,10 @@ from utils.risk_anchors import _ANCHORS, _ANCHORS_BY_SIGNATURE, RiskAnchor, format_anchors_block, lookup +def _selector(sig: str) -> str: + return "0x" + function_signature_to_4byte_selector(sig).hex() + + class TestLookup(unittest.TestCase): def test_known_high_risk_selector(self) -> None: anchor = lookup("0xf2fde38b") # transferOwnership(address) @@ -20,16 +24,25 @@ def test_known_low_risk_selector(self) -> None: def test_governance_votes_are_low(self) -> None: for sig in ( - "vote_yea(address,uint256)", - "vote_nay(address,uint256)", "castVote(uint256,uint8)", "castVoteWithReason(uint256,uint8,string)", + "castVoteWithReasonAndParams(uint256,uint8,string,bytes)", + "castVoteBySig(uint256,uint8,address,bytes)", + "castVoteBySig(uint256,uint8,uint8,bytes32,bytes32)", + "castVote(uint256,bool)", + "submitVote(uint256,bool)", + "vote_yea(address,uint256)", + "vote_nay(address,uint256)", + "vote_for_gauge_weights(address,uint256)", ): with self.subTest(sig): - anchor = lookup("0x" + function_signature_to_4byte_selector(sig).hex()) + anchor = lookup(_selector(sig)) assert anchor is not None self.assertEqual(anchor.level, "LOW") + def test_delegation_is_not_anchored_as_a_vote(self) -> None: + self.assertIsNone(lookup(_selector("delegate(address)"))) + def test_case_insensitive(self) -> None: self.assertIsNotNone(lookup("0xF2FDE38B")) @@ -146,5 +159,30 @@ def test_safe_module_is_critical(self) -> None: self.assertEqual(anchor.level, "CRITICAL") +class TestExecutesIfDecidedRefinement(unittest.TestCase): + """Aragon-style votes execute the vote's script when ``_executesIfDecided`` is set.""" + + def test_flag_off_is_a_plain_low_vote(self) -> None: + anchor = lookup(_selector("vote(uint256,bool,bool)"), [("uint256", 7), ("bool", True), ("bool", False)]) + assert anchor is not None + self.assertEqual(anchor.level, "LOW") + self.assertIn("_executesIfDecided is false", anchor.rationale) + + def test_flag_on_says_the_script_may_execute(self) -> None: + anchor = lookup( + _selector("votePct(uint256,uint256,uint256,bool)"), + [("uint256", 7), ("uint256", 10**18), ("uint256", 0), ("bool", True)], + ) + assert anchor is not None + self.assertEqual(anchor.level, "MEDIUM") + self.assertIn("judge that script", anchor.rationale) + + def test_without_params_the_anchor_covers_both_cases(self) -> None: + anchor = lookup(_selector("vote(uint256,bool,bool)")) + assert anchor is not None + self.assertEqual(anchor.level, "MEDIUM") + self.assertIn("LOW if _executesIfDecided is false", anchor.rationale) + + if __name__ == "__main__": unittest.main() diff --git a/utils/risk_anchors.py b/utils/risk_anchors.py index 9704aa11..8eb9fa2d 100644 --- a/utils/risk_anchors.py +++ b/utils/risk_anchors.py @@ -11,6 +11,7 @@ implementation isn't LOW. Skipped intentionally: +- delegate(address) — hands voting power to someone else; not a vote. - ERC20 transfer/approve — too context-dependent (rewards distribution vs exit liquidity look identical). - Generic timelock schedule/execute — the inner call is the thing being @@ -73,20 +74,6 @@ class RiskAnchor: "diamondCut((address,uint8,bytes4[])[],address,bytes)": RiskAnchor( "HIGH", "diamondCut(): replaces/adds/removes selectors — bytecode-level change" ), - # Governance votes — record a ballot only; a passed proposal still needs its own execute() - # call, which is judged on its actions when it is queued. - "vote_yea(address,uint256)": RiskAnchor( - "LOW", "vote_yea() via Yearn Voter: records a yes vote; moves no funds and executes nothing" - ), - "vote_nay(address,uint256)": RiskAnchor( - "LOW", "vote_nay() via Yearn Voter: records a no vote; moves no funds and executes nothing" - ), - "castVote(uint256,uint8)": RiskAnchor( - "LOW", "castVote() on a Governor: records a vote; moves no funds and executes nothing" - ), - "castVoteWithReason(uint256,uint8,string)": RiskAnchor( - "LOW", "castVoteWithReason() on a Governor: records a vote; moves no funds and executes nothing" - ), # Gnosis Safe self-administration — changes who/what can move the multisig's funds "addOwnerWithThreshold(address,uint256)": RiskAnchor("HIGH", "addOwnerWithThreshold(): adds a Safe signer"), "removeOwner(address,address,uint256)": RiskAnchor("HIGH", "removeOwner(): removes a Safe signer"), @@ -98,6 +85,46 @@ class RiskAnchor: "setFallbackHandler(address)": RiskAnchor("MEDIUM", "setFallbackHandler(): changes the Safe's fallback behavior"), } +# Governance votes record a ballot and nothing else: a passed proposal still needs its +# own execute() call, which is judged on its actions when it is queued. Delegation +# (`delegate(address)`) is deliberately absent — it hands voting power to someone else. +_VOTE_RATIONALE = "records a governance vote; moves no funds and executes nothing" +_VOTE_SIGNATURES = ( + # OpenZeppelin Governor / Compound GovernorBravo + "castVote(uint256,uint8)", + "castVoteWithReason(uint256,uint8,string)", + "castVoteWithReasonAndParams(uint256,uint8,string,bytes)", + "castVoteBySig(uint256,uint8,address,bytes)", + "castVoteBySig(uint256,uint8,uint8,bytes32,bytes32)", + # Compound GovernorAlpha + "castVote(uint256,bool)", + # Aave governance v2 + "submitVote(uint256,bool)", + # Yearn Voter + "vote_yea(address,uint256)", + "vote_nay(address,uint256)", +) +_ANCHORS_BY_SIGNATURE.update( + {sig: RiskAnchor("LOW", f"{sig.split('(')[0]}(): {_VOTE_RATIONALE}") for sig in _VOTE_SIGNATURES} +) +_ANCHORS_BY_SIGNATURE["vote_for_gauge_weights(address,uint256)"] = RiskAnchor( + "LOW", "vote_for_gauge_weights(): shifts future gauge emissions; moves no funds from the voter" +) + +# Aragon-style votes (Aragon Voting, Curve DAO) whose trailing bool `_executesIfDecided` +# runs the vote's EVM script in this same transaction when this vote decides it. +# Signature → index of that bool; `_refine_execute_if_decided_anchor` picks the level. +_EXECUTES_IF_DECIDED_SIGNATURES: dict[str, int] = { + "vote(uint256,bool,bool)": 2, + "votePct(uint256,uint256,uint256,bool)": 3, +} +for _sig in _EXECUTES_IF_DECIDED_SIGNATURES: + _ANCHORS_BY_SIGNATURE[_sig] = RiskAnchor( + "MEDIUM", + f"{_sig.split('(')[0]}() Aragon-style vote: LOW if _executesIfDecided is false; if true and this " + "vote decides it, the vote's script executes now — judge that script", + ) + # Selector → RiskAnchor. Lowercase, with 0x prefix to match the decoder's format. _ANCHORS: dict[str, RiskAnchor] = { "0x" + function_signature_to_4byte_selector(signature).hex(): anchor @@ -111,6 +138,12 @@ class RiskAnchor: ) +_EXECUTES_IF_DECIDED_INDEX: dict[str, int] = { + "0x" + function_signature_to_4byte_selector(sig).hex(): index + for sig, index in _EXECUTES_IF_DECIDED_SIGNATURES.items() +} + + def lookup(selector_hex: str, params: list[tuple[str, Any]] | None = None) -> RiskAnchor | None: """Return the risk anchor for a selector, or None if no anchor is registered. @@ -125,9 +158,30 @@ def lookup(selector_hex: str, params: list[tuple[str, Any]] | None = None) -> Ri return anchor if selector_hex.lower() in _UPGRADE_AND_CALL_SELECTORS: return _refine_upgrade_anchor(anchor, params) + if selector_hex.lower() in _EXECUTES_IF_DECIDED_INDEX: + return _refine_execute_if_decided_anchor(anchor, params, _EXECUTES_IF_DECIDED_INDEX[selector_hex.lower()]) return anchor +def _refine_execute_if_decided_anchor(anchor: RiskAnchor, params: list[tuple[str, Any]], index: int) -> RiskAnchor: + """Rate an Aragon-style vote by its ``_executesIfDecided`` flag. + + With the flag off the call only records a vote. With it on, a deciding vote + executes the proposal's script in this transaction, so the vote is only as + safe as that script. + """ + if index >= len(params) or params[index][0] != "bool": + return anchor + name = anchor.rationale.split("(")[0] + if not params[index][1]: + return RiskAnchor("LOW", f"{name}(): {_VOTE_RATIONALE} (_executesIfDecided is false)") + return RiskAnchor( + "MEDIUM", + f"{name}(): _executesIfDecided is true — if this vote decides it, the vote's script executes " + "in this transaction; judge that script", + ) + + def _refine_upgrade_anchor(anchor: RiskAnchor, params: list[tuple[str, Any]]) -> RiskAnchor: """Say whether this `upgrade*AndCall` actually delegatecalls a payload. From 942eda6aaedb9fd326854c26a522132a45a8d2de Mon Sep 17 00:00:00 2001 From: spalen0 Date: Fri, 23 Oct 2026 12:26:10 +0000 Subject: [PATCH 3/3] refactor(llm): rate votes with one general prompt rule instead of anchors Per-signature vote anchors only cover the implementations we list. Drop them and state the rule once in the system prompt: a call that only records a governance vote, in any implementation, is LOW. Exceptions are a vote that itself executes the proposal (Aragon-style `_executesIfDecided`, auto-execution on quorum) and delegation, which is not a vote. Co-Authored-By: Claude Opus 5.5 --- tests/ai_explainer/test_prompt.py | 7 ++++ tests/test_risk_anchors.py | 50 ----------------------- utils/llm/ai_explainer.py | 9 ++-- utils/risk_anchors.py | 68 ------------------------------- 4 files changed, 13 insertions(+), 121 deletions(-) diff --git a/tests/ai_explainer/test_prompt.py b/tests/ai_explainer/test_prompt.py index 692beadf..77298d54 100644 --- a/tests/ai_explainer/test_prompt.py +++ b/tests/ai_explainer/test_prompt.py @@ -84,6 +84,13 @@ def test_hardened_prompt_includes_unit_guidance(self) -> None: self.assertIn("Do NOT assume the semantic meaning", SYSTEM_INSTRUCTIONS) self.assertIn("source context", SYSTEM_INSTRUCTIONS.lower()) + def test_votes_are_rated_low_by_a_general_rule(self) -> None: + # One implementation-agnostic rule instead of per-signature anchors, with the + # cases where a "vote" does more than record a ballot called out. + self.assertIn("only records a governance vote (any implementation) is LOW", SYSTEM_INSTRUCTIONS) + self.assertIn("_executesIfDecided", SYSTEM_INSTRUCTIONS) + self.assertIn("delegating voting power is not a vote", SYSTEM_INSTRUCTIONS) + def test_context_note_appears_in_prompt(self) -> None: calls = [DecodedCall(function_name="swapOwner", signature="swapOwner(address,address,address)")] result = _build_prompt( diff --git a/tests/test_risk_anchors.py b/tests/test_risk_anchors.py index 651d57f5..f9bae60b 100644 --- a/tests/test_risk_anchors.py +++ b/tests/test_risk_anchors.py @@ -7,10 +7,6 @@ from utils.risk_anchors import _ANCHORS, _ANCHORS_BY_SIGNATURE, RiskAnchor, format_anchors_block, lookup -def _selector(sig: str) -> str: - return "0x" + function_signature_to_4byte_selector(sig).hex() - - class TestLookup(unittest.TestCase): def test_known_high_risk_selector(self) -> None: anchor = lookup("0xf2fde38b") # transferOwnership(address) @@ -22,27 +18,6 @@ def test_known_low_risk_selector(self) -> None: assert anchor is not None self.assertEqual(anchor.level, "LOW") - def test_governance_votes_are_low(self) -> None: - for sig in ( - "castVote(uint256,uint8)", - "castVoteWithReason(uint256,uint8,string)", - "castVoteWithReasonAndParams(uint256,uint8,string,bytes)", - "castVoteBySig(uint256,uint8,address,bytes)", - "castVoteBySig(uint256,uint8,uint8,bytes32,bytes32)", - "castVote(uint256,bool)", - "submitVote(uint256,bool)", - "vote_yea(address,uint256)", - "vote_nay(address,uint256)", - "vote_for_gauge_weights(address,uint256)", - ): - with self.subTest(sig): - anchor = lookup(_selector(sig)) - assert anchor is not None - self.assertEqual(anchor.level, "LOW") - - def test_delegation_is_not_anchored_as_a_vote(self) -> None: - self.assertIsNone(lookup(_selector("delegate(address)"))) - def test_case_insensitive(self) -> None: self.assertIsNotNone(lookup("0xF2FDE38B")) @@ -159,30 +134,5 @@ def test_safe_module_is_critical(self) -> None: self.assertEqual(anchor.level, "CRITICAL") -class TestExecutesIfDecidedRefinement(unittest.TestCase): - """Aragon-style votes execute the vote's script when ``_executesIfDecided`` is set.""" - - def test_flag_off_is_a_plain_low_vote(self) -> None: - anchor = lookup(_selector("vote(uint256,bool,bool)"), [("uint256", 7), ("bool", True), ("bool", False)]) - assert anchor is not None - self.assertEqual(anchor.level, "LOW") - self.assertIn("_executesIfDecided is false", anchor.rationale) - - def test_flag_on_says_the_script_may_execute(self) -> None: - anchor = lookup( - _selector("votePct(uint256,uint256,uint256,bool)"), - [("uint256", 7), ("uint256", 10**18), ("uint256", 0), ("bool", True)], - ) - assert anchor is not None - self.assertEqual(anchor.level, "MEDIUM") - self.assertIn("judge that script", anchor.rationale) - - def test_without_params_the_anchor_covers_both_cases(self) -> None: - anchor = lookup(_selector("vote(uint256,bool,bool)")) - assert anchor is not None - self.assertEqual(anchor.level, "MEDIUM") - self.assertIn("LOW if _executesIfDecided is false", anchor.rationale) - - if __name__ == "__main__": unittest.main() diff --git a/utils/llm/ai_explainer.py b/utils/llm/ai_explainer.py index 5d0a62d4..31ddae27 100644 --- a/utils/llm/ai_explainer.py +++ b/utils/llm/ai_explainer.py @@ -183,9 +183,12 @@ no value leaves the protocol. - Base the risk tag on what this transaction executes. Missing context about something it does not execute — what a voted-on proposal would do, how a recipient later uses funds — - goes under `### Not verified` and does not raise the tag. A vote that moves no funds and - enacts nothing is LOW even when the proposal contents are unknown. This does not soften - the rules for unverified targets or undecoded payloads, which do execute here. + goes under `### Not verified` and does not raise the tag. This does not soften the rules + for unverified targets or undecoded payloads, which do execute here. +- A call that only records a governance vote (any implementation) is LOW, even when the + proposal contents are unknown. Exceptions: the vote itself executes the proposal (e.g. an + Aragon-style `_executesIfDecided` flag set to true, or auto-execution on quorum) — judge + what executes; delegating voting power is not a vote. - When a Risk Anchors section is provided, treat it as a typical floor/ceiling, not a verdict. Adjust up or down based on the specific parameters (e.g. grantRole of a minor role can be LOW; an upgrade to fresh-bytecode code can be CRITICAL). diff --git a/utils/risk_anchors.py b/utils/risk_anchors.py index 8eb9fa2d..a4042dba 100644 --- a/utils/risk_anchors.py +++ b/utils/risk_anchors.py @@ -11,7 +11,6 @@ implementation isn't LOW. Skipped intentionally: -- delegate(address) — hands voting power to someone else; not a vote. - ERC20 transfer/approve — too context-dependent (rewards distribution vs exit liquidity look identical). - Generic timelock schedule/execute — the inner call is the thing being @@ -85,46 +84,6 @@ class RiskAnchor: "setFallbackHandler(address)": RiskAnchor("MEDIUM", "setFallbackHandler(): changes the Safe's fallback behavior"), } -# Governance votes record a ballot and nothing else: a passed proposal still needs its -# own execute() call, which is judged on its actions when it is queued. Delegation -# (`delegate(address)`) is deliberately absent — it hands voting power to someone else. -_VOTE_RATIONALE = "records a governance vote; moves no funds and executes nothing" -_VOTE_SIGNATURES = ( - # OpenZeppelin Governor / Compound GovernorBravo - "castVote(uint256,uint8)", - "castVoteWithReason(uint256,uint8,string)", - "castVoteWithReasonAndParams(uint256,uint8,string,bytes)", - "castVoteBySig(uint256,uint8,address,bytes)", - "castVoteBySig(uint256,uint8,uint8,bytes32,bytes32)", - # Compound GovernorAlpha - "castVote(uint256,bool)", - # Aave governance v2 - "submitVote(uint256,bool)", - # Yearn Voter - "vote_yea(address,uint256)", - "vote_nay(address,uint256)", -) -_ANCHORS_BY_SIGNATURE.update( - {sig: RiskAnchor("LOW", f"{sig.split('(')[0]}(): {_VOTE_RATIONALE}") for sig in _VOTE_SIGNATURES} -) -_ANCHORS_BY_SIGNATURE["vote_for_gauge_weights(address,uint256)"] = RiskAnchor( - "LOW", "vote_for_gauge_weights(): shifts future gauge emissions; moves no funds from the voter" -) - -# Aragon-style votes (Aragon Voting, Curve DAO) whose trailing bool `_executesIfDecided` -# runs the vote's EVM script in this same transaction when this vote decides it. -# Signature → index of that bool; `_refine_execute_if_decided_anchor` picks the level. -_EXECUTES_IF_DECIDED_SIGNATURES: dict[str, int] = { - "vote(uint256,bool,bool)": 2, - "votePct(uint256,uint256,uint256,bool)": 3, -} -for _sig in _EXECUTES_IF_DECIDED_SIGNATURES: - _ANCHORS_BY_SIGNATURE[_sig] = RiskAnchor( - "MEDIUM", - f"{_sig.split('(')[0]}() Aragon-style vote: LOW if _executesIfDecided is false; if true and this " - "vote decides it, the vote's script executes now — judge that script", - ) - # Selector → RiskAnchor. Lowercase, with 0x prefix to match the decoder's format. _ANCHORS: dict[str, RiskAnchor] = { "0x" + function_signature_to_4byte_selector(signature).hex(): anchor @@ -138,12 +97,6 @@ class RiskAnchor: ) -_EXECUTES_IF_DECIDED_INDEX: dict[str, int] = { - "0x" + function_signature_to_4byte_selector(sig).hex(): index - for sig, index in _EXECUTES_IF_DECIDED_SIGNATURES.items() -} - - def lookup(selector_hex: str, params: list[tuple[str, Any]] | None = None) -> RiskAnchor | None: """Return the risk anchor for a selector, or None if no anchor is registered. @@ -158,30 +111,9 @@ def lookup(selector_hex: str, params: list[tuple[str, Any]] | None = None) -> Ri return anchor if selector_hex.lower() in _UPGRADE_AND_CALL_SELECTORS: return _refine_upgrade_anchor(anchor, params) - if selector_hex.lower() in _EXECUTES_IF_DECIDED_INDEX: - return _refine_execute_if_decided_anchor(anchor, params, _EXECUTES_IF_DECIDED_INDEX[selector_hex.lower()]) return anchor -def _refine_execute_if_decided_anchor(anchor: RiskAnchor, params: list[tuple[str, Any]], index: int) -> RiskAnchor: - """Rate an Aragon-style vote by its ``_executesIfDecided`` flag. - - With the flag off the call only records a vote. With it on, a deciding vote - executes the proposal's script in this transaction, so the vote is only as - safe as that script. - """ - if index >= len(params) or params[index][0] != "bool": - return anchor - name = anchor.rationale.split("(")[0] - if not params[index][1]: - return RiskAnchor("LOW", f"{name}(): {_VOTE_RATIONALE} (_executesIfDecided is false)") - return RiskAnchor( - "MEDIUM", - f"{name}(): _executesIfDecided is true — if this vote decides it, the vote's script executes " - "in this transaction; judge that script", - ) - - def _refine_upgrade_anchor(anchor: RiskAnchor, params: list[tuple[str, Any]]) -> RiskAnchor: """Say whether this `upgrade*AndCall` actually delegatecalls a payload.