Reproduced Exploit
LEND: Cross-chain borrow ignores existing debt in collateral validation
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: 341. Standalone Foundry PoC and full write-up: 58375-lend-cross-chain-borrow-ignores-existing-debt-in-collateral-validation_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/logic · vuln/theft · vuln/cross-chain
Reproduction: a faithful minimal reproduction of the vulnerable finding — the source-chain
borrowCrossChaincollateral read is reproduced verbatim (marked@>) alongside the destination-chain_handleBorrowCrossChainRequestcheck, with faithful minimal doubles for LayerZero transport, LendStorage, and the token; local deploy, no fork.
Root cause#
In borrowCrossChain(), the collateral read destructures getHypotheticalAccountLiquidityCollateral, which returns (totalBorrowed, totalCollateral), but keeps only the collateral value and discards totalBorrowed. The raw collateral — with no deduction for the user's existing debt — is then shipped to the destination chain as the sole liquidity check. The vulnerable lines, reproduced verbatim:
@> (, uint256 collateral) =
lendStorage.getHypotheticalAccountLiquidityCollateral(msg.sender, LToken(_lToken), 0, 0);
The destination chain validates the inbound borrow only against this raw value:
require(payload.collateral >= totalBorrowed, "Insufficient collateral");
where totalBorrowed is only the destination-chain debt plus the new borrow. Because the source chain's existing debt was never subtracted, a user already borrowed near their limit on chain A can borrow again on chain B against the same collateral.
Why it's exploitable here#
Following the finding's worked example ($1 stablecoins, 80% collateral factor):
- The attacker supplies 1000 USDC on chain A → $800 borrowing capacity.
- The attacker borrows 600 USDT on chain A — an honest same-chain borrow, leaving only $200 of capacity.
- The attacker calls
borrowCrossChain(700, USDT, chainB). The source reads collateral as the raw $800 and ships that value — the $600 debt is dropped. - Chain B receives
collateral = 800, computes its owntotalBorrowed = 0 + 700, and checksrequire(800 >= 700)→ passes. CoreRouter.borrowForCrossChainpays out the full 700 USDT from honest depositors' destination liquidity.
Total exposure is now $1300 against $800 capacity (162.5% utilization). A correct check would have compared the borrow against the $200 of available capacity and reverted. The reproduction asserts the concrete harm: crossChainReceived == 700e18, dstPoolDrained == 700e18, while availableCapacity == 200e18.
Attack path#
Marked-line walkthrough (Playground)#
The EVM Playground pins each step to the exact executed source line in 0x8f111d8b…:
- L254 — Wire the cross-chain peer: Setup:
setPeerstores the counterpart router aspeer, the faithful LayerZero link the source chain uses to deliver its borrow messages. - L279 — Market-entry membership check:
isMarketEnteredscans the user's supplied assets to confirm the collateral lToken is an entered market before the cross-chain borrow proceeds. - L293 — Enter borrowCrossChain on source: The attacker calls
borrowCrossChainon chain A to borrow 700 USDT on chain B, having already borrowed 600 against the same 1000 USDC collateral. - L312 — Confirm collateral market entered:
borrowCrossChainchecks whether the source collateral lToken is an entered market, entering it if needed, before computing the value to ship. - L319 — Ship raw collateral, drop debt: Root cause: the call returns
(totalBorrowed, collateral)but onlycollateralis kept — raw $800 is shipped while the existing $600 debt is ignored. - L323 — Send message to destination chain:
_sendpacks the payload — amount 700 and the inflated $800collateral— and hands it to the faithful LayerZero transport bound for chain B. - L344 — Deliver payload across LayerZero hop:
_sendforwards the payload (sender, dest lToken, null liquidator, source token) to the peer router, delivering the inflated $800 collateral to chain B's check.
PoC#
Registry (Foundry, local deploy — verbatim vulnerable source + harm-asserting test):
cd 58375-lend-cross-chain-borrow-ignores-existing-debt-in-collateral-validation_exp && forge test -vvv
The browser Playground replays the same synthetic opcode-for-opcode and measures the harm: supply 1000 USDC, borrow 600 on chain A, then cross-chain borrow 700 on chain B against the same collateral — draining the destination pool for a debt that should have been rejected. Both gates are green (registry forge test PASS + Playground _verify-poc VERDICT: PASS).
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 58375-lend-cross-chain-borrow-ignores-existing-debt-in-collateral-validation_exp (evm-hack-registry mirror).
- AuditVault finding: 341.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "LEND: Cross-chain borrow ignores existing debt in collateral validation".
- 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.