Banked from Spark (sparklend) Phase-0 = WALK CLEAN across SparkLend-core (Aave-V3 fork), sparklend-advanced oracles, and spark-alm-controller (Liquidity Layer). Heavily-audited target; the value is the methodology refinement.
A fork frozen at an OLD upstream version (SparkLend = Aave v3.0.x, frozen 2024) WILL be missing the upstream's later fixes (Aave 3.1/3.2/3.3: virtual accounting, SUPPLY_TO_ATOKEN guard, borrow<=supply cap, bad-debt-burn/deficit, dust-prevention/MIN_LEFTOVER_BASE, LTV0-on-freeze). BUT an unported fix is a LEAD ONLY IF it patched a REACHABLE ATTACKER-PROFITABLE bug - NOT if the upstream changelog itself labels it "defense-in-depth / hygiene / optional / extra layer." - Aave's 3.1/3.3 headline items (virtual accounting, deficit, dust) are EXPLICITLY defense-in-depth in Aave's own docs - the base 3.0.x ran on Aave mainnet without them and was NOT inflation/insolvency-exploitable in isolation, because aToken minting is INDEX-BASED (scaled by liquidityIndex), NOT an ERC4626 shares/assets ratio -> no first-deposit/donation rounding primitive. So "no virtual accounting on an Aave-3.0 fork" is NOT a Critical. - RULE: before scoring a port-lag finding, (a) read the upstream changelog's OWN framing of the fix (security-patch vs hardening), (b) verify the affected mechanism is attacker-PROFITABLE in the fork (e.g. check the aToken mint math is ratio-based not index-based), (c) check the fix isn't publicly-documented-as-optional (high dup). Unported HYGIENE != bounty Critical. - The one real SparkLend gap (NO on-chain bad-debt cleanup -> phantom debt accrues forever) = protocol-risk ADVISORY, not a Critical: requires adverse market (not a cheap attacker action) + is documented Aave-upstream behavior (high dup) -> fails Gate-2 (privilege/reachability) + Gate-4 (known). Advisory, not staged.
When the in-scope repo ships its OWN audit reports (sparklend-advanced/audits/ has ChainSecurity v150/v160 + Cantina v160), GREP them by the EXACT sink (function name calculateTVLs/getRate, contract name, the vuln class) BEFORE any analysis. Spark's audit corpus had ALREADY pre-accepted: weETH-manip, rsETH-manip, ezETH-TVL-manip, depeg-ignore (non-market pricing), OOG-griefing, exchange-rate staleness. Disclosure-Gate-0 satisfied from disk -> do NOT spend compute re-deriving known-accepted classes. This is the cheapest, highest-yield first move on any audited target.
Of 9 Spark exchange-rate oracles, 7 read a STORED protocol rate (Lido getPooledEthByShares, RocketPool getExchangeRate, EtherFi getRate, ERC4626 convertToAssets) = share-accounted, LOW manipulability -> not findings. Only 2 read a manipulable source: EZETH (live-computed calculateTVLs()/totalSupply()) + rsETH (keeper-pushed price) - and THOSE are exactly the two the auditors flagged (and risk-accepted: same TVL/supply Renzo uses to mint; trusted admin). RULE: triage LST/LRT oracles by stored-protocol-rate (skip) vs live-computed/spot/keeper-pushed (scrutinize) FIRST; the spot/live ones are the only candidates.
- Carries forward the fork-warp lesson: composite latestAnswer (no-timestamp) staleness on these = ACCEPTED Aave design (AaveOracle relies on Chainlink heartbeat + Spark freezer/KillSwitch), not a novel gap. Confirmed = the cycle267 "verify it's not accepted-design" discipline holding.
- rate-source underflow (ssr()-1e27) is NOT a finding when the sole consumer (CappedFallbackRateSource) try-catches with an err.length>0 guard -> routes to defaultRate (self-protect). Trace to the live consumer before scoring a revert.
Spark ALM treats the RELAYER as UNTRUSTED (THREAT_MODEL.md: "compromised at any time") with RATE LIMITS as the boundary. The patterns that make it safe (bank as positive case studies / audit checklist for any keeper/relayer-driven fund-mover):
- Rate-limit fail-CLOSED: an unconfigured key MUST revert (RateLimits.sol:95 reverts on maxAmount==0); "unlimited" requires an EXPLICIT sentinel (type(uint256).max via setUnlimitedRateLimitData). The inverse (zero => unlimited) is the fail-OPEN bug to hunt.
- Destination-keyed limits neutralize exfil: transfer limit keyed by (asset, destination) (makeAddressAddressKey) -> an untrusted relayer can't reach an unconfigured (asset, attacker) pair (zero => revert). If the limit is keyed only by asset (not destination) and the relayer picks the recipient -> exfil.
- ALMProxy arbitrary-call gated to CONTROLLER, not relayer: doCall/doDelegateCall onlyRole(CONTROLLER); the relayer never holds CONTROLLER; the controller builds calls with HARDCODED selectors -> relayer can't choose target/calldata. doDelegateCall had ZERO call sites (dead). Check: can the relayer reach any path where it controls target+calldata? = drain.
- Rate-limit-at-end is safe IFF nonReentrant: a trailing triggerRateLimitDecrease after the external call reverts the whole tx atomically if the moved amount exceeds the cap -> no value escapes. Only safe under nonReentrant.
- Value-preserving vs value-out discriminator: a relayer path with NO rate limit is grief-only (not a finding) IFF both legs output to address(proxy) (custody retained, e.g. DAI<->USDS rebalance). Distinguish "moves value OUT of custody" (must be limited) from "rebalances WITHIN custody" (need not be).
- Cross-chain recipient binding: CCTP/LZ mintRecipient admin-set with require(!=0), relayer-immutable -> relayer can only bridge to the pre-configured ALMProxy. Mint-on-dest enforced by Circle attestation (out of scope by design).
CLAUDE.md / memory references 5 banked Aave anti-patterns (aave-v3-bytes32-mapping-key-confusion, aave-v3-non-enumerable-acl, aave-v3-sentinel-governance-unset-pattern, ...) but ~/bounty/skills/_shared/anti-patterns/ does NOT contain them (stale pointers; dir has 5 different files). Re-bank or fix the pointers if those Aave anti-patterns are needed for future Aave-fork audits. Related: [[feedback_cross_fork_portlag_primitive]], [[2026-06-21-cycle267-oracle-config-field-deadcode-and-pushrate-staleness]], [[2026-06-21-cycle267-delegatecall-router-config-and-failclosed-dedup-key]].