Notes / Findings

Who can move the prices Aave V3 Ethereum Core lends against: 58% of supply is priced by Aave's own adapters, moved by a two-key council and one live automated agent, within limits the update path enforces (disclosed, not scored)

Findings2026-10-01Chain: Ethereum

Source: data/finding_2026-10-01-aave-capo-price-authority.md in the repository, rendered here without changes. Source file

2026-10-01. Tool: scripts/check_lending_price_authority.py (read-only, eth_call only), test scripts/tests/test_check_lending_price_authority.py. Amounts are from the run of about 10:50 UTC. The tool went through two independent reviews that day; the second found that its selector search missed a compiler encoding, which changed one conclusion (see below).

Why we looked#

On 10 Mar 2026 an automated agent updated the wstETH CAPO price adapter of Aave V3 Ethereum with no multisig and no timelock. The snapshot ratio and its timestamp ended up misaligned, the cap fell 2.85% under the live rate, and about 27 M$ (some 10,938 wstETH, 34 accounts) were liquidated (Aave post-mortem). The repository scores who controls the Aave pool itself (score_aave_v3_pool), but not who can move the price the pool uses. This is the first measurement of that path.

What was measured#

Each reserve's price source comes from AaveOracle.getSourceOfAsset. A source that answers ACL_MANAGER() is one of Aave's own adapters: its parameters are set by RISK_ADMIN or POOL_ADMIN holders of that ACLManager. A source that reverts there is an external feed (Chainlink and the like), whose own owner is out of scope here.

Price source kindReservesSourcesSupplied
LST CAPO adapter (setter setCapParameters)1515$8.68B
Stable CAPO adapter (setter setPriceCap)1110 (USDG and PT-USDG share one)$5.43B
Older capped adapter, "Capped USDT/USD", used by USDe (setter setPriceCap)11$0.56B
"Capped EURC/USD" ratio-capped adapter (setter setPriceCapRatio)11$0.05B
Pendle PT adapter (setter setDiscountRatePerYear)1515$0.004B
"Fixed mUSD/USD" adapter (setter setPrice)11about $0
External feed2320$10.62B
All reserves67$25.34B

So 44 reserves ($14.71B, 58% of supply) are priced by Aave adapters, each with a known setter. 42 of them answer to the Core ACLManager 0xc2aaCf65...; tETH and ezETH (about $1M) answer to the Prime instance's ACLManager 0x013E2C75....

On each ACLManager, the RISK_ADMIN and POOL_ADMIN holders were replayed from RoleGranted/RoleRevoked since deployment and each confirmed live by hasRole on two RPCs. A holder is a contract that can only do what its code does, so each holder's code (behind one EIP-1967 hop for the two proxies) was searched for the selector of an adapter setter or of a steward entry point that leads to one, in the three forms solc emits that the search knows (pushed whole, as a PUSH32 of the selector already shifted, or shifted at run time: PUSH4 (sel >> k), PUSH1 (224 + k), SHL):

ACLManagerHolderName (aave-permissions-book)Controlled bySetter path in code
Core0x13a9CC64...Manual AGRSRISK_COUNCIL 0x47c71dFE...LST, stable, Pendle (setters and steward entries)
Core0x6f48d9Cd...Manual AGRS, second one (granted 30 Sep 2026)same RISK_COUNCILLST, stable, Pendle (setters, shifted form, and steward entries)
Core0x98217A06...Core GHO Aave StewardRISK_COUNCIL 0x8513e6F3... (Safe 2-of-3)none seen
Core0x529e2374...PendleDiscountRateAgentAgentHub, see belowsetDiscountRatePerYear (shifted form)
Core0x5513224d...Gho Core Direct Minter (proxy, implementation 3.9 kB)none seen
Core0xbe284044...EModeCategoryAgentnone seen
Core and Prime0x5300A1a1... (POOL_ADMIN)governance executornone seen; adapters accept POOL_ADMIN directly (checked on the USDe adapter)
Prime0x5BA8d98f...a steward of the same size as Manual AGRS (16,066 bytes)same RISK_COUNCIL 0x47c71dFE...LST, stable, Pendle
Prime0x2cE01c87... (proxy, implementation 4.6 kB), 0x5C905d62... (council 0x8513e6F3...)none seen

