Skip to content

fix(vault): verify Aave proxy contract on-chain to block indexer poisoning#2129

Merged
gbarkhatov merged 3 commits into
mainfrom
fix/aave-proxy-onchain-verify
Jul 24, 2026
Merged

fix(vault): verify Aave proxy contract on-chain to block indexer poisoning#2129
gbarkhatov merged 3 commits into
mainfrom
fix/aave-proxy-onchain-verify

Conversation

@gbarkhatov

@gbarkhatov gbarkhatov commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Verify the Aave proxy contract on-chain (F8)

Closes https://github.com/babylonlabs-io/baby-auditor/issues/37

What this fixes

When you click "Repay all" on a loan, the app needs to know exactly how much debt to clear. It reads that debt amount from your proxyContract — the address that holds your position on-chain — and that proxy address comes from an off-chain indexer (GraphQL). Until now we trusted the indexer's proxy as-is and sized the repayment straight from it.

That's a problem. If the indexer is compromised (or just wrong), it can return a different proxy — one with less debt. "Repay all" would then quote against the wrong debt, so you'd under-repay (your real debt stays, and you can't withdraw your collateral until it's cleared). Money can't be redirected — the transaction always settles against your own connected wallet — but the amount you sign for could be wrong.

What we did

Before "Repay all" reads the debt, we now cross-check the indexer's proxy against the blockchain and refuse to proceed if they don't match ("fail closed"). The blockchain is the source of truth.

Concretely:

  • Added a small shared guard, assertProxyMatchesOnChain, that reads the real proxy from the on-chain Aave adapter (getPosition) and throws a clear ProxyMismatchError if the indexer's value disagrees (case-insensitive compare). It caches the on-chain lookup per user so we don't repeat the call.
  • Wired it into repayAll — the one path that sizes a signed transaction from the proxy's debt — right before the debt quote, and read the debt against the verified proxy.
  • Added a user-facing message so a blocked repay reads as an integrity warning ("your position details don't match what's registered on-chain — refresh and try again"), not a generic failure.

This mirrors two guards already in the codebase that follow the same "check the indexer against the chain, fail closed" pattern (assertReserveMatchesOnChain for the token, and the reorder-flow membership check for vault IDs). We deliberately reuse the same style so reviewers and future maintainers see one consistent approach.

Approaches we considered

  • Option A — verify once at the read chokepoint. Resolve the real proxy on-chain in the live position query and use it for every read (health factor, borrow limits, notifications) as well as repay. Covers the whole surface in one place.
  • Option B — verify at the value-funding site. Check the proxy on-chain right where "Repay all" sizes the signed transaction, and fail closed there.
  • Option C — a dedicated cached React hook for the verified proxy. Rejected: the read path runs inside a data-fetching function and the repay path inside a click handler — neither consumes a hook cleanly, so this was the most plumbing for the worst fit.

Why we chose Option B (repay-only)

The first revision of this PR did A + B. On review we scoped it down to B only, and removed the read/display-path check, because:

  • The display figures don't sign anything. The two consequential actions are already anchored to on-chain truth independently of the indexer: borrow re-validates the health factor against a fresh on-chain read at sign time (useBorrowTransaction) and, more fundamentally, Aave enforces the real health factor on-chain — so a spoofed displayed number can't push a real borrow past the protocol's limit. Repay all is the one place that sizes a signed value from the proxy, and that's exactly what Option B guards.
  • Verifying the display path added no real security bar but introduced a reliability regression: it turned a transient getPosition RPC blip — or normal indexer/chain lag on a freshly-deployed proxy — into a fail-closed blank Loans page (the query is polled every ~30s). That's a bad trade on a path that only shows numbers.

So the fix is deliberately minimal: one on-chain cross-check at the single site that funds a signed transaction.

