Cross-Contract Analysis: Tracing Attack Paths Across Protocol Boundaries

Every DeFi exploit in 2021 and 2022 that exceeded $50 million in losses involved more than one contract. A flash loan came from Aave. An oracle was manipulated on Uniswap. Collateral was valued by a lending market. Funds were extracted via a vulnerability in a third protocol that trusted the first two.

Single-contract analysis misses these attacks entirely. The lending market looks safe in isolation — its math is correct, its guards are present. The oracle looks safe in isolation — it reports accurate spot prices. The attack surface is not inside either contract; it is between them.

The Composability Blind Spot

Consider a simplified version of the Cream Finance October 2021 attack pattern:

flowchart LR
    classDef attack fill:#331a1a,stroke:#d97777,stroke-width:2px,color:#f0c0c0
    classDef process fill:#1a2233,stroke:#7ea8d4,stroke-width:2px,color:#c0d8f0
    classDef system fill:#1a3333,stroke:#5ba8a8,stroke-width:2px,color:#c0e8e8
    classDef data fill:#332a1a,stroke:#d4b870,stroke-width:2px,color:#f0e0c0

    A["Attacker"]:::attack
    FL["Aave Flash Loan\n$500M"]:::process
    U["Uniswap Pool\n(oracle source)"]:::system
    C["Cream Finance\n(lending market)"]:::system
    T["Token Price\n(inflated)"]:::data

    A -->|"borrow"| FL
    A -->|"large swap"| U
    U -->|"reserve ratio changed"| T
    T -->|"getPrice() reads reserves"| C
    C -->|"over-collateralized borrow"| A
    A -->|"repay"| FL

From Cream Finance’s perspective, its borrow() function is correct. It checks the user’s collateral value against the loan amount. The check passes because the oracle — Uniswap reserve ratio — is read after the attacker has manipulated it with flash-loaned capital.

Detecting this vulnerability requires connecting four separate facts:

  1. Cream Finance’s oracle reads from Uniswap reserves
  2. Uniswap reserves are writable within a transaction by any caller
  3. Flash loans provide capital to make that write financially meaningful
  4. These facts compose into a complete attack chain

None of these facts are visible from inside any single contract.

How Cross-Contract Analysis Works

The analysis proceeds in three phases.

Phase 1: Call Graph Construction

For each CALL, STATICCALL, and DELEGATECALL in every contract being analyzed, the engine:

  • Extracts the call target from bytecode (static address from storage or code, or traces the value if it comes from a storage slot or function argument)
  • Resolves the function selector being called by matching the 4-byte prefix against known function signature databases
  • Records the arguments passed to the callee, tracking their origin (user-controlled calldata, internal computation, or trusted storage)

The result is a directed graph where nodes are contract-function pairs and edges are call relationships annotated with the data flowing between them.

flowchart TD
    classDef system fill:#1a3333,stroke:#5ba8a8,stroke-width:2px,color:#c0e8e8
    classDef process fill:#1a2233,stroke:#7ea8d4,stroke-width:2px,color:#c0d8f0

    subgraph LendingProtocol ["Lending Protocol"]
        LP_B["borrow(amount)"]:::system
        LP_GP["getPrice(token)"]:::system
    end

    subgraph AMM ["AMM / DEX"]
        AMM_GR["getReserves() → (r0, r1)"]:::system
        AMM_SW["swap(tokenIn, amountIn)"]:::system
    end

    subgraph FlashLender ["Flash Loan Provider"]
        FL_FL["flashLoan(asset, amount, receiver)"]:::system
    end

    LP_B --> LP_GP
    LP_GP --> AMM_GR
    AMM_SW -.->|"mutates reserves"| AMM_GR
    FL_FL -.->|"provides capital for"| AMM_SW

Phase 2: Taint Propagation Across Calls

The second phase tracks which values an attacker can influence, across contract boundaries.

Taint sources include:

  • Calldata arguments (the attacker controls all inputs to the entry function)
  • Return values from contracts the attacker can manipulate (spot price from an AMM the attacker can swap into)
  • CALLER / ORIGIN values, which the attacker controls by choosing which address initiates the transaction

Taint propagates through arithmetic, memory writes, return values, and event data. It crosses contract boundaries in two ways: when a tainted value is passed as an argument to an external call, and when the return value from a call that the attacker can manipulate feeds into the calling contract’s logic.

The second case is the composability attack surface. Return values from externally-called contracts are tainted if the attacker can influence the callee’s state. Cream Finance’s oracle call returns a tainted value because the attacker manipulated Uniswap before calling borrow().

Taint sinks are operations with financial impact: collateral valuations, borrow amount calculations, liquidation triggers, reward distributions. When tainted data reaches a sink, the analysis records a potential cross-contract attack path.

Phase 3: Path Enumeration and Risk Scoring

The third phase enumerates complete attack paths from source to sink and scores them by feasibility.

A complete path includes:

  • The entry point the attacker calls directly
  • The sequence of cross-contract calls along the path
  • The taint propagation steps that connect attacker-controlled data to the vulnerable sink
  • The prerequisites (flash loan liquidity, minimum capital, specific token approvals)

Each path receives a risk score based on:

FactorLower scoreHigher score
Capital requirementRequires significant capitalExecutable with minimal capital
Intermediary countMany protocols in chainShort path, fewer failure points
Oracle staleness windowRequires multiple blocksSingle-transaction execution
Validation presenceIntermediate checks block pathNo blocking checks detected

