Reproduced Exploit
Lend V2: cross-chain liquidation check misuses `payload.amount`
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: 930. Standalone Foundry PoC and full write-up: 58391-lend-liquidation-validation-logic-is-wrong_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/theft · vuln/logic
Reproduction: a faithful minimal reproduction of the vulnerable finding — the vulnerable
_checkLiquidationValidcheck is reproduced verbatim (marked@>) with faithful minimal doubles; local deploy, no fork.
Root cause#
On Chain A (the collateral chain), the cross-chain liquidation check treats payload.amount as if it were an additional borrow amount — but payload.amount is the number of collateral tokens to seize (seizeTokens, computed on Chain B). It is passed into getHypotheticalAccountLiquidityCollateral(...) as the borrowAmount parameter, so the check asks "if this user borrowed that much more, would they be underwater?" instead of validating actual health. The vulnerable line, reproduced verbatim:
function _checkLiquidationValid(LZPayload memory payload) private view returns (bool) {
(uint256 borrowed, uint256 collateral) = lendStorage.getHypotheticalAccountLiquidityCollateral(
@> payload.sender, LToken(payable(payload.destlToken)), 0, payload.amount
);
return borrowed > collateral;
}
Because payload.amount is a seize amount and not a proposed borrow, a perfectly healthy position can be marked liquidatable purely because "borrowing seizeTokens more" would tip it over the edge — even though no such borrow ever happens.
Why it's exploitable here#
Following the synthetic's worked example, with collateral and borrow normalized at price 1:
- A healthy borrower holds
1000e18collateral against a500e18borrow (LTV 50%). Under the correct check (borrowAmount = 0):borrowed(500) > collateral(1000)? No — not liquidatable. - An attacker submits a cross-chain liquidation-execute message with
payload.amount = seizeTokens = 600e18. - The buggy check adds the seize amount to the borrow side:
borrowed(500 + 600 = 1100) > collateral(1000)? Yes — the healthy position is flagged liquidatable. _handleLiquidationExecuteruns:600e18of the healthy borrower's collateral is seized and credited to the attacker — a direct drain of a solvent user's funds.
Attack path#
Marked-line walkthrough (Playground)#
The EVM Playground pins each step to the exact executed source line in 0xce01759b…:
- L133 — Seize amount treated as borrow: Root cause: payload.amount is the collateral seize amount but is passed here as a hypothetical borrowAmount, so a healthy position is flagged liquidatable.
- L141 — Seize amount becomes liquidator share: The same payload.amount is taken as the liquidator's share, the collateral to move out of the healthy borrower's position.
- L147 — Credit seized collateral to attacker: The seized collateral is credited to the liquidator, transferring the healthy borrower's funds to the attacker.
- L159 — Liquidation reported as executed: liquidate returns true, so the wrongful seizure is confirmed executed even though the borrower was fully solvent.
- L161 — Correct-rejection path never reached: This else branch would have correctly rejected the liquidation, but the buggy check never routes the healthy borrower here.
- L176 — Seized collateral measured at sink: The sink marker token records the 600e18 of collateral wrongly seized from the healthy borrower as a concrete, measurable loss.
PoC#
Registry (Foundry, local deploy — verbatim vulnerable source + harm-asserting test):
cd 58391-lend-liquidation-validation-logic-is-wrong_exp && forge test -vvv
The browser Playground replays the same synthetic opcode-for-opcode and measures the harm: a healthy borrower (1000e18 collateral vs 500e18 borrow) is flagged liquidatable and has 600e18 of collateral seized. Both gates are green (registry forge test PASS + Playground _verify-poc VERDICT: PASS).
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 58391-lend-liquidation-validation-logic-is-wrong_exp (evm-hack-registry mirror).
- AuditVault finding: 930.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "Lend V2: cross-chain liquidation check misuses
payload.amount". - 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.