Research / Chain files / Base

Base: who can change what the oracle reads

13 contracts on Base are tracked and scored by an oracle on Base Sepolia, among them Morpho Blue, Aerodrome and the Uniswap factories. Base is where one Safe signs for the largest tracked lending contract and where Uniswap is governed from Ethereum through a forwarder.

What a track judge can check in two minutes

Three calls on the Base (Sepolia) oracle, replayed at the block the Watch read (48160953). They need a public RPC and cast (Foundry) and nothing from us. If your RPC does not keep that block, drop --block: the answers change when the oracle is re-pushed.

RPC=https://sepolia.base.org
ORACLE=0x50840a7667baEa9D05ad4ae3dCeb384724b58720
TARGET=0xbbbbbbbbbb9cc5e90e3b3af64bdaf62c37eeffcb   # Morpho Blue (singleton)

# 1. how many contracts this oracle tracks
cast call $ORACLE "trackedTargetsCount()(uint256)" --rpc-url $RPC --block 48160953

# 2. its score, five sub-scores then the composite, then lastUpdated and the method hash
cast call $ORACLE "getScore(address)((uint8,uint8,uint8,uint8,uint8,uint8,uint64,bytes32))" $TARGET --rpc-url $RPC --block 48160953

# 3. is that score older than maxStaleness
cast call $ORACLE "isStale(address)(bool)" $TARGET --rpc-url $RPC --block 48160953

Expected

1 -> 13
2 -> (65, 95, 0, 100, 80, 55, 1791847930, 0x0ffe08e65a4a36a26b89b8257fca91ff5c4c558f13e367f9bf457231d6848c70)   admin, multisig, timelock, price feed, shared signers, composite
3 -> false

3 findings, each with its replay

1. Morpho Blue on Base answers to a 5-of-9 Safe with no module, no guard and no timelock, and the same nine owners sign for Morpho Blue on Robinhood Chain

The owner of the Morpho Blue singleton on Base is a Safe that needs 5 of 9 signatures. It has no module and no guard, and no timelock sits in front of it. The Morpho Blue contract of Robinhood Chain is owned by a different Safe address whose owner set is identical, at the same threshold: the same nine signers hold the lending contract on two chains. The oracle publishes 80 for shared signers on this target for that reason; the composite (55) does not include that dimension.

Replay it (public data, no key)
# owner of Morpho Blue on Base, and its threshold
BASE=https://mainnet.base.org
OWNER=$(cast call 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb "owner()(address)" --rpc-url $BASE)
cast call $OWNER "getThreshold()(uint256)" --rpc-url $BASE
# -> 5
# number of owners, modules and guard
cast call $OWNER "getOwners()(address[])" --rpc-url $BASE | tr ',' '\n' | wc -l
cast call $OWNER "getModulesPaginated(address,uint256)(address[],address)" 0x0000000000000000000000000000000000000001 10 --rpc-url $BASE
cast storage $OWNER 0x4a204f620c8c5ccdca3fd54d003badd85ba500436a431f0cbda4f558c93c34c8 --rpc-url $BASE
# -> 9 owners; modules: [] ; guard slot: all zeros
# the Robinhood Chain Safe: same threshold, same owner set (the diff prints nothing)
RH=https://rpc.mainnet.chain.robinhood.com
SAFE=$(cast call 0x9d53d5e3bd5e8d4cbfa6db1ca238aea02e651010 "owner()(address)" --rpc-url $RH)   # Morpho Blue on Robinhood Chain
cast call $SAFE "getThreshold()(uint256)" --rpc-url $RH
diff <(cast call $OWNER "getOwners()(address[])" --rpc-url $BASE | tr -d '[] ' | tr ',' '\n' | tr A-F a-f | sort) <(cast call $SAFE "getOwners()(address[])" --rpc-url $RH | tr -d '[] ' | tr ',' '\n' | tr A-F a-f | sort) && echo identical
# -> 5, then: identical

Notes: finding_2026-09-19-base-morpho-blue-robinhood-overlap.md · scored_targets_2026-09-19-maintenance.md

2. One 3-of-7 Safe is governor, pauser and fee manager of Aerodrome, and nothing delays it