The Flash Loan Amplification Pattern

The most common cross-contract attack pattern combines flash loans with oracle manipulation or governance attacks. Flash loans make this pattern particularly dangerous because they eliminate the capital barrier: any attacker with enough ETH to pay gas can borrow hundreds of millions for the duration of a single transaction.

Cross-contract analysis models flash loan providers (Aave, Balancer, Uniswap V3, dYdX) and marks their flashLoan return values as providing temporary, effectively unlimited capital to the receiver. Downstream analysis then checks whether any oracle or valuation function reads state that can be manipulated with this capital.

Example finding: Flash Loan Oracle Manipulation Path

Entry: LendingMarket.borrow(collateral=0xYUSD, amount=10M)
  → Calls: PriceOracle.getPrice(0xYUSD)
    → Calls: UniswapPair.getReserves()  [spot price read]
  ← Returns: inflated collateral value
← Returns: loan amount based on inflated value

Attack path:
  1. Call AaveFlashLoan.flashLoan(USDC, 100M) → receive 100M USDC
  2. Call UniswapPair.swap(USDC→YUSD, 80M)   → inflate YUSD price in pool
  3. Call LendingMarket.borrow(YUSD, 10M)     → borrow against inflated collateral
  4. Call UniswapPair.swap(YUSD→USDC, 80M)   → restore YUSD price
  5. Repay flash loan

Profit: 10M USDC (borrowed) - flash loan fee
Prerequisites: AaveFlashLoan liquidity ≥ 100M USDC
Single-transaction feasibility: YES

Proxy Chain Resolution

Cross-contract analysis must resolve proxy chains to analyze the actual code being executed. A call to a proxy address is not a call to the proxy’s logic — it is a delegatecall to whatever implementation address is stored in the proxy’s EIP-1967 slot.

The analysis resolves proxy chains by:

  1. Fetching the proxy’s implementation slot from on-chain storage
  2. Fetching the implementation contract’s bytecode
  3. Using the implementation’s code for taint and call graph analysis while using the proxy’s storage layout for storage slot computations

This matters because implementations can change. Re-resolving implementation addresses at every analysis run catches cases where a proxy upgrade introduced a new external call that creates a previously-absent attack path.

Composability with Unverified Contracts

Many contracts in a typical DeFi protocol’s ecosystem have no verified source code. Cross-contract analysis handles unverified contracts by analyzing their bytecode directly — extracting function selectors, storage patterns, and external call targets from the raw opcodes.

When an unverified contract appears in an attack path, analysis continues with reduced confidence rather than stopping. The path is reported, the confidence adjustment reflects the analytical uncertainty, and the output identifies which contracts on the path are unverified.

A Common Vulnerable Pattern: Vault Token Price Calculation

A recurring class of vulnerability uses vault tokens (shares in a yield protocol) as collateral. The vault token price is computed from the vault’s underlying balance:

function getVaultTokenPrice() external view returns (uint256) {
    uint256 totalAssets = vault.totalAssets();
    uint256 totalSupply = vaultToken.totalSupply();
    return totalAssets * 1e18 / totalSupply;
}

This is vulnerable if vault.totalAssets() can be manipulated in the same transaction. Some vaults compute totalAssets from the underlying token balance:

function totalAssets() external view returns (uint256) {
    return underlyingToken.balanceOf(address(this));  // just a balance read
}

A direct token transfer inflates totalAssets, which inflates the vault token price, which allows an attacker to borrow far more than their real collateral is worth.

Cross-contract analysis identifies this pattern:

Path: LendingMarket → VaultOracle.getVaultTokenPrice()
              → Vault.totalAssets()
              → IERC20.balanceOf(vault_address)

Finding: balanceOf(vault) is manipulable by direct token transfer
Manipulation cost: N tokens to inflate by M%
Impact: Overcollateralized borrow proportional to inflation factor

This class of vulnerability is invisible from inside any single contract. From the cross-contract graph, it is a straightforward taint path.

Limitations

Cross-contract analysis has real limits that follow from the problem’s complexity.

State explosion. The full DeFi ecosystem contains thousands of contracts with recursive call relationships. Complete enumeration is computationally intractable. Analysis uses scope limits and heuristics to focus on contracts and call patterns most likely relevant to the target.

Time-separated attacks. Multi-transaction attacks — where an attacker sets up state in one transaction and exploits it in a later one — require reasoning about which state changes persist and what conditions the later transaction can rely on. Simple cases are handled; arbitrary multi-transaction sequences are not.

MEV-dependent ordering. Some attacks require specific transaction ordering within a block that only a validator with MEV infrastructure can guarantee. Analysis identifies MEV-sensitive patterns but does not model the probability of validator cooperation.


The majority of high-value DeFi exploits since 2020 are composability attacks — vulnerabilities that do not exist inside any single contract but emerge from the interaction between contracts. Analyzing each contract in isolation misses them entirely.

The mechanism is the call graph and taint propagation across it: taint that originates in attacker-controlled inputs propagates through the return values of manipulable external calls until it reaches a financial sink. Flash loan modeling handles the capital barrier. Proxy resolution ensures analysis follows the code that actually executes.

The result is a class of findings that single-contract analysis cannot produce: complete attack paths from attacker entry point to fund extraction, spanning multiple protocols, with prerequisites validated against current on-chain state.