Methodology

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.

The five dimensions#

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.

DimensionQuestion it answers
adminKeyScoreWho can act as this contract's ultimate authority, and how hard is that authority to compromise or coerce?
multisigScoreIf that authority is a multisig (Gnosis Safe or a bespoke equivalent), how strong is it - threshold, signer count?
timelockScoreIs there a real, enforced delay between an authority action being queued and taking effect?
oracleAuthorityScoreWho 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")
crossExposureScoreDoes 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)

Aggregation#

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.

What each dimension actually reads#

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, 5, extends the same check ACROSS ecosystems (does a Robinhood Chain Safe's owner also sit on an Ethereum L1 or Base Safe this project tracks?) and runs standalone (scripts/check_cross_ecosystem_overlap.py).

Convention (decided 2026-09-20): a cross-ecosystem overlap IS folded into crossExposureScore, everywhere. The field's documented meaning is "100 = no root signer shared with any other tracked target", and "any other tracked target" is not limited to the same chain. So when a target's root committee is identical to the committee of a tracked target on ANOTHER ecosystem, its score is a flat 80, on every EVM ecosystem, and 100 when nothing overlaps. "Identical" is read strictly and is not loosened to "shares a signer": almost every scorer requires the owner set it just read to equal the snapshot's owner set exactly, as a set (the Safe address and threshold may differ). Three shapes differ, each stated here rather than hidden: Monad's Steakhouse Morpho vault accepts containment (its curator or owner Safe is a subset of the Robinhood Chain Steakhouse committee); Monad's Curve factory needs the same factory address AND the same admin EOA; and Robinhood Chain has no cross-ecosystem comparison in code at all, only a hand-set, dated cross_ecosystem flag on the group in scripts/lib/signer_overlap.py, to be removed by hand if that committee rotates. Two rules keep this honest: (1) on the ecosystems changed 2026-09-20 (Ethereum L1, Robinhood Chain, Tempo) the fold is min(within-ecosystem score, 80), so a target already lowered by an in-ecosystem overlap is never raised back up (on Plasma the three targets folded that day, Pendle and the two Euler targets, are set to a flat 80 with no min, since none of them has a within-Plasma overlap that could be lower; Fluid Liquidity on Plasma joined them later that day, once its proposer signer set was resolved and found identical to the Arbitrum one); (2) a scorer does not call a second chain's RPC. It compares the owner set it just read on its own chain against a hardcoded, dated snapshot of the other chain's committee (same idiom as the Aave guardian scorers), with the date in the snapshot constant's name and, in most scorers, in the note, so a rotation on the other chain is caught by scripts/check_cross_ecosystem_overlap.py and by the next drift audit rather than silently trusted. One deviation from a flat 80 was closed on 2026-09-21: the older Arbitrum scorers deducted 20 per matching snapshot, so Arbitrum's Pendle (the same Safe is tracked on Plasma and on Robinhood Chain) read 60 while the same committee read 80 on the other two. It is now 80 too. Base still deducts 20 more for each overlap inside Base. Before this decision Ethereum L1, Robinhood Chain and Tempo did not fold cross-ecosystem overlaps while Arbitrum, Base, Plasma (for Aave and Ethena only) and Monad did, so the same committee scored 80 on four chains and 100 on the others. The values this moved, the rationale and how each was verified are in 6. These values were pushed on-chain on 2026-09-20 (six transactions, all status 1, read back with getScore() on all 7 oracles with 0 differences; the transaction table is section 9 of that finding). Before the push they were scorer output and the deployed testnet oracles held the previous values.

Extension (2026-09-20, same day): a shared contract root counts like a shared committee. Uniswap's Ethereum L1 Governance Timelock is the root of 11 tracked targets on 5 ecosystems (L1 2, Arbitrum 1, Base 3, Robinhood Chain 4, Monad 1), reached through a bridge alias, an OP-stack forwarder or a Wormhole relay. These were left at crossExposureScore 100 (nine of them) by an older rule that DAO or Timelock rooted targets, having no Safe signer set, were "not counted"; that rule was a limit of the sweep, which could only compare Safe owners, not a decision. The Timelock is one root identity confirmed live by each scorer, so the same flat 80 applies (min(within-ecosystem, 80), composite unchanged). Each scorer folds only on a root it confirmed that run, and the Robinhood Chain group carries a hand-set dated flag like Pendle, Morpho Blue and Curve. Details and the live drift check: 2. Scorer output only until the next re-push of Arbitrum, Base, Monad and Robinhood Chain.

l1CappedComposite (added 2026-09-17, off-chain only) - no protocol built on Robinhood Chain can be meaningfully safer than the L1 authority that can upgrade or reconfigure the rollup itself (score_rollup_l1_authority(), currently 57/100: a real 7-of-8 Security Council Safe plus a 7-day Timelock whose proposer and canceller roles sit on the same address, itself a Safe whose own signers overlap 75% with the Security Council -- revised 2026-09-17 from 60 once that cross-Safe overlap was live-verified and wired in). l1CappedComposite = min(target's own compositeScore, Robinhood Chain's own L1 rollup authority compositeScore), computed once per score_all() run and attached to every entry. Deliberately published as a SEPARATE field, never overwriting compositeScore -- same reasoning as crossExposureScore above: compositeScore keeps meaning exactly what it has always meant (this target's own authority setup, in isolation), and a consumer who cares about the chain-level ceiling reads the new field instead of losing the original number. Off-chain only: the deployed oracle contract's AuthorityScore struct has no field for this yet (adding one is a contract migration, a bigger, separate decision) -- published in 8, not pushed on-chain, and deliberately not read by dashboard/index.html either, so the dashboard's own stated design ("reads every score directly from the deployed oracle... nothing to trust but the RPC and the contract itself") stays true without a silent exception.

Two dimensions considered and checked, deliberately not scored (2026-09-17):

oracleAuthorityScore for price consumers#

DECIDED by Spap on 2026-10-04, after data/finding_2026-10-01-aave-capo-price-authority.md and its 2026-10-04 addendum showed that a lending market's riskiest lever often sits outside its own governance: whoever can swap a Chainlink aggregator, re-point a rate provider or lift a price limit moves every borrower's health factor at once. Until that date the score was 100 for every EVM lending market and the paths were only described in a finding. Code: 1, called by the Aave V3 (Ethereum Core and Horizon, Base, Arbitrum, Plasma, Monad), SparkLend (Ethereum, an Aave V3 fork), Aave V2 (Radiant on Arbitrum), GMX (V2 Synthetics and the V1 Vault, Arbitrum), Compound V3 (Ethereum, Base, Arbitrum), Moonwell (Base, a Compound V2 fork), Morpho V1 (Adpend, 1337, Steakhouse USDT and USDC on Ethereum, four vaults on Base, one on Monad), Morpho Vault V2 (Steakhouse Prime USDC and EURCV on Ethereum, six vaults on Robinhood Chain), Euler V2 (the eVaultFactory on Plasma and on Monad, the TelosC Surge EulerEarn vault on Plasma), Fluid Liquidity (Arbitrum, Plasma) and Dolomite (Arbitrum) scorers. Kamino Liquidity and Jupiter Lend on Solana follow the same rule in chains/solana/scorers.py, with the Solana formula.

oracleAuthorityScore = min, over the MATERIAL price paths ONE HOP upstream, of each path's own composite

Every classification is re-read live each run; a spec names what to check and which answer keeps the classification, and any other answer is UNREAD. One fact cannot be re-read directly: the rsETH LRTConfig is not enumerable, so its admin set was replayed from the logs on 2026-10-04, and each run re-reads that holder with hasRole and checks that no admin grant or revoke happened since. The push guard (scripts/lib/push_guard.py) holds, before a real send, an entry whose published oracleAuthorityScore drops by more than 15 points, moves while a material path is UNREAD or after a failed walk, or rises to 100 from a lower value (a path that stopped counting), so the first push under this rule (several markets from 100 to 52) is accepted by a person, once.

Families added on 2026-10-05, each from the read-only study of that day and the decisions above:

Scope. Decided for the ten markets above and the Robinhood Chainlink admin, and extended on 2026-10-05 to Aave V3 Horizon, the Morpho V1 vaults (Steakhouse USDT and USDC on Ethereum, four on Base, one on Monad), the Morpho Vault V2 vaults (two on Ethereum, six on Robinhood Chain), Radiant and GMX (V2 and V1) on Arbitrum, and later that day, after the decisions above, to SparkLend, Moonwell, Fluid Liquidity (Arbitrum, Plasma), Dolomite and Jupiter Lend: every tracked target that reads prices is now computed. Euler V2 and Kamino Liquidity were added on 2026-10-05 from the same study; the Euler AccessControlEmergencyGovernor on Plasma stays at 100, not applicable: a governance relay that holds no funds and reads no price, whose vaults are rows of the eVaultFactory. The Morpho Blue singletons are not price consumers in this sense: each market's oracle is fixed at creation, and bad debt stays in that market. A read-only study of 2026-10-05 (each recipe re-checked live by a second reader) found how to read each of them; the choices it raised were decided the same day (see the bullets above and data/finding_2026-10-05-price-consumer-scope-study.md).

Results on 2026-10-04 (live dry runs): Aave Core, Base, Arbitrum, Plasma and Monad 52; Compound Ethereum, Base and Arbitrum 52; Morpho Adpend and 1337 31 (a RedStone deUSD feed behind a 2-of-3 Safe ProxyAdmin); the Robinhood Chainlink admin 52 (its own composite). The 52s are the Chainlink proxy owners, a 4-of-9 Safe with no delay. On 2026-10-05: Aave Horizon and the four Morpho V1 vaults on Base 52; Robinhood Chain Steakhouse USDG 2 (its mGLO market is priced by setRoundData from a single bare EOA) and NetNet Credit 2 (the stock tokens' admin role sits with a bare EOA); Ethena x Steakhouse and Grove x Steakhouse 100 (no allocation today, live caps noted); Steakhouse Turbo, Purinta, the two Ethereum Vault V2 vaults, the Steakhouse USDT and USDC V1 vaults and the Monad vault 20 (material paths UNREAD, listed in data/finding_2026-10-05-price-consumer-scope-study.md); Radiant 52 and GMX V2 52 (Chainlink and the Chainlink Data Streams verifier, each behind a 4-of-9 Safe), GMX V1 2 (the admin() of its PriceFeedTimelock, a bare EOA, can switch every token to keeper prices at once). Later on 2026-10-05: the Euler eVaultFactory on Plasma 2 (the sdeUSD that one router converts through is upgradeable by a bare EOA) and on Monad 2 (the vUSD strategy's investmentManager is a bare EOA), the TelosC Surge EulerEarn vault 43 (a Pendle SY upgrade behind a 3-of-5 Safe; 34 after the review below, when the Earn's curator stopped counting as the target's own and the EulerRouters it governs, a 2-of-5 Safe acting at once, were scored), Kamino Liquidity 45 (the Scope admin, a 4-of-10 Squads with no delay, the same path score_kamino_lend publishes). After the decisions of 2026-10-05 (every wired target re-run live on the final code): the Steakhouse USDT and USDC V1 and Prime USDC and EURCV V2 vaults 52 (Chainlink binds; EtherFi's OPERATION proposer 67, Sky 81, Lido governance-grade); SparkLend 31 (RedStone inside each Chronicle Aggor median, a 2-of-3 Safe ProxyAdmin); the Monad Morpho vault 49 (Chronicle's 7-day timelocks, each scheduled by a 2-of-3 Safe, against 14 days); Moonwell 52; Fluid Plasma 52 and Fluid Arbitrum 20 (its sUSDai rate, 75% of debt, has no spec, and 23 dust vaults the API does not price count as material); Dolomite 20 (the GMX keeper keys, the three GMX ROLE_ADMIN timelock paths and two frozen markets of unknown value are all UNREAD); Jupiter Lend 35 (the Huma PST loss authority, a 2-of-3 Squads with no delay); Purinta 50 (a proxyless 4-of-8 Safe); Steakhouse Turbo 52. Every other target is unchanged. These are the scorer's values at that decision, not the values on chain today: an oracleAuthorityScore is pushed as 20 when a material price path could not be read at push time, and getScore returned 20 for SparkLend, the Monad vault and Jupiter Lend when read on 2026-10-11. Read the current value on chain. Open points are listed at the end of the finding. The deeper inputs left out by the one-hop rule are listed in data/finding_2026-10-04-price-paths-beyond-one-hop.md.

Tokens: WETH, the large dollar stablecoins (added 2026-10-11)#

Seven Ethereum L1 targets score a token itself: who can change it (upgrade, mint, freeze, pause). chains/ethereum-l1/scorers.py (score_weth9, score_usdc, score_usdt, score_dai, score_usds, score_pyusd, score_fdusd, oracle indices 27 to 33). Every value was read on two independent RPCs the same day, and they agreed.

How a token is read: every controller found is classified, and the token takes the componentwise minimum over all of them. The controllers are the proxy admin (EIP-1967 and ZeppelinOS slots, read from storage), owner(), and the role getters the token exposes (blacklister(), pauser(), masterMinter() for USDC). A controller that is itself a contract with an owner() is followed one hop, never two.

Controller foundAdmin / multisig / timelockNote
an account with no code5 / 0 / 0a bare EOA by this methodology
an EIP-7702 delegation5 / 0 / 0one account
a Gnosis Safe (threshold t of n)65 if t is 3 or more, 50 if 2, 10 if 1 / min(100, 15t + 5(n - t)) / 0same convention as every Safe-rooted target
a legacy multisig wallet (required(), getOwners())55 if 3 or more required, else 20 / same multisig formula / 0the WBTC convention
a contract of an unknown kindunresolvedthe token degrades to 20 / 20 / 0

"An account with no code" is the fact; "one key" is not claimed. Whether one person, a hardware module or an MPC service holds such an account cannot be observed on chain, and every note and page says so. A score here describes who holds the controls today, not how they are operated, and it is not a prediction of loss.

Special cases:

What this does not read: the token's off-chain issuer, custody and reserves, the in-state roles a controller can grant, and any role getter a build does not expose. The existing issuer-power report (scripts/check_issuer_power.py) still discloses freeze, seize and pause functions by name; it is not folded into these scores.

Morpho Vault V2 scoring#

ADDED 2026-09-26, on Spap's explicit go-ahead after data/finding_2026-09-25-vault-v2-scoring-scope.md scoped this as "a real methodology decision, not a mechanical extension" and deliberately left it unbuilt three weeks running rather than answer it unilaterally. V2's architecture differs from every V1 vault already scored here: per-function timelocks instead of one vault-wide delay, gates that can be permanently abdicated (a power renounced forever, not just delayed), and a curator role that is NOT uniformly a Safe the way V1's Steakhouse/Gauntlet templates are. Three decisions, each reasoned below, each checked against the vault's own verified source before being finalized -- not assumed from the September research that first scoped this.

1. What timelockScore means for a per-function-timelock vault. Score the MINIMUM delay among the functions that can redirect capital or change what the vault can invest in: addAdapter, removeAdapter, setAdapterRegistry, increaseAbsoluteCap, increaseRelativeCap, and the four exit gates (setReceiveSharesGate/setSendSharesGate/setReceiveAssetsGate/setSendAssetsGate) where still live (not yet abdicated -- see decision 2). Excluded, matching this project's own established practice for V1 (Compound V3's pauseGuardian gets the same "bounded blast radius, not scored as a bypass" treatment): pure fee setters (setPerformanceFee/setManagementFee/their recipients) and setIsAllocator/setForceDeallocatePenalty, all correctly documented at 0-day delay by the vault's own design because an allocator or fee-setter cannot redirect principal or drain funds outright.

Clarification of 2026-10-04, confirmed by Spap the same day. A fund-redirecting function that is permanently abdicated (abdicated(selector) true) can never be called again, so its own timelock protects nothing and is left out of the minimum, exactly as an abdicated exit gate already is. Live case: 5 of the 6 Robinhood Chain V2 vaults and both Ethereum L1 V2 vaults abdicated setAdapterRegistry; its own timelock reads 0 only on Purinta USDG (7 or 3 days elsewhere), where counting it gave a false 0-day minimum. If every listed function and gate is abdicated, the vault takes the top band (75). A failed timelock() or abdicated() read makes the minimum UNREAD, scored 0 with a note, never read as "0 days". Shared reader: 4, used on Ethereum L1 and Robinhood Chain.

A real bypass was suspected and DISPROVEN by reading the actual source before writing anything down. decreaseTimelock's own listed delay reads 0 days in Morpho's API, which looked at first like a way to instantly zero out any OTHER function's 7-day protection (submit decreaseTimelock(addAdapter, 0), wait 0 days, then call addAdapter immediately). Read VaultV2.sol's actual submit() function before trusting that reading: _timelock = selector == decreaseTimelock.selector ? timelock[bytes4(data[4:8])] : timelock[selector] -- decreasing a FUNCTION's timelock is itself gated by THAT function's CURRENT timelock, not by decreaseTimelock's own. Decreasing addAdapter's 7-day delay to 0 requires waiting the full current 7 days first. No bypass exists; the nominal per-function delays are real.

2. Whether an abdicated gate counts as a protection. Not folded into timelockScore or any other existing 0-100 field -- this project's methodology has no dimension for "a power was permanently renounced," and inventing a numeric weighting for that would be exactly the unilateral-invention this finding was scoped to avoid. Disclosed instead as a plain fact per gate, read live via the vault's own public abdicated(bytes4) getter -- checked per-function, not assumed uniform: on Steakhouse Prime USDC/EURCV specifically, 3 of the 4 exit gates (setReceiveSharesGate, setSendSharesGate, setReceiveAssetsGate) are permanently abdicated; setSendAssetsGate is NOT -- it remains a live, curator-controlled, 7-day-timelocked lever. A vault-wide "gates abdicated: yes/no" would have been wrong for this exact vault; always read all four individually.

3. How to score a non-Safe, non-role-registry curator (a bare EOA) versus a Safe curator. No new formula needed -- V1 already answers this identically: a bare-EOA owner or curator is scored with the SAME near-worst-case treatment already used for Adpend USDC and 1337 USDC (adminKeyScore/ multisigScore near the floor), and a Safe curator uses the SAME min(100, threshold*15 + max(0, n-threshold)*5) formula already applied to every V1 Safe curator. The "nested/dispersed signer" question this decision cross-references was independently investigated on 2026-09-25 (data/finding_2026-09-25-nested-signers-formula-investigation.md) and found NOT a real gap in 19 of 20 real cases -- reuse the existing formula verbatim, don't invent a V2-specific one.

Scope, deliberately incremental, not a single mass rollout. V2's own scoping finding stressed "each candidate vault needs its own individual investigation... not a template that generalizes cheaply" -- the same per-target rigor every other scorer in this file already gets. Built first: Steakhouse Prime USDC and EURCV on Ethereum L1 (score_morpho_steakhouse_prime_usdc_v2/_eurcv_v2), chosen because their owner/curator resolve (via the already-documented VaultV2Supervisor one-hop) to the EXACT SAME Steakhouse Safes this file already tracks and scores for 4 V1 vaults -- reusing already-verified infrastructure, not introducing two brand-new unverified multisigs on the first pass. The remaining Vault V2 targets on Ethereum L1, Robinhood Chain, Monad and Tempo (per data/finding_2026-09-25-vault-v2-inventory.md's $2.87B inventory) are the natural continuation of this same methodology, one verified vault at a time, not assumed to generalize automatically. Robinhood Chain, 2026-10-04 (Spap's go). Its 6 V2 vaults now follow decisions 1 to 3 with the same reader and owner bands as Ethereum L1 (scripts/lib/morpho_v2.py, scripts/lib/scorers.py::_v2_owner_admin_key), each owner shape read live that day. A 1-of-1 Safe owner (NetNet) is one key and scores as a bare EOA (5), the convention the earlier manual analysis of these vaults used. Grove x Steakhouse's owner became, on 2026-10-02, an executor contract with a 1-day delay() whose roles are not read yet: it stays unresolved (20) until they are.

Data collection discipline#

Limitations - what this oracle cannot see, and where it isn't consistent yet#

Publishing this list is deliberate: a number nobody can falsify isn't worth much, and a DAO deciding whether to rely on this oracle needs to know its edges, not just its claims.

Reproducing a score#

pip install -r scripts/requirements.txt
python3 -c "
import sys; sys.path.insert(0, 'scripts')
from lib.web3_utils import get_w3
from lib.scorers import score_all
for r in score_all(get_w3('https://rpc.mainnet.chain.robinhood.com')):
    print(r['label'], r['compositeScore'], r['notes'])
"

Every number printed is re-derived from live chain state in that run - never replayed from a cached value. See scripts/update_scores.py --dry-run for the same re-derivation against the exact code path a real push runs (pushes are run by hand, at the latest every nine days, the staleness limit; the weekly workflow update_scores.yml only pushes Robinhood Chain testnet and stops at the same guards, so a held or missing target fails it instead of being sent).

To check which code produced a published score, check out the commit recorded with the push (methodologyCommit in api/scores.json for Robinhood Chain, in the push record for Solana, in the push log otherwise) and recompute the hash; it must equal the methodologyHash returned by getScore():

python3 scripts/lib/methodology.py arbitrum-ecosystem --label authority-risk-oracle-arbitrum-ecosystem-v1

The label of each ecosystem is METHODOLOGY_VERSION in its push script: authority-risk-oracle-v3 (Robinhood Chain, scripts/update_scores.py) and authority-risk-oracle-<ecosystem>-v1 for ethereum-l1, arbitrum-ecosystem, base-ecosystem, tempo, plasma-ecosystem, monad, hyperliquid and solana (chains/<ecosystem>/deploy/).

Not an audit, not investment advice. Back to the dashboard.