On Aerodrome, the Voter's governor is the same Safe address as the pool factory's pauser and fee manager: a 3-of-7 Safe with no module attached. None of the governor functions checks a timelock; each one takes effect in the transaction. The Voter's emergency council is a separate Safe (3-of-5) that shares no owner with it and whose powers are limited to killing and reviving gauges. The published composite for the pool factory is 46, with a timelock score of 0.

Replay it (public data, no key)
# the three roles point at one address
BASE=https://mainnet.base.org
cast call 0x16613524e02ad97eDfeF371bC883F2F5d6C480A5 "governor()(address)" --rpc-url $BASE
cast call 0x420DD381b31aEf6683db6B902084cB0FFECe40Da "pauser()(address)" --rpc-url $BASE
cast call 0x420DD381b31aEf6683db6B902084cB0FFECe40Da "feeManager()(address)" --rpc-url $BASE
# -> the same address three times (0xE6A4...2075)
# that Safe: threshold, owners, modules
G=$(cast call 0x16613524e02ad97eDfeF371bC883F2F5d6C480A5 "governor()(address)" --rpc-url $BASE)
cast call $G "getThreshold()(uint256)" --rpc-url $BASE
cast call $G "getOwners()(address[])" --rpc-url $BASE | tr ',' '\n' | wc -l
cast call $G "getModulesPaginated(address,uint256)(address[],address)" 0x0000000000000000000000000000000000000001 10 --rpc-url $BASE
# -> 3, then 7, then modules: []

Notes: finding_2026-09-18-aerodrome-voter-governor-identity.md

3. Uniswap on Base is governed from Ethereum: a forwarder whose storage names the Uniswap Governance Timelock, 2-day delay

The owner of the Uniswap V4 PoolManager and of the V2 factory on Base is an OP-stack forwarder contract. Its storage slot 0 holds the L2 cross-domain messenger predeploy and its slot 1 holds the Uniswap Governance Timelock on Ethereum, whose delay is 2 days. Three Base targets (V3, V2, V4) sit under that one root, which also roots targets on four other ecosystems: 11 tracked targets in all. The oracle folds that shared root into the shared-signers dimension as 80.

Replay it (public data, no key)
# the owner of the V4 PoolManager and of the V2 factory on Base is one forwarder
BASE=https://mainnet.base.org
cast call 0x498581ff718922c3f8e6a244956af099b2652b2b "owner()(address)" --rpc-url $BASE
cast call 0x8909dc15e40173ff4699343b6eb8132c65e18ec6 "feeToSetter()(address)" --rpc-url $BASE
# -> the same address twice (0x31FAfd4889FA1269F7a13A66eE0fB458f27D72A9)
# its storage: slot 0 the OP-stack messenger predeploy, slot 1 the Ethereum Timelock
F=0x31FAfd4889FA1269F7a13A66eE0fB458f27D72A9
cast storage $F 0 --rpc-url $BASE
cast storage $F 1 --rpc-url $BASE
# -> a word ending in 4200...0007 (the OP-stack messenger), then a word ending in the Uniswap Timelock address (0x1a9C...35BC)
# that Timelock on Ethereum: delay in seconds
TL=$(cast parse-bytes32-address $(cast storage $F 1 --rpc-url $BASE))
cast call $TL "delay()(uint256)" --rpc-url https://ethereum-rpc.publicnode.com
# -> 172800 (2 days)

Notes: finding_2026-09-20-uniswap-one-timelock-eleven-targets.md · scored_targets_2026-09-16.md

Targets

13 targets are tracked on Base, read from mainnet and scored by the oracle on Base (Sepolia). Bands of the composite score:

moderate3
decent7
strong3

Up to 12 targets, ordered by value where known, then by lowest score. Each name opens the contract page.

TargetCompositeBand
Morpho Blue (singleton)55moderate
Aave V3 Base71decent
Uniswap V3 Factory (Base)85strong
Aerodrome Slipstream CLFactory46moderate
Aerodrome Finance PoolFactory46moderate
Uniswap V2 Factory (Base)85strong
Uniswap V4 PoolManager (Base)85strong
Compound V3 (Comet, USDC market)78decent
Moonwell Comptroller (Unitroller)71decent
Spark USDC Vault (sparkUSDC)63decent
Gauntlet USDC Prime (gtUSDCp)66decent
Steakhouse USDC73decent

What is not read

Read from the public repository and the Watch at build, 2026-10-15 21:09 UTC. A finding is a dated reading with its replay, not an audit and not investment advice. All chain files · Dashboard.