Reproduced Exploit
OpenLeverage `LPool.doTransferOut` — native `transfer` 2300-gas stipend freezes contract lenders' funds
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: 42441-h-01-openlevv1libs-and-lpools-dotransferout-functions-call-n. Standalone Foundry PoC and full write-up: 42441-h-01-openlevv1libs-and-lpools-dotransferout-functions-call-n_exp in the
evm-hack-registrymirror.
AuditVault #42441 (H-01) — OpenLeverage (Code4rena, 2022-01)
Root cause#
For a WETH money-market pool, LPool.doTransferOut unwraps WETH to native ETH
and forwards it with Solidity's payable.transfer, which hard-caps the callee
to a 2300-gas stipend:
// openleverage-contracts/contracts/liquidity/LPool.sol
function doTransferOut(address payable to, uint amount, bool convertWeth) internal {
if (isWethPool && convertWeth) {
IWETH(underlying).withdraw(amount);
to.transfer(amount); // @> 2300-gas stipend
} else {
IERC20(underlying).safeTransfer(to, amount);
}
}
Any lender that is a smart contract (a multisig, a vault, a router, another
protocol) whose ETH-receiving path does more than trivial work — e.g. a single
SSTORE (~20k gas) — cannot execute inside the 2300-gas stipend. The
transfer reverts, and with it the entire redeem / redeemUnderlying /
borrow call. The identical bug exists in OpenLevV1Lib.doTransferOut
(payable(to).transfer(amount)), which backs OpenLevV1.closeTrade and
liquidate — so a contract trader's principal-return path can be permanently
bricked as well. The funds stay escrowed in the pool with no way out.
Real exploit (this PoC)#
The PoC deploys the real audited LPool through the real LPoolDelegator
Compound-style proxy (both copied verbatim from the contest repo), backed by
OpenLeverage's own test WETH, and drives it through the real mintEth /
redeem path. Two contract lenders each supply 1 ETH:
CheapLender(emptyreceive(), fits the 2300-gas stipend) supplies 1 ETH, thenredeems — succeeds, gets its 1 ETH back.ContractLender(ordinaryreceive()doing oneSSTORE, ~20k gas) supplies 1 ETH, thenredeems — the realdoTransferOutunwraps the WETH and callsto.transfer(amount), which runs out of gas in the stipend and reverts the whole redeem.
Concrete harm: the ContractLender receives nothing, still holds its
1e18 unredeemable lTokens, and its 1 ETH (1e18 wei) of WETH is permanently
frozen in the pool. The Playground scores exactly this — a 1 ETH WETH balance
stranded at the pool address that its rightful owner can never withdraw.
Reproduce#
_shared/run-poc/run_poc.sh 42441-h-01-openlevv1libs-and-lpools-dotransferout-functions-call-n_exp -vvvvv
The Forge test deploys the real LPool/LPoolDelegator/WETH, runs the two
lenders, and asserts the concrete harm: CheapLender withdraws its full 1 ETH,
ContractLender's redeem reverts, and weth.balanceOf(pool) == 1 ether
(the frozen deposit). The trace shows WETH::withdraw followed by
ContractLender::receive hitting an out-of-gas inside the transfer stipend,
reverting the redeem.
Fix#
Replace transfer with a gas-forwarding low-level call (checked) or
OpenZeppelin Address.sendValue. All affected paths (LPool.redeem /
redeemUnderlying / borrow, and OpenLevV1.closeTrade / liquidate) are
already nonReentrant, so reentrancy is not a concern:
(bool ok, ) = to.call{value: amount}("");
require(ok, "ETH transfer failed");
Sources: AuditVault finding #42441,
real repo code-423n4/2022-01-openleverage —
LPool.sol#L294-L301,
OpenLevV1Lib.sol#L253.
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 42441-h-01-openlevv1libs-and-lpools-dotransferout-functions-call-n_exp (evm-hack-registry mirror).
- AuditVault finding: 42441-h-01-openlevv1libs-and-lpools-dotransferout-functions-call-n.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "OpenLeverage
LPool.doTransferOut". - 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.