Reproduced Exploit
Lend V2: cross-chain collateral is miscounted as zero in `borrowWithInterest`
Chain
Other
Category
untagged
Date
Jan 1970
Source
AuditVault
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.
Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 946. Standalone Foundry PoC and full write-up: 58392-lend-cross-chain-collateral-is-miscalculated-in-borrowwithinterest_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/logic · vuln/accounting-error · vuln/dos
Reproduction: a faithful minimal reproduction of the vulnerable finding — the cross-chain-collateral branch of
borrowWithInterestis reproduced verbatim (marked@>) with faithful minimal doubles; local deploy, no fork.
Root cause#
When a user borrows cross-chain from source chain A to destination chain B, the collateral record is stored on chain B with srcEid != destEid. But while summing that debt, borrowWithInterest only counts a cross-chain collateral when destEid == currentEid && srcEid == currentEid — a condition that can never be satisfied because srcEid is always different from destEid. The vulnerable line, reproduced verbatim from the finding:
// LendStorage.borrowWithInterest — cross-chain-collateral branch (LendStorage.sol L497)
@> if (collaterals[i].destEid == currentEid && collaterals[i].srcEid == currentEid)
Because the guard is always false, the borrower's outstanding cross-chain debt is never added to borrowedAmount, and the function returns 0 for any live cross-chain borrow.
Why it's exploitable here#
Following the finding with a clean worked example (borrowIndex = 1e18, no accrual):
- The borrower has a live cross-chain borrow recorded on the destination chain (
currentEid = 2) withsrcEid = 1,destEid = 2,principle = 100e18. Their true outstanding debt is100e18 * borrowIndex / storedIndex = 100e18. borrowWithInteresttakes thecollateralsbranch and evaluatesdestEid == currentEid && srcEid == currentEid→2 == 2 && 1 == 2→ false, so nothing is added and it returns0.- The cross-chain repay path (
repayBorrowInternal) reads that0and hitsrequire(borrowedAmount > 0, "Borrowed amount is 0"), which reverts. - The borrower can never repay or close the
100e18cross-chain borrow, and the debt is invisible to liquidity accounting.
Attack path#
Marked-line walkthrough (Playground)#
The EVM Playground pins each step to the exact executed source line in 0x671d353a…:
- L76 — Cross-chain debt ledgers declared: Setup: crossChainBorrows holds locally-originated debts and stays empty for a pure cross-chain borrow, whose record instead lives in crossChainCollaterals.
- L88 — Record the cross-chain collateral: Setup: addCrossChainCollateral stores the borrower's 100e18 cross-chain debt with srcEid=1 and destEid=2 (this chain), so srcEid never equals destEid.
- L96 — borrowWithInterest tallies cross-chain debt: borrowWithInterest sums the borrower's outstanding cross-chain debt, relying on exactly one of the two ledgers being populated on a given chain.
- L112 — Borrows branch matches srcEid: The crossChainBorrows branch correctly matches srcEid == currentEid, but it is skipped here because this cross-chain debt lives in the collaterals ledger.
- L117 — Enter the collaterals branch: Execution enters the collaterals branch, which is meant to sum exactly the cross-chain-collateral debt the borrower is now trying to repay.
- L120 — Impossible collateral match condition: Root cause: it needs destEid == currentEid AND srcEid == currentEid, but a cross-chain borrow always has srcEid != destEid, so it never matches and the debt counts as 0.
- L133 — Repayment reverts on zeroed debt: The repay path reads the debt as 0 and its require(borrowedAmount > 0) reverts, so the borrower can never repay or close a live 100e18 cross-chain borrow.
PoC#
Registry (Foundry, local deploy — verbatim vulnerable source + harm-asserting test):
cd 58392-lend-cross-chain-collateral-is-miscalculated-in-borrowwithinterest_exp && forge test -vvv
The browser Playground replays the same synthetic opcode-for-opcode and measures the harm: a borrower's live 100e18 cross-chain debt is reported as 0, and their repayment reverts on the "Borrowed amount is 0" guard. Both gates are green (registry forge test PASS + Playground _verify-poc VERDICT: PASS).
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 58392-lend-cross-chain-collateral-is-miscalculated-in-borrowwithinterest_exp (evm-hack-registry mirror).
- AuditVault finding: 946.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "Lend V2: cross-chain collateral is miscounted as zero in
borrowWithInterest". - Web3Sec X hacked database: search.
- Rekt leaderboard: search.
- Solodit incident search: search.
These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.