Cross-Contract Analysis
Call Graphs and Inter-Procedural Security
How Sigvex constructs inter-contract call graphs and performs cross-boundary taint analysis to detect vulnerabilities that span multiple smart contracts—catching attack patterns that single-contract analysis fundamentally cannot see.
Cross-Contract Analysis
Modern DeFi protocols are not single contracts. A typical lending protocol involves a comptroller, interest rate models, price oracles, collateral tokens, debt tokens, and governance contracts—each a separate deployed bytecode, each with its own storage and logic, yet deeply intertwined through cross-contract calls. The vulnerabilities that matter most to security—reentrancy chains, privilege escalation paths, oracle manipulation sequences—frequently span multiple contracts.
Single-contract analysis cannot see these patterns. An analyzer that examines each contract in isolation will miss the reentrancy chain where contract A calls contract B which calls back into contract A before A’s state is finalized. It will miss the taint flow where user input enters through contract A’s public interface, passes through contract B as an intermediate, and reaches a sensitive write in contract C. This article describes how cross-contract analysis closes that detection gap.
What Single-Contract Analysis Misses
The vulnerability classes that have driven the largest DeFi losses share a common property: they require understanding the interaction between contracts, not just the behavior of each one in isolation.
Cross-Function and Cross-Contract Reentrancy
The classic reentrancy vulnerability—external call before state update—was generalized by DeFi protocols in ways that single-contract analysis cannot fully capture.
Cross-function reentrancy exploits the fact that multiple functions in the same contract share storage. Function A performs an external call and has not yet updated state. The called contract re-enters not function A but function B, which reads the stale state that A has not yet written. Single-contract analysis can detect this if the analyzer tracks shared state across functions, but many tools analyze functions independently.
Cross-contract reentrancy extends the pattern to protocol boundaries. The Cream Finance exploit in October 2021 (approximately $130 million) illustrates this: the attacker’s contract called into Cream’s token contracts, which called into the attacker’s contract during ERC-777 transfer hooks, which called back into Cream’s lending functions before Cream’s balance records were updated. Three contracts, three reentrancy steps, one attack sequence that no single-contract analyzer could reconstruct.
Read-only reentrancy targets view functions during inconsistent state. When a lending protocol’s price oracle calls back into a DEX pool’s price calculation during a flash loan, the pool’s state is mid-update. Price calculations in that window return manipulated values. This pattern—observed in multiple Curve Finance integrations in 2023—requires understanding the call relationship between the oracle consumer and the pool.
Multi-Hop Privilege Escalation
Access control vulnerabilities can span contract boundaries when a privileged operation in contract A is reachable by calling an insufficiently protected function in contract B. If B validates the caller correctly but A grants excessive trust to callers from B, an attacker who can invoke B can trigger A’s privileged operations.
The Poly Network exploit of August 2021 (approximately $611 million) follows this pattern. A function in the cross-chain contract accepted calls from a contract that had been granted keeper privileges. The attacker found a path to invoke this function through a sequence of calls that terminated with a privileged write operation. Reconstructing this path requires tracking trust relationships across contract boundaries.
Taint Propagation Through Protocol Stacks
DeFi protocols layer on top of each other. A flash loan provider invokes a callback. The callback invokes a yield aggregator. The yield aggregator queries a price oracle. The price oracle reads a spot price from a DEX pool. User-controlled data can enter at any layer and propagate down this stack, potentially reaching sensitive sinks that are only accessible through this multi-hop path.
Each hop is a cross-contract call where the receiving contract may apply some transformations or validations. Tracking whether untrusted data remains tainted through these transformations—and whether taint can reach security-sensitive operations at the bottom of the stack—requires inter-contract data flow analysis that follows calls across bytecode boundaries.
Call Graph Construction
The foundation of cross-contract analysis is an accurate call graph—a representation of which contracts call which other contracts, through which functions, and with what data flows.
Static Call Resolution
The simplest case is a call to a hardcoded address. When bytecode contains a CALL instruction with a compile-time-constant address, the target is known statically. The target contract’s bytecode is fetched and a directed edge is added to the call graph.
Many call targets are stored in contract state rather than hardcoded. A lending protocol typically stores its price oracle address in a storage slot. Reading these storage slot values at analysis time resolves indirect call targets from current on-chain state. This state-aware resolution handles the common factory and proxy patterns where contract addresses are configured post-deployment.
flowchart TD
classDef system fill:#1a3333,stroke:#5ba8a8,stroke-width:2px,color:#c0e8e8
classDef process fill:#1a2233,stroke:#7ea8d4,stroke-width:2px,color:#c0d8f0
classDef data fill:#332a1a,stroke:#d4b870,stroke-width:2px,color:#f0e0c0
classDef attack fill:#331a1a,stroke:#d97777,stroke-width:2px,color:#f0c0c0
A["Contract A\nLending Pool"]:::system
B["Contract B\nPrice Oracle"]:::system
C["Contract C\nDEX Pool"]:::system
D["Contract D\nFlash Loan"]:::system
E["Attacker\nContract"]:::attack
E -->|"1. borrow()"| D
D -->|"2. callback()"| E
E -->|"3. deposit()"| A
A -->|"4. getPrice()"| B
B -->|"5. spotPrice()"| C
C -.->|"returns manipulated price"| B
B -.->|"returns stale price"| A
A -.->|"accepts overcollateralized loan"| E
Dynamic Call Site Analysis
Some call targets are genuinely dynamic—computed from user input or runtime state that cannot be determined statically. These call sites fall into several categories that receive different handling:
Attacker-controlled targets receive the highest scrutiny. A CALL instruction where the target address flows from CALLDATALOAD without a whitelist check represents an arbitrary call vulnerability. These sites trigger immediate findings.
Configuration-set targets are addresses stored in contract state but set during initialization. These are resolved from current on-chain storage values.
Computed targets are derived from deterministic computation, such as a CREATE2 address computed from known parameters. These can often be resolved through symbolic evaluation.
Unknown targets that cannot be resolved statically are conservatively treated as calls to unknown code with unknown side effects.
Proxy-Aware Resolution
The EVM proxy pattern complicates call graph construction because the destination of a DELEGATECALL is not the implementation of the calling contract but rather a separate implementation contract. Proxy resolution runs as a preprocessing step for call graph construction, replacing proxy contracts with their resolved implementations in the call graph.
Without this proxy awareness, a protocol where all logic passes through a diamond proxy would appear as calls to a single forwarding contract, missing the actual function implementations. With proxy resolution, the call graph reflects the actual code that executes.
Inter-Procedural Reentrancy Detection
With an accurate call graph, reentrancy patterns that span function and contract boundaries become detectable.
Single-Contract Cross-Function Detection
Within a single contract, the reentrancy detector operates on the call graph among the contract’s own functions. For each function F that contains an external call, the detector asks: can the recipient of that call re-enter any function G in the same contract, where G reads or writes storage that F has not yet finalized?
This question is answered by:
- Identifying all external calls in F and their positions relative to state updates
- Enumerating all functions reachable from external calls (the callee’s interface)
- Checking whether any reachable function G accesses storage slots that F accesses before or after the external call
When F calls external code before updating storage slot S, and G also reads or writes slot S, and G is callable from the external call recipient, the detector reports a cross-function reentrancy vulnerability.
Cross-Contract Reentrancy Detection
The inter-contract extension follows the same logic across contract boundaries. For each contract call edge in the call graph where contract A calls contract B, the detector constructs the set of A’s functions that are callable from B’s execution context. If any of these functions access state that A’s calling function has not yet finalized, a cross-contract reentrancy path exists.
flowchart LR
classDef system fill:#1a3333,stroke:#5ba8a8,stroke-width:2px,color:#c0e8e8
classDef attack fill:#331a1a,stroke:#d97777,stroke-width:2px,color:#f0c0c0
classDef process fill:#1a2233,stroke:#7ea8d4,stroke-width:2px,color:#c0d8f0
classDef data fill:#332a1a,stroke:#d4b870,stroke-width:2px,color:#f0e0c0
subgraph "Contract A (Lending)"
FA["withdraw()\n[state not yet updated]"]:::system
FB["getBalance()\n[reads stale state]"]:::system
end
subgraph "Contract B (Attacker)"
BA["receive()"]:::attack
BB["attack()"]:::attack
end
BB -->|"1. calls withdraw()"| FA
FA -->|"2. sends ETH (external call)"| BA
BA -->|"3. re-enters withdraw()\nor getBalance()"| FB
FB -.->|"reads stale balance"| BA
The detector considers ERC-777 receive hooks, ERC-721 and ERC-1155 safeTransfer callbacks, flash loan callbacks, and any other mechanism that can trigger re-entry into the calling contract during an external call sequence.
Cross-Contract Taint Analysis
Taint analysis tracks the flow of untrusted data from sources to security-sensitive sinks. The cross-contract extension propagates taint through call boundaries.
Taint Source Classification
Sources are EVM operations that introduce data from untrusted external parties:
CALLDATALOADandCALLDATASIZE: Direct user input via transaction calldataCALLER: The transaction sender address (trusted in some contexts, untrusted in others)ORIGIN: The transaction origin (generally untrusted; weaker guarantee than CALLER)- External call return data: Values returned from calls to external contracts
The last category is critical for cross-contract analysis. When contract A calls contract B and uses B’s return value in a security-sensitive operation, the taint status of that return value depends on whether B’s implementation can be manipulated. If B’s implementation is attacker-controlled or if B reads from attacker-controlled state, the return value is tainted.
Cross-Boundary Propagation
When a tainted value is passed as an argument in a cross-contract call, the analysis marks the corresponding parameter in the callee as tainted. When the callee returns a value derived from tainted state, the return value is marked as tainted in the caller.
This bidirectional propagation allows the analyzer to trace multi-hop taint flows:
User input →
Contract A: processes input, passes derived value to Contract B →
Contract B: uses value in computation, returns result →
Contract A: uses returned value in sensitive SSTORE operation
Each arrow represents a potential taint propagation step. Without cross-boundary analysis, the SSTORE in the last step appears to receive a value from a trusted source (Contract B’s return). With cross-boundary analysis, the full chain from user input to storage write is visible.
Sink Classification
The same sinks used in single-contract taint analysis apply, with additional cross-contract-specific ones:
- Tainted CALL/DELEGATECALL target: Arbitrary code execution
- Tainted SSTORE slot: Arbitrary storage write
- Tainted JUMP/JUMPI destination: Control flow hijacking
- Tainted value used as external call value: Theft via ETH transfer
- Tainted return value from price oracle call: Oracle manipulation path
The final category is specific to cross-contract analysis. When a price oracle’s return value flows from attacker-controlled state (because the oracle reads from a DEX pool that can be manipulated), any lending decision based on that oracle price is a potential taint sink.
Protocol-Level Risk Analysis
Beyond individual vulnerability detection, cross-contract analysis produces protocol-level risk assessments.
Composability Risk Scoring
DeFi composability—the ability to combine protocols—is both a feature and a security surface. Risk scoring analyzes several dimensions:
Integration depth: How many external contracts does this contract call? More integration points mean more surfaces through which unexpected behavior can enter.
Trust assumptions: Which external contracts are treated as trusted? Contracts that blindly accept return values from externally-configurable addresses have high composability risk.
Oracle dependencies: How many price oracles does the protocol depend on, and what is the manipulation resistance of each? Multiple interdependent oracle dependencies compound risk.
Flash loan exposure: Can flash loans be used to manipulate any state that this contract reads from external sources? Flash-loan-amplified attacks are more powerful than comparable attacks requiring traditional capital.
Protocol Analysis Output
For each analyzed protocol, cross-contract analysis produces statistics like the following:
Protocol Analysis Report
========================
Contracts analyzed: 7
Total call edges: 43
External call depth: 4 (max hops from entry)
Reentrancy paths: 2 cross-contract paths identified
Taint sinks reached: 1 via oracle manipulation chain
Composability score: HIGH RISK (score: 78/100)
High-risk interfaces:
- LendingPool.borrow() → PriceOracle.getPrice() [unvalidated return]
- Vault.deposit() → ERC777Token.transfer() [reentrancy via hook]
Attack Scenario Reconstruction
When multiple findings are detected across multiple contracts, the system correlates them into complete attack scenarios. Rather than presenting isolated findings per contract, it reconstructs the multi-step sequence an attacker would follow:
- Entry point: the first function the attacker calls
- Propagation: each cross-contract call in the attack chain
- Exploitation: the final sensitive operation that delivers value to the attacker
- Extraction: how the attacker converts the exploit into profit
This scenario reconstruction maps disconnected findings to actionable remediation priorities.
Integration into the Analysis Pipeline
Cross-contract analysis integrates as an enhancement to both the static analysis and dynamic validation phases.
flowchart TD
classDef process fill:#1a2233,stroke:#7ea8d4,stroke-width:2px,color:#c0d8f0
classDef data fill:#332a1a,stroke:#d4b870,stroke-width:2px,color:#f0e0c0
classDef highlight fill:#332519,stroke:#e8a87c,stroke-width:2px,color:#f0d8c0
classDef success fill:#1a331a,stroke:#a8c686,stroke-width:2px,color:#c8e8b0
A["Contract Address"]:::data
B["Single-Contract Analysis\nDecompile, HIR, Detectors"]:::process
C["Call Site Extraction\nIdentify external calls"]:::process
D["Target Resolution\nStatic + State-based"]:::process
E["Multi-Contract HIR\nDecompile all targets"]:::process
F["Cross-Contract Analysis\nReentrancy, Taint, Privilege"]:::highlight
G["Scenario Correlation\nJoin findings into attack chains"]:::success
H["Protocol Risk Report\nComposability score, attack scenarios"]:::data
A --> B --> C --> D --> E --> F --> G --> H
The single-contract phase produces findings for each individual contract. Call site extraction identifies all external call targets. The multi-contract phase decompiles each target and builds the complete call graph. Cross-contract analysis detects patterns that span boundaries. Scenario correlation joins related findings into coherent attack scenarios.
Limits and Practical Considerations
Cross-contract analysis faces complexity challenges that single-contract analysis avoids.
State freshness: External call targets are resolved from current on-chain storage. If a proxy upgrade changes the implementation between analysis time and deployment, the analysis reflects state that no longer matches reality. All analyses are timestamped and alert when external call targets change between runs.
Analysis scope explosion: Following all call targets recursively can reach a large fraction of the deployed contract ecosystem. Configurable depth limits and call-path tracking avoid repeated analysis of shared dependencies like token contracts.
Dynamic target uncertainty: When call targets cannot be determined statically, those call edges cannot be followed. These sites are conservatively flagged as potential vulnerabilities, noting that the actual risk depends on what the unknown target does.
False positives from conservative assumptions: Cross-contract taint analysis may identify taint flows that are mitigated by validation logic in intermediate contracts. Each intermediate contract is examined for validation patterns, and findings are suppressed where the validation is sufficient.
Why This Matters
The Euler Finance exploit of March 2023 (approximately $197 million) exploited a sequence that passed through Euler’s main lending logic, its donation mechanism, and its collateral checking function in an order that violated Euler’s accounting invariants. Examining the Euler contracts function by function might flag individual concerns—but only a cross-function view of shared state access reveals the complete vulnerability.
The Cream Finance CreamV1 exploit of October 2021 exploited cross-contract reentrancy through ERC-777 token callbacks. The flow was: attacker calls Cream’s borrow function, Cream calls the ERC-777 token’s transfer, the token calls the attacker’s tokensReceived hook, the attacker calls Cream’s borrow again before the first call updates Cream’s records. Three contracts, three layers of calls, one coherent attack that requires inter-contract analysis to detect.
These cases are representative. The vulnerability classes that have driven the largest DeFi losses—reentrancy chains, oracle manipulation paths, privilege escalation through trusted intermediaries—are structurally cross-contract problems. Analyzing each contract in isolation misses the attack surface that matters most.