How it behaves

  • Normal case (indexer agrees with the chain): "Repay all" works exactly as before; the only difference is one extra, cached on-chain read before the debt quote.
  • Tampered/mismatched case: "Repay all" refuses with the integrity message — before the debt is read and before any transaction is built.
  • The on-chain adapter address is the environment-pinned one, never an indexer-supplied address, so an attacker can't point us at a fake adapter that would "agree" with a fake proxy.

Tests

  • New assertProxyMatchesOnChain unit tests: match → returns the on-chain address; mismatch (attacker's healthier proxy) → throws; checksummed-vs-lowercase → passes; on-chain says "no position" while the indexer supplied one → throws (fail closed); RPC failure → propagates and doesn't poison the cache.
  • repayAll tests: verifies the proxy on-chain before quoting debt; mismatch → throws before reading debt and builds no transaction; the debt quote (which sizes the approval) keys off the verified proxy, not the indexer's.
  • Repay-hook test: a ProxyMismatchError surfaces as the integrity copy and the action returns without submitting.

Full Aave suite passes, typecheck and lint clean.

Note

Rebased onto main to pick up the repay stale-RPC hardening (#2122), which renamed repayFullrepayAll and moved the debt read to AaveProxy.getPositionReserveTotalDebt; the F8 check is applied on top of that new structure.

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown

🔐 Commit Signature Verification

All 3 commit(s) passed verification

Commit Author Signature Key Type Key Check
d907e0ce6c20 Govard Barkhatov sk-ssh-ed25519
0b65fd48a233 Govard Barkhatov sk-ssh-ed25519
2388cdaa5358 Govard Barkhatov sk-ssh-ed25519

Summary

  • Commits verified: 3
  • Signature check: ✅ All passed
  • Key type enforcement: ✅ All sk-ssh-ed25519

Required key type: sk-ssh-ed25519 (FIDO2 hardware key)

Last verified: 2026-07-24 08:38 UTC

@gbarkhatov

Copy link
Copy Markdown
Contributor Author

@greptile review

@greptile-apps

greptile-apps Bot commented Jul 23, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds on-chain verification for Aave proxy addresses. The main changes are:

  • A cached guard that compares indexer and on-chain proxy addresses.
  • Verification before position risk reads and full-repay sizing.
  • Dedicated integrity-error copy and test coverage.

Confidence Score: 4/5

The position risk-read path needs a security fix before merging.

  • Proxy verification and full-repay sizing fail closed.
  • Position reads still target an indexer-derived Spoke contract.
  • A substituted Spoke can return false risk figures after the proxy check passes.

services/vault/src/applications/aave/services/positionService.ts and services/vault/src/applications/aave/hooks/useAaveUserPosition.ts

Security Review

The proxy check fails closed, but the position-loading path still reads from an indexer-derived Spoke address. An attacker controlling the indexer can keep the correct proxy while substituting a contract that returns false debt, collateral, and health data. No SECURITY-GUARDRAILS.md file was present; the available security guidance requires external GraphQL data used for security decisions to be validated.

Important Files Changed

Filename Overview
services/vault/src/applications/aave/services/assertProxyMatchesOnChain.ts Adds cached on-chain proxy resolution, mismatch detection, and fail-closed error handling.
services/vault/src/applications/aave/services/positionService.ts Verifies the proxy but continues to perform risk reads against the indexer-derived Spoke address.
services/vault/src/applications/aave/services/positionTransactions.ts Re-verifies the proxy before full-repay sizing and resolves the Core Spoke through the configured adapter.
services/vault/src/applications/aave/hooks/useAaveUserPosition.ts Adds the configured adapter to position loading and its query key.
services/vault/src/applications/aave/hooks/useRepayTransaction.ts Maps proxy mismatches to centralized integrity-warning copy.

Sequence Diagram

sequenceDiagram
    participant UI as Loans UI
    participant IDX as Indexer
    participant PS as Position service
    participant AD as Configured adapter
    participant SP as Supplied Spoke
    UI->>IDX: Fetch position and config
    IDX-->>UI: proxyContract and coreSpokeAddress
    UI->>PS: Load live position
    PS->>AD: getPosition(user)
    AD-->>PS: On-chain proxy
    PS->>PS: Verify proxy equality
    PS->>SP: Read debt and account data
    SP-->>PS: Risk figures
    PS-->>UI: Display position
Loading

Reviews (1): Last reviewed commit: "fix(vault): verify Aave proxy contract o..." | Re-trigger Greptile

@greptile-apps

greptile-apps Bot commented Jul 23, 2026

Copy link
Copy Markdown

Greptile Summary

This PR verifies indexer-provided Aave proxy addresses against on-chain state. The main changes are:

  • Adds a cached, fail-closed proxy integrity guard.
  • Applies verification before live position reads and full-repay sizing.
  • Uses the verified proxy for downstream debt and risk calculations.
  • Adds dedicated integrity-error copy and focused test coverage.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.
  • Unverified proxies cannot reach the updated live-read or full-repay paths.
  • Mismatches, missing positions, and RPC failures stop processing.

Important Files Changed

Filename Overview
services/vault/src/applications/aave/services/assertProxyMatchesOnChain.ts Adds on-chain proxy resolution, successful-result caching, case-insensitive comparison, and fail-closed errors.
services/vault/src/applications/aave/services/positionService.ts Verifies the indexed proxy before live Spoke reads and returns the authoritative address.
services/vault/src/applications/aave/services/positionTransactions.ts Re-verifies the proxy immediately before full-repay debt sizing.
services/vault/src/applications/aave/hooks/useAaveUserPosition.ts Threads the pinned adapter into position loading and the query key.
services/vault/src/applications/aave/hooks/useRepayTransaction.ts Maps proxy mismatches to dedicated user-facing integrity copy.

Sequence Diagram

sequenceDiagram
    participant UI as Loans UI
    participant Indexer as GraphQL indexer
    participant Guard as Proxy guard
    participant Adapter as Pinned Aave adapter
    participant Spoke as Aave Spoke

    UI->>Indexer: Fetch position and proxy
    UI->>Guard: Verify adapter, user, and indexed proxy
    Guard->>Adapter: getPosition(user)
    Adapter-->>Guard: On-chain proxy
    alt Proxy matches
        Guard-->>UI: Return verified proxy
        UI->>Spoke: Read position or debt
    else Mismatch, missing position, or RPC failure
        Guard-->>UI: Throw and fail closed
    end
Loading

Reviews (1): Last reviewed commit: "fix(vault): verify Aave proxy contract o..." | Re-trigger Greptile

Comment thread services/vault/src/applications/aave/services/positionService.ts Outdated
Comment thread services/vault/src/applications/aave/services/positionService.ts Outdated
Comment thread services/vault/src/applications/aave/services/index.ts Outdated
jonybur
jonybur previously approved these changes Jul 24, 2026

@jonybur jonybur left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid F8 hardening. Verified the two things this needed to get right: the guard resolves the on-chain proxy via getPosition against the env-pinned adapter (never the indexer's), and it fails closed on both the missing-position case and RPC failures (the .catch also deletes the cache entry so a transient failure doesn't poison future reads). The position service applies the check at the chokepoint AND overwrites position.proxyContract with the verified value so downstream consumers can't accidentally re-read the indexer one; repayFull re-verifies at the value-site as an independent check. The Spoke read is safe too, though the PR doesn't advertise it: config.coreSpokeAddress is itself resolved on-chain in fetchConfig from the same pinned adapter. Case-insensitive compare is correct for checksummed-vs-lowercase inputs, and the per-user cache dedupe keeps the extra RPC at one call. Tests cover the match, mismatch, missing-position, RPC-failure, and repay-amount-byte-identity cases.

@gbarkhatov
gbarkhatov merged commit 2a44948 into main Jul 24, 2026
8 checks passed
@gbarkhatov
gbarkhatov deleted the fix/aave-proxy-onchain-verify branch July 24, 2026 12:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants