Reproduced Exploit
LEND (Lend-V2): cross-chain liquidator seizes collateral without providing repayment
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: 636. Standalone Foundry PoC and full write-up: 58379-lend-malicious-liquidator-can-liquidate-without-providing-collateral_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/theft · vuln/logic · vuln/cross-chain
Reproduction: a faithful minimal reproduction of the vulnerable finding — the Chain-B cross-chain liquidation entry-point and its internal helpers are reproduced verbatim (marked
@>) with faithful minimal doubles for LendStorage, the Lendtroller seize math, the Chain-A seize handler and the LayerZero transport; local deploy, no fork.
Root cause#
In Lend-V2/src/LayerZero/CrossChainRouter.sol, the Chain-B (debt chain) liquidation entry-point validates the borrower's position and immediately calls _executeLiquidation, which dispatches a collateral-seize message to Chain A — but it never transfers or escrows the repayment token from the liquidator. The vulnerable lines, reproduced verbatim:
function liquidateCrossChain(
...
) external {
LendStorage.LiquidationParams memory params = LendStorage.LiquidationParams({
...
});
_validateAndPrepareLiquidation(params);
@> _executeLiquidation(params);
}
The seize on Chain A executes as an independent LayerZero message and hands the borrower's collateral to the liquidator. The return LiquidationSuccess message then tries to pull the repayment from the liquidator on Chain B via transferFrom — if the liquidator never approved, that message reverts forever while the Chain-A seize is already committed. Net effect: the liquidator receives the liquidatee's collateral for free and the borrower's debt is never deducted.
Why it's exploitable here#
Following the finding's flow with the reproduction's concrete values:
- A borrower has
100e18of outstanding cross-chain debt on Chain B, with50e18of collateral seizable on Chain A. The 50% close factor bounds a single liquidation torepayAmount = 50e18. - The malicious liquidator holds no borrow token and grants no approval, then calls the verbatim
liquidateCrossChain(borrower, 50e18, ...). Validation passes;_executeLiquidationdispatches the seize to Chain A without escrowing anything. - Chain A seizes the collateral, withholds the 2.8% protocol share, and transfers the
48.6e18liquidator share to the liquidator. - The returning
LiquidationSuccessrepaymenttransferFromreverts (no approval), so that independent Chain-B message is stuck forever — the borrower's100e18debt is never deducted. - The liquidator walks away with
48.6e18of the liquidatee's collateral, paid0, and the position remains fully indebted — a direct drain.
Attack path#
Marked-line walkthrough (Playground)#
The EVM Playground pins each step to the exact executed source line in 0xaf38a9c5…:
- L55 — 18-decimal collateral and debt tokens: Setup: MiniToken declares the 18-decimal collateral and borrowed-underlying doubles used across both chains for the seize and the repayment.
- L246 — Seed the borrower's debt: Setup: the harness seeds the borrower's 100e18 outstanding debt into Chain B's debt book, the amount an honest liquidation would deduct.
- L266 — Build the liquidation params: The verbatim liquidateCrossChain packs borrower, repayAmount and collateral into LiquidationParams, with borrowedlToken resolved during validation.
- L270 — Liquidate with no repayment escrow: Root cause: _executeLiquidation dispatches the collateral seize to the source chain without ever transferring or escrowing the repayment token from the liquidator.
- L324 — Bound repay by close factor: _prepareLiquidationValues accrues interest and applies the 50% close factor to bound repayAmount, still pulling no repayment from the liquidator.
- L371 — Dispatch seize to Chain A: _send encodes the seize payload and forwards it to Chain A as an independent message, so the collateral is seized while the liquidator escrows nothing.
PoC#
Registry (Foundry, local deploy — verbatim vulnerable source + harm-asserting test):
cd 58379-lend-malicious-liquidator-can-liquidate-without-providing-collateral_exp && forge test -vvv
The browser Playground replays the same synthetic opcode-for-opcode and measures the harm: liquidate with no approval, receive 48.6e18 of the liquidatee's collateral for free while the borrower's 100e18 debt is never deducted. Both gates are green (registry forge test PASS + Playground _verify-poc VERDICT: PASS).
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 58379-lend-malicious-liquidator-can-liquidate-without-providing-collateral_exp (evm-hack-registry mirror).
- AuditVault finding: 636.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "LEND (Lend-V2): cross-chain liquidator seizes collateral without providing repayment".
- 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.