ChainGuard investigation report
lending-protocol (fixture)
Demo Analysis. Pre-recorded fixture data; not produced by a live run.
- Source
- contracts/vulnerable/multi_contract
- Completed
- Sun, 20 Sep 2026 14:08:00 GMT
- Static analysis
- slither · fixture
- AI investigation
- fixture
Executive summary
ChainGuard identified 2 potential attack chain(s) and 8 potential finding(s) across 6 contracts and 13 functions. 4 findings connect into chains, 3 are isolated, and 1 could not be validated.
These are potential issues derived from static analysis and evidence-validated AI reasoning. They are not confirmed exploits and require manual verification.
Attack chains
Potential oracle takeover leading to over-borrowing
An unprotected price-source setter lets any caller point SpotPriceOracle at a pool they control. LendingVault then values collateral at the attacker-chosen price and lends against it.
Entry point: SpotPriceOracle.setPool() contracts/SpotPriceOracle.sol:L18–21
- 1.Deploy a malicious poolAttacker actioncontracts/IPool.sol:L5 · evidence E-15
- 2.Replace the oracle price sourceVulnerabilitycontracts/SpotPriceOracle.sol:L18–21 · evidence E-01, E-11
- 3.`pool` now points to the attacker contractState changecontracts/SpotPriceOracle.sol:L20 · evidence E-02
- 4.Deposit nominal collateralAttacker actioncontracts/LendingVault.sol:L25–27 · evidence E-19
- 5.Oracle returns an inflated spot priceExternal dependencycontracts/SpotPriceOracle.sol:L25 · evidence E-03
- 6.Collateral valued at the manipulated priceContract functioncontracts/LendingVault.sol:L30 · evidence E-04
- 7.Borrow limit inflated beyond real collateralVulnerabilitycontracts/LendingVault.sol:L38 · evidence E-05
- 8.Vault debt-token liquidity drainedImpactcontracts/LendingVault.sol:L40 · evidence E-06
Impact. An attacker could borrow up to the vault’s entire debt-token balance against a nominal ETH deposit, leaving the protocol with unrecoverable bad debt.
How to break the chain
1. Restrict setPool to the owner contracts/SpotPriceOracle.sol:L18–21
Add an owner check to setPool (for example, require(msg.sender == owner) or an onlyOwner modifier) and consider a timelock for price-source changes.
2. Use a manipulation-resistant price contracts/SpotPriceOracle.sol:L24–28
Replace spot reserves with a TWAP or an aggregated oracle feed, with staleness and deviation checks.
3. Bound oracle input in the vault contracts/LendingVault.sol:L29–31
Reject prices outside configured min/max bounds or beyond a per-block deviation before using them in borrow limits.
Potential cross-function reentrancy enabling borrow against withdrawn collateral
withdrawCollateral hands execution to the caller before reducing collateral. A contract caller can re-enter borrow() and borrow against collateral that is already leaving the vault.
Entry point: LendingVault.withdrawCollateral() contracts/LendingVault.sol:L43–52
- 1.Withdraw collateral from a contract accountAttacker actioncontracts/LendingVault.sol:L43–52 · evidence E-20
- 2.ETH sent before state updateVulnerabilitycontracts/LendingVault.sol:L48 · evidence E-07, E-12
- 3.Re-enter borrow() from receive()Attacker actioncontracts/LendingVault.sol:L37–41 · evidence E-21
- 4.Stale collateral inflates the borrow limitState changecontracts/LendingVault.sol:L30 · evidence E-14
- 5.Collateral decremented after borrowState changecontracts/LendingVault.sol:L51 · evidence E-13
- 6.Undercollateralized debt positionImpactcontracts/LendingVault.sol:L43–52 · evidence E-13, E-06
Impact. Each withdraw/borrow cycle can extract debt tokens backed by collateral that has already left the vault.
How to break the chain
1. Apply checks-effects-interactions contracts/LendingVault.sol:L43–52
Decrement collateral[msg.sender] before sending ETH.
2. Share a reentrancy guard contracts/LendingVault.sol:L37–41
Apply a nonReentrant modifier to both withdrawCollateral and borrow.
Findings
| ID | Severity | Finding | Location | Relationship |
|---|---|---|---|---|
| F-001 | High | Unprotected oracle price-source setterAccess control | contracts/SpotPriceOracle.sol:L18–21 | Part of attack chain |
| F-002 | High | Collateral priced from single-pool spot reservesOracle / price manipulation | contracts/SpotPriceOracle.sol:L24–28 | Part of attack chain |
| F-004 | High | Cross-function reentrancy from withdrawCollateral into borrowReentrancy | contracts/LendingVault.sol:L43–52 | Part of attack chain |
| F-003 | Medium | Borrow limit trusts an unbounded oracle priceLogic / arithmetic | contracts/LendingVault.sol:L37–41 | Part of attack chain |
| F-005 | Medium | Unchecked return value of send() in claimUnchecked external call | contracts/RewardDistributor.sol:L22–26 | Isolated |
| F-006 | Medium | Treasury authorization relies on tx.originAccess control | contracts/Treasury.sol:L12–15 | Isolated |
| F-008 | Medium | Oracle reserves may be flash-loan manipulableFlash-loan surface | contracts/SpotPriceOracle.sol:L25 | Unverified |
| F-007 | Informational | Floating compiler pragmaOther | contracts/LendingVault.sol:L2 | Isolated |
Methodology & limitations
- Repository ingestion with path-traversal, size, and file-type guards. Repository code is never executed.
- Deterministic mapping of contracts, functions, external calls, state access, and oracle dependencies.
- Static analysis with Slither where available.
- AI investigation over a bounded context package; outputs are treated as candidates.
- Evidence validation of every contract, function, file, and line reference against the repository map.
- This is a Demo Analysis built from a fixture repository. It is not a live scan.
- ChainGuard reports potential attack chains from static evidence; it does not confirm runtime exploitability.
- Absence of findings is not proof of security.