Changelog · Methodology · API · Security
Source: METHODOLOGY.md in the repository, rendered here without changes. Source file
What every score means, what it reads on-chain to get there, and what it cannot see. Written so a DAO delegate, a hackathon judge, or a protocol team being scored can check any number below against the live chain themselves, not take it on trust.
This document covers the core model - the one live on Robinhood Chain today and used (in slightly adapted form) by the other EVM oracles (Ethereum L1, Base, Arbitrum, Plasma, Monad). Updated 2026-10-11: Hyperliquid (a HyperEVM oracle), Tempo (an EVM chain), Solana and Zcash also have their own chains/<ecosystem>/METHODOLOGY.md adapting these same five dimensions to a different or non-Safe authority model, plus their own scorers.py and dated data/scored_targets_*.md notes with the live-verified numbers. Target counts change with every batch: python3 scripts/live_target_counts.py reads them from the oracles (197 across nine oracles on 2026-10-11, plus the Zcash commitment). This document doesn't duplicate the chain files.
Every scored target gets five sub-scores, each 0-100, higher is safer. A dimension that genuinely does not apply to a target (no oracle role, no cross-protocol signer data available yet) scores 100 - full marks, not penalized as if it were a real weakness. This is a project-wide convention, not a per-target choice.
| Dimension | Question it answers |
|---|---|
adminKeyScore | Who can act as this contract's ultimate authority, and how hard is that authority to compromise or coerce? |
multisigScore | If that authority is a multisig (Gnosis Safe or a bespoke equivalent), how strong is it - threshold, signer count? |
timelockScore | Is there a real, enforced delay between an authority action being queued and taking effect? |
oracleAuthorityScore | Who can change the prices this target reports (a provider) or relies on (a lending market that reads feeds)? (100 = not applicable, or not yet computed for a price consumer outside the rule's current scope; see "oracleAuthorityScore for price consumers") |
crossExposureScore | Does this target's root authority also control another tracked target? A shared compromised key becomes a systemic risk, not an isolated one. (100 = no overlap found, or not computed for that target; for Solana, Hyperliquid and Zcash that means within the ecosystem only, not yet checked across ecosystems; see Convention below and Limitations) |
compositeScore = floor(0.4 * adminKeyScore + 0.3 * multisigScore + 0.3 * timelockScore + 0.5)
(scripts/lib/scorers.py::_composite(), mirrored per-ecosystem.) Standard round-half-up, deliberately not Python's built-in round() (banker's rounding produced a value one point off an already-published score once - see that function's own docstring).
oracleAuthorityScore and crossExposureScore are published on-chain and on the dashboard but are not folded into compositeScore. Each is a distinct fact a consuming contract might weight differently - a lending protocol gating collateral factors by getScore() may care far more about crossExposureScore on a target whose Safe also secures three other protocols it's exposed to than about that target's own compositeScore in isolation. Collapsing five numbers into one would throw that choice away.
adminKeyScore - trace the target's authority getter(s) (owner(), admin(), hasRole(DEFAULT_ADMIN_ROLE, ...), an EIP-1967 admin slot, or a bespoke pattern like a PoolAddressesProvider/ACLManager role system) to its root: a bare EOA (near-worst case), a Gnosis Safe or bespoke multisig (scored better, see multisigScore), or a real, live, active DAO (Governor Bravo + Timelock, or equivalent) reached directly or via a legitimate cross-domain bridge mechanism (an L1→L2 address alias, an OP-stack L2CrossDomainMessenger predeploy check) - see data/correction_2026-09-16-uniswap-bridge-alias.md for a case where this distinction mattered: a bridge alias reads identically to a bare EOA under a mechanical is_eoa() check (no bytecode), and treating the two the same is a real scoring bug, not just an imprecision. A resolved external-DAO root scores in the 75-85 range depending on how directly its quorum/delay were independently re-confirmed live; a bare EOA scores 2-10; an unresolved contract (a real contract exists, but its own controller wasn't traced this pass) scores 20 as an explicit "unknown, not assumed safe" placeholder, never silently treated as either extreme.
multisigScore - for a Safe-rooted target, reads getOwners() and getThreshold() live (or the bespoke getSigners()/threshold() pattern found on non-Safe multisigs). The general shape used across most targets: min(100, threshold * 15 + max(0, ownerCount - threshold) * 5) - a higher threshold matters more than a larger owner count at a fixed threshold, since more owners at the same threshold means more key combinations can reach it. Not every target uses this exact formula - several scorer functions use a tuned variant reflecting that specific target's context (see Limitations).
Safe authority gate (added 2026-09-21). A Safe's owners and threshold only say who can act if nothing else can. A module executes as the Safe without the owners' signatures, and a Safe on unknown logic is not the Safe the threshold describes. So the shared resolver every scorer uses (safe_owners_and_threshold) now also reads the Safe's modules and singleton, and returns "unresolved" (the same result as "not a Safe", which every scorer already scores at the conservative unresolved-authority floor) when a module is enabled that has not been analyzed, or the singleton is neither a published Safe build nor analyzed. Analyzed means read, understood and recorded with its evidence in scripts/lib/safe_modules.py (KNOWN_ANALYSES, KNOWN_SINGLETON_ANALYSES); five modules or guards and one odd singleton are recorded today. Three deliberate limits: a module list that could not be read leaves the score as computed and adds a note, so a flaky RPC cannot collapse a score; a guard or a fallback handler never degrades, since they can block or answer calls but not act as the Safe; and an analyzed module that can act as the Safe by design (Arbitrum's UpgradeExecutor on the Security Council Safe) is accepted, its power disclosed in the notes rather than scored. The gate adds a note to every affected result saying which Safe and why. Owner-only tools (the cross-ecosystem sweep, the modules sweep) pass check_modules=False so a gated Safe's signers stay visible. Record: data/finding_2026-09-21-safe-modules-and-guards.md. Score is 0 if no Safe/multisig layer exists (the root is a bare EOA or a plain contract), 100 if not applicable (the root is a real, active DAO with no Safe layer - a token-vote quorum is a different strength model than a Safe threshold, deliberately not conflated with one here).
timelockScore - looks for a real TimelockController/Timelock-shaped contract (or a bespoke per-function delay mapping) gating the specific authority-changing action(s) this target's root can take, and reads its configured delay() live. 0 if no delay mechanism is found, or if one exists but doesn't cover the action that actually matters (e.g. setOwner excluded from an otherwise-real 7-day delay on other functions - scored on the gap, not the presence of a timelock somewhere in the contract). A confirmed real delay scores 60-75 depending on length and whether an emergency-bypass path was checked for and ruled out; capped below 100 whenever that bypass check wasn't done this pass, rather than assumed clean. A confirmed bypass that is bounded (it can change configuration or pause, not upgrade or withdraw) caps the score at 55. Fluid Liquidity on Arbitrum is scored this way (four config-handler auths that point at the team multisig, plus a guardian that is that multisig, act without the 24h delay). Fluid on Plasma has the same shape, enumerated on chain, and takes the same cap; that its handler contracts' powers are bounded is inherited from the Arbitrum analysis, not re-derived for them.
oracleAuthorityScore: who can change the prices a target reports or relies on. Each derivation in use: the Robinhood Chainlink admin Safe scores its own composite (since 2026-10-04); Lido stETH scores its HashConsensus quorum; Solana's Drift and Kamino Lend score the minimum over their oracles' composites; Hyperliquid's targets follow chains/hyperliquid/METHODOLOGY.md section 4.4 (the HIP-3 dexes' oracle key, the L1 validator set, Kinetiq's operator push); ten EVM lending markets score the price paths upstream of them (see "oracleAuthorityScore for price consumers" below). Every other target publishes 100: not applicable for a target that neither reports nor reads a price, and not yet computed for a price consumer outside that rule's current scope (listed there). Not applicable includes a target that reads prices only through oracles nobody holding a key on it can change: each Morpho Blue market's oracle is fixed by its creator when the market is created, and its bad debt stays in that market, so the singletons publish 100 and the exposure is scored at the vaults that choose the markets.
crossExposureScore - computed once per score_all() run, after every individual target is scored (scripts/lib/signer_overlap.py): re-derives every tracked group's root signer set live, and checks whether any signer also appears in another tracked group's signer set. crossExposureScore = max(0, 100 - 20 * sharedGroupCount). This reuses the same discipline as this project's sister research program, multisig-overlap (about 400 protocol entries on 25 chains, as of its own README; the 2026-09 figure was 337 protocols and 547 confirmed Safes tracked for exactly this pattern at a larger scale), scoped down to this oracle's own tracked target set. A separate module,