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:
- Cream Finance’s oracle reads from Uniswap reserves
- Uniswap reserves are writable within a transaction by any caller
- Flash loans provide capital to make that write financially meaningful
- 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/ORIGINvalues, 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:
| Factor | Lower score | Higher score |
|---|---|---|
| Capital requirement | Requires significant capital | Executable with minimal capital |
| Intermediary count | Many protocols in chain | Short path, fewer failure points |
| Oracle staleness window | Requires multiple blocks | Single-transaction execution |
| Validation presence | Intermediate checks block path | No 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:
- Fetching the proxy’s implementation slot from on-chain storage
- Fetching the implementation contract’s bytecode
- 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.