LAYER3 Oracle-Mint Drain
Exploit Transactions
0x1eeef7b9a12b13f82ba04a7951c163eb566aa048050d6e9318b725d7bcec6bfaVictim Addresses
0x637de69f45f3b66d5389f305088a38109aa0cf7cBSC0xdec12a1dcbc1f741ccd02dfd862ab226f6383003BSC0x3cd632c25a4db4c1a636cfb23b9285be1097a60dBSCLoss Breakdown
Similar Incidents
PHIL Public Mint Drain
39%SellToken Reward Oracle Manipulation
38%Public Mint Drains USDT Pair
38%SellToken Short Oracle Manipulation
35%NeverFallToken LP Drain
35%Cellframe Migration Drain
34%Root Cause Analysis
LAYER3 Oracle-Mint Drain
1. Incident Overview TL;DR
On BNB Chain block 29,756,867, transaction 0x1eeef7b9a12b13f82ba04a7951c163eb566aa048050d6e9318b725d7bcec6bfa exploited LAYER3's Loan and LUSDPool contracts in one public transaction. The attacker flash-borrowed 3,524,111.384121628915657192 USDT through five public DODO pools, pushed the Pancake BTCB/USDT spot price sharply upward, supplied a tiny amount of now-overpriced BTCB into Loan::supply, received 10,000.411182292803780033 freshly minted LUSD, immediately redeemed that LUSD through LUSDPool::withdraw, unwound the price manipulation, repaid the flashloans, and finished with 9,464.716730420359231245 USDT.
The root cause is a composition flaw across three public contracts. Loan::supply trusts a same-transaction Pancake spot quote as its collateral oracle, LUSD lets both Loan and LUSDPool mint or burn under the same manager-role model, and LUSDPool::withdraw pays treasury USDT immediately against whatever LUSD it receives. That made the incident an ACT exploit: no privileged key, no whitelist bypass, and no attacker-only infrastructure were required.
2. Key Background
LAYER3 used Loan at 0xdec12a1dcbc1f741ccd02dfd862ab226f6383003 to accept supported collateral and mint LUSD. For BTCB, the on-chain configuration at block 29,756,866 was:
payoutToken = 0x3cd632c25a4db4c1a636cfb23b9285be1097a60d(LUSD)redeemFee = 500supplyRatio = 5000dailyRate = 3
That configuration came directly from Loan::info(BTCB) on-chain and means Loan minted LUSD equal to 50% of the current Pancake BTCB-to-USDT quote.
LUSDPool at 0x637de69f45f3b66d5389f305088a38109aa0cf7c held the treasury USDT that made the exploit valuable. Immediately before the attack it held 10,000.411182292800000000 USDT and 1,594.549232465655263332 L3. Its withdraw settings were nodeFee = 200 and lpFee = 100, so each withdrawal sent 2% of the LUSD to nodePool, burned 1%, attempted to pair that 1% slice with L3 on Pancake, then paid the remaining 97% in USDT.
The BTCB/USDT Pancake pair at 0x3f803ec2b816ea7f06ec76aa2b6f2532f9892d62 was shallow relative to the available flash liquidity. Before the exploit it held 39,642.343651114056995536 USDT and 1.308898664806062396 BTCB. Because Loan read that pair through PancakeRouter::getAmountsOut, its collateral pricing was only as trustworthy as the current pair reserves.
3. Vulnerability Analysis & Root Cause Summary
The vulnerable behavior starts in Loan::supply. Instead of using a time-weighted oracle, external price feed, or delayed redemption flow, the contract directly asks PancakeRouter for the current BTCB/USDT spot quote and uses that quote to mint LUSD in the same transaction. That quote can be manipulated by the same caller moments earlier by trading against the referenced pair.
LUSDPool::withdraw then compounds the issue. It does not ask how the caller obtained LUSD or whether the minted amount was economically backed; it simply accepts the tokens, routes fixed fee slices, and transfers the remaining 97% in USDT from treasury inventory. The shared LUSD manager-role model makes this immediate redemption possible because a bad valuation in Loan becomes redeemable supply for LUSDPool without any additional validation layer.
The broken invariant is straightforward: newly minted LUSD must never exceed the manipulation-resistant value of the deposited collateral if that LUSD can be redeemed against treasury assets. The concrete breakpoint is the pair of lines in Loan::supply that compute usdtAmount from router.getAmountsOut and then compute payoutAmount from usdtAmount * supplyRatio / 1e4.
The exploit conditions were also simple and fully permissionless: BTCB had to remain enabled in Loan, the referenced Pancake market had to be shallow enough to manipulate with public flash liquidity, LUSDPool had to hold enough USDT to honor the post-fee redemption, and the withdrawing address merely had to avoid LUSDPool's blacklist. All four conditions held at the seed block.
4. Detailed Root Cause Analysis
The verified Loan source shows the decisive pricing logic:
// Verified Loan source
address[] memory path = new address[](2);
path[0] = address(supplyToken);
path[1] = address(USDT);
uint256 usdtAmount = router.getAmountsOut(supplyAmount, path)[1];
Order memory order = Order({
supplyToken: supplyToken,
payoutToken: info[supplyToken].payoutToken,
redeemFee: info[supplyToken].redeemFee,
supplyRatio: info[supplyToken].supplyRatio,
dailyRate: info[supplyToken].dailyRate,
supplyAmount: supplyAmount,
payoutAmount: (usdtAmount * info[supplyToken].supplyRatio) / 1e4,
supplyTime: block.timestamp,
redeemAmount: 0,
redeemTime: 0
});
Nothing in that path prevents same-transaction price manipulation. The seed trace confirms the attacker first pushed 800,000 USDT into the BTCB/USDT pair and received 1.246953598313175025 BTCB, moving reserves from 39,642.343651114056995536 USDT / 1.308898664806062396 BTCB to 839,642.343651114056995536 USDT / 0.061945066492887371 BTCB. Immediately after that, Loan asked PancakeRouter for the value of just 0.001515366635982742 BTCB:
Seed transaction trace
PancakeRouter::getAmountsOut(
1515366635982742,
[BTCB, USDT]
) -> [1515366635982742, 20000822364585607560066]
LUSD::mint(
0x724CEFeF59D673359bcA2b5A9DbA47e8Feb30b4D,
10000411182292803780033
)
So the protocol treated 0.001515366635982742 BTCB as worth 20,000.822364585607560066 USDT and minted 10,000.411182292803780033 LUSD from that quote. That is the exact over-mint.
The verified LUSDPool source shows why the over-minted LUSD became treasury loss:
// Verified LUSDPool source
LUSD.safeTransferFrom(msg.sender, address(this), amount);
uint256 nodeAmount = (amount * nodeFee) / 1e4;
LUSD.safeTransfer(nodePool, nodeAmount);
uint256 lpAmount = (amount * lpFee) / 1e4;
LUSD.burn(address(this), lpAmount);
uint256 l3Balance = L3.balanceOf(address(this));
L3.approve(address(router), l3Balance);
USDT.approve(address(router), lpAmount);
router.addLiquidity(address(L3), address(USDT), l3Balance, lpAmount, 0, 0, address(this), block.timestamp);
LUSD.burn(address(this), amount - nodeAmount - lpAmount);
USDT.safeTransfer(msg.sender, amount - nodeAmount - lpAmount);
The seed trace shows the exact fee routing and payout:
Seed transaction trace
LUSD transfer to nodePool: 200008223645856075600
LUSD burn for LP leg: 100004111822928037800
L3 sent into Pancake LP: 2596858424817186073
LUSD burn for cash leg: 9700398846824019666633
USDT paid to attacker: 9700398846824019666633
One subtle but important point is that the contract approved its full L3 balance, but PancakeRouter only consumed 2.596858424817186073 L3 alongside the 1% USDT slice at the prevailing pool price. The rest of the pool's L3 remained in place. That detail does not change the exploit mechanism: the decisive loss is the 9,700.398846824019666633 USDT transfer caused by redeeming freshly over-minted LUSD.
The LUSD token makes the composition issue worse because both victim-side contracts can directly mint or burn under the same manager-role model:
// Verified LUSD source
function mint(address user, uint256 amount) external onlyRole(MANAGER_ROLE) {
_mint(user, amount);
}
function burn(address user, uint256 amount) external onlyRole(MANAGER_ROLE) {
_burn(user, amount);
}
At block 29,756,866, both Loan and LUSDPool held MANAGER_ROLE on LUSD. That means a manipulated mint in Loan flowed into a trusted burn-and-payout path in LUSDPool with no oracle reconciliation between them.
Put differently, the protocol violated three basic security principles at once: it used a same-transaction AMM spot quote as collateral pricing, it made newly minted liabilities immediately redeemable against treasury USDT without a solvency check, and it failed to validate the mint-side oracle assumptions together with the redeem-side payout assumptions of a shared liability token.
5. Adversary Flow Analysis
The attacker cluster had three addresses that matter:
- EOA
0x0000004883ab5434202dc9d8d5bfbcd87247dee3sent the seed transaction and received the final profit. - Contract
0x21ad028c185ac004474c21ec5666189885f9e518aggregated the nested DODO flashloans. - Contract
0x724cefef59d673359bca2b5a9dba47e8feb30b4dexecuted the manipulation, mint, withdrawal, unwind, and final profit transfer.
The trace shows the flashloan stack clearly:
Seed transaction trace
0x26d0...5618::flashLoan(...)
0xFeAF...d681::flashLoan(...)
0x9ad3...e69A::flashLoan(...)
0x6098...B476::flashLoan(...)
0x8191...fC1d::flashLoan(...)
Those calls delivered 3,524,111.384121628915657192 USDT to the attacker helper in the same transaction. The helper then transferred 800,000 USDT into the BTCB/USDT pair, inflated the BTCB quote, approved Loan for 0.001515366635982742 BTCB, minted LUSD, approved LUSDPool, withdrew USDT, sold the remaining BTCB back into the pair for 799,764.317883596339564612 USDT, repaid the flashloan helper, and returned the residual 9,464.716730420359231245 USDT to the originating EOA.
The balance diff proves that the profit was real and that the treasury loss was not offset elsewhere in the same transaction:
{
"attacker_eoa_usdt_delta": "9464716730420359231245",
"lusd_pool_usdt_delta": "-9800402958646947704433",
"loan_btcb_delta": "1515366635982742"
}
The attacker paid 0.00373335 BNB in gas. At block 29,756,867, PancakeRouter valued that gas at 0.877049425784078840 USDT, so the net profit remained 9,463.839680994575152405 USDT.
6. Impact & Losses
The directly drained victim asset was USDT from LUSDPool. The withdrawal path transferred 9,700.398846824019666633 USDT to the attacker-controlled contract, moved 200.008223645856075600 LUSD to nodePool, burned 9,800.403958646947704433 LUSD in total, and used 100.004111822928037800 USDT plus 2.596858424817186073 L3 to mint Pancake LP tokens.
From an impact perspective, the key loss is the treasury USDT that left LUSDPool because it was irreversibly transferred to the adversary and later surfaced as 9,464.716730420359231245 USDT on the seed EOA after all flashloan repayments. The exploit was repeatable in principle wherever the same preconditions held: an enabled collateral token, a manipulable referenced pair, and enough liquid USDT sitting in LUSDPool.
7. References
- Seed transaction:
0x1eeef7b9a12b13f82ba04a7951c163eb566aa048050d6e9318b725d7bcec6bfa - BNB Chain block:
29,756,867 - Victim contracts:
Loanat0xdec12a1dcbc1f741ccd02dfd862ab226f6383003LUSDPoolat0x637de69f45f3b66d5389f305088a38109aa0cf7cLUSDat0x3cd632c25a4db4c1a636cfb23b9285be1097a60d
- Public liquidity venues used by the exploit:
- PancakeRouter at
0x10ed43c718714eb63d5aa57b78b54704e256024e - BTCB/USDT Pancake pair at
0x3f803ec2b816ea7f06ec76aa2b6f2532f9892d62 - DODO flashloan pools
0x26d0c625e5f5d6de034495fbde1f6e9377185618,0xfeafe253802b77456b4627f8c2306a9cebb5d681,0x9ad32e3054268b849b84a8dbcc7c8f7c52e4e69a,0x6098a5638d8d7e9ed2f952d35b2b67c34ec6b476,0x81917eb96b397dfb1c6000d28a5bc08c0f05fc1d
- PancakeRouter at
- Evidence used:
- Seed transaction trace showing the flashloan chain, manipulated quote, Loan mint, LUSDPool payout, unwind swap, and final profit transfer
- Seed balance diff showing the attacker EOA profit, LUSDPool USDT loss, and collateral moved into Loan
- Verified source code for Loan, LUSDPool, and LUSD