The council. 0x47c71dFE... is a Safe 2-of-2 with no module: one bare EOA (0x606dC57c...) and one Safe 1-of-3 (0xb291232F...). So two keys, the EOA and any one of three, drive all three stewards that reach a price. The EOA also sits in the 2-of-6 Safe under the GHO council, and 0xb291232F... is an owner of both councils.

What the update path lets the council do. Read from getRiskConfig() of 0x13a9CC64... and 0x5BA8d98f... (15 limit pairs, layout from aave-dao/aave-v3-risk-stewards IRiskSteward.sol, main branch) and from that repository's RiskSteward.sol:

The second Core steward 0x6f48d9Cd... does not answer getRiskConfig(), so its limits were not read. All three were then simulated by eth_call from the council's address (no transaction), each next to a harmless control update:

Simulated updateControl0x13a9CC64...0x6f48d9Cd...0x5BA8d98f... (Prime)
LST snapshot at half the live ratio (weETH; tETH on Prime)0.01% under the live ratio: acceptedrefusedrefusedrefused
Stable cap 10% lower in one update (USDT)0.1% lower: acceptedrefusedrefusedno stable adapter behind Prime
Pendle discount 50 points higher in one update (PT-srUSDe-22OCT2026)+0.1 point: acceptedrefusedrefusedno PT adapter behind Prime

Checked by hand the same day:

The automated path that is live#

The March incident went through an AgentHub: an automated agent holding RISK_ADMIN, executing what a risk oracle publishes. That path still exists, with different parties. The AgentHub 0x95E3015c... (owner: the governance executor) has 7 registered agents:

Who can publish to that RiskOracle, read by hand from its getters and from the verified sources on Blockscout:

Who sets the agents' limits: the agent admin of agents 5 and 6 is the same Safe 4-of-7 0x2CFe3ec4.... It may, in one transaction and with no timelock, widen the range in the range module, add allowed markets, shorten the minimum delay and enable or disable these two agents (onlyOwnerOrAgentAdmin and onlyHubOwnerOrAgentAdmin in the verified AgentConfigurator.sol and RangeValidationModule.sol). The 2-of-4 decides what gets published. So the 1 point and 2 days above are settings, not hard limits. The hard limit is the PT adapter's own MAX_DISCOUNT_RATE_PER_YEAR: 10.22 points on PT-srUSDe-22OCT2026, against 3.77 today, which bounds the automated path at about 0.36% of that PT's price today.

What it means#

Limits#

Addendum 2026-10-04: every adapter simulated, and where the dominant price authority really sits#

Every adapter, not one per kind. The three council-driven stewards were simulated on every adapter they reach (the tool now does this by default). The council is the same Safe 2-of-2, and each steward was re-confirmed by hasRole on two RPCs; the holder list is that day's replay. The harmful update was refused, and the harmless control accepted, on:

KindCore stewards 0x13a9CC64... and 0x6f48d9Cd...Prime steward 0x5BA8d98f...
LST CAPO12 of 13 adapters, $8.705B of $8.705B2 of 2
Stable CAPO10 of 10 (11 reserves: USDG and PT-USDG share one), $5.429B of $5.429Bnone behind Prime
Pendle PT1 of 15: the only PT with supply, $3.5Mnone behind Prime

The cases left INCONCLUSIVE are eUSDe and 14 matured PTs. For each, the control update was refused too, and none of them carries supply. So on every adapter that prices supply, the bound holds where a harmful update is attempted.

The dominant authority is upstream. 45 of the 67 reserves, about 99.4% of supply, are priced through a Chainlink feed owned by the Safe 4-of-9 0x21f73D42..., either directly or as the base of an Aave adapter. The weETH CAPO adapter, for example, reads ETH/USD 0x5424384B... as BASE_TO_USD_AGGREGATOR. That Safe can swap a proxy's aggregator with no delay, and the aggregators' minAnswer is 1. This comes from an independent depth pass of the same day, verified by a second agent. Under the existing composite rule, the path scores 52. It is the number that would set Aave Core's oracleAuthorityScore if the project scored price paths of consumers; whether it does is a methodology decision, not taken here. Today the published value is 100.

Decided later on 2026-10-04. The project now scores price paths of consumers: oracleAuthorityScore is the minimum over the material paths one hop upstream (METHODOLOGY, "oracleAuthorityScore for price consumers"). Aave Core comes out at 52, set by this Safe; the inputs further upstream are listed in data/finding_2026-10-04-price-paths-beyond-one-hop.md.

Not an audit, not investment advice. All notes ยท Back to the dashboard.