LML / APower Reward-Claim Price Manipulation
On March 31, 2026 at 20:39:02 UTC, the attacker used flash-loaned capital on BNB Chain to manipulate the LML/USDT market, then batch-triggered reward claims for pre-seeded accounts through APower and immediately sold the resulting LML back into the distorted pool. The primary issue is a price-manipulable reward-claim flow: the public APower claim entrypoint calls updatePrice(), updateUser(account), and claimReward(account) in the same transaction, after the attacker has already moved the LML spot price. The trace confirms 11 reward-claim attempts, 56.591788627072709517 LML paid out to claimant wallets, and a subsequent dump of 52.448700122473787159 LML for 310,718,697.249411714088249714 gross USDT before the financing legs were unwound. The user-supplied hypothesis is therefore substantially confirmed: the exploit is not a pure flash-loan bug, but a reward-accounting design that can be re-priced and settled inside the same manipulated market window.
Root Cause
Vulnerable Contract
The economically vulnerable backend is the stake proxy at 0xae406f357541f45f01bec21f9f28c43757f202e4, which delegates to unverified implementation 0xbe97138647a993d9d1aabb25f10b3611c0adce19. The publicly reachable trigger path is APower at 0xb7b7631b97d93344b2a29e926e42578006794b3b, whose verified source is available locally as 0xb7.../APower_excerpt.sol. The trace shows every reward claim going through APower -> StakeProxy.claimReward(address) at depths 37-38, so APower is the user-controlled entrypoint and the stake proxy is the reward-accounting sink.
Source type:
APower: verified excerptStakeProxy: proxy with unverified implementationAttackerHelper: recovered only, medium confidence, used only to explain call orchestration
Vulnerable Function
The concrete exploit entrypoint is APower receive() together with _claimReward(address):
receive()has no selector; any plain call to APower withmsg.value < userMinenters_claimReward(account)._claimReward(address)then executesupdatePrice() -> updateUser(account) -> claimReward(account)against the stake proxy.
Trace evidence:
idx 311,515,773,1037,1298+,1450+,1599+,1744+,1875+,2040+,2213+: attacker-controlled claimant addresses call APower with empty calldata.idx 318/319,522/523, …,2220/2221: APower callsStakeProxy.updatePrice().idx 320/321,524/525, …,2222/2223: APower callsStakeProxy.updateUser(address).idx 339/340,543/544, …,2241/2242: APower callsStakeProxy.claimReward(address).
Vulnerable Code
function _transfer(address from, address to, uint256 amount) internal virtual {
amount;
if (to == address(this) && tx.origin == from) {
_claimReward(from); // <-- VULNERABILITY: direct self-transfer can trigger claim settlement
}
else revert("Can't Transfer");
}
receive() external payable {
address account = msg.sender;
ISTAKE _STAKE = ISTAKE(stakeAdd);
(IMAIN.ConfigSingle memory config, IMAIN.TokenAdd memory _ta, ) = IMAIN(mainAdd).getConfig();
(ISTAKE.ConfigSingle memory _cs, ISTAKE.TokenAdd memory tokenAdd, ) = _STAKE.getConfig();
require(!IMAIN(mainAdd).isBlackList(msg.sender), "Power: User Invalid");
if (msg.value < _cs.userMin) {
if (msg.value > 0) payable(account).transfer(msg.value);
_claimReward(account); // <-- VULNERABILITY: zero-value/plain calls enter claim path
}
else {
uint usdt = _ta.USDT.balanceOf(address(this));
uint lp = _ta.LP.balanceOf(address(this));
if (lp > 0) {
_ta.LP.transfer(_ta.lpRecieve, _ta.LP.balanceOf(address(this)));
}
AiWeb3Tools.swapETHForToken(_ta.ROUTER, _ta.USDT, msg.value, 1000, address(this));
usdt = _ta.USDT.balanceOf(address(this)) - usdt;
_ta.USDT.transfer(tokenAdd.market, (usdt * config.fundRate) / 10000);
{
uint beforeBalance = _ta.TOKEN.balanceOf(address(this));
AiWeb3Tools.swapForToken(_ta.ROUTER, _ta.USDT, _ta.TOKEN, (usdt * (10000 - config.fundRate)) / 20000, 1000, address(this));
beforeBalance = _ta.TOKEN.balanceOf(address(this)) - beforeBalance;
AiWeb3Tools.addLiquidityUSDT(_ta.ROUTER, _ta.USDT, (usdt * (10000 - config.fundRate)) / 20000, _ta.TOKEN, beforeBalance, address(this));
lp = _ta.LP.balanceOf(address(this));
_ta.LP.transfer(_ta.lpRecieve, _ta.LP.balanceOf(address(this)));
}
_STAKE.deposit(account, msg.value, lp, usdt);
_mint(account, lp);
}
}
function _claimReward(address account) private {
ISTAKE _STAKE = ISTAKE(stakeAdd);
_STAKE.updatePrice(); // <-- VULNERABILITY: refreshes claim state from live manipulated market conditions
_STAKE.updateUser(account); // <-- VULNERABILITY: re-prices the user immediately after the manipulated update
uint balance = _STAKE.users(account).balance;
uint rate = IMAIN(mainAdd).getDynamicRate();
balance = balance * (10000 + rate) / 10000;
if (balance > 0) {
(, IMAIN.TokenAdd memory _ta, ) = IMAIN(mainAdd).getConfig();
IMAIN(address(_ta.PROOF)).sendMining(balance);
_ta.TOKEN.approve(address(_STAKE), balance);
_STAKE.claimReward(account); // <-- VULNERABILITY: claim is settled in the same manipulated window
if (_ta.TOKEN.balanceOf(address(this)) > 0) {
_ta.TOKEN.transfer(address(_ta.PROOF), _ta.TOKEN.balanceOf(address(this)));
}
}
}
The LML token also participates in the issue because its transfer logic continuously pushes price updates into the stake system:
if (!_inSwapAndLiquify && _cs.startBlock > 0 && !(to == _ta.recieve || to == address(_ta.PROOF) || to == address(_ta.POWER) || to == address(this) || to == _dead) && from == _ta.swapPair) {
_inSwapAndLiquify = true;
try _ta.STAKE.updatePrice() {} catch {}
try _ta.STAKE.updatePool() {} catch {}
_inSwapAndLiquify = false;
} else if (!_inSwapAndLiquify && _cs.startBlock > 0 && !(from == _ta.recieve || from == address(_ta.PROOF) || from == address(_ta.POWER) || from == address(this)) && to == _ta.swapPair) {
_inSwapAndLiquify = true;
try _ta.STAKE.updatePrice() {} catch {}
try _ta.STAKE.updatePool() {} catch {}
_inSwapAndLiquify = false;
}
This excerpt comes from 0x737d.../0x737d....sol lines 744-753 and shows that LML buy/sell activity itself can refresh stake-side price state.
Why It’s Vulnerable
Expected behavior:
- Reward claims should not be settled from a price that the claimant can manipulate in the same transaction.
- The claim trigger should not allow a plain zero-value call to force
updatePrice(),updateUser(account), andclaimReward(account)back-to-back while the attacker still controls the market state. - If a market price is needed for reward settlement, it should come from manipulation-resistant data or a delayed checkpoint that cannot be changed and consumed atomically.
Actual behavior:
- APower exposes
_claimReward(account)through both self-transfer and zero-value/plainreceive(). _claimReward(account)immediately callsupdatePrice(), thenupdateUser(account), thenclaimReward(account).- The attacker manipulates the LML/USDT pool first, then calls APower from attacker-controlled claimant wallets, so the stake system settles rewards against the manipulated price window.
- The attacker then routes the freshly claimed LML back to the helper and sells it into the same distorted pool before unwinding financing.
Why this matters:
- The exploitable invariant is not “can rewards be claimed?” but “can the claim-trigger and the price refresh happen in the same attacker-controlled transaction?” Here the answer is yes.
- The trace proves the attacker did not need a separate delayed settlement phase. Price movement, price refresh, account update, reward claim, reward aggregation, and dump all happen inside one transaction.
- The user’s TWAP/snapshot hypothesis remains plausible for the unverified stake implementation, but the trace is already sufficient to conclude that reward settlement consumed a manipulable same-tx price update. That is the on-chain-proven root cause.
Attack Execution
High-Level Flow
- The attacker calls
AttackerHelper.Transfer(...)from EOA0x982e.... - The helper draws nested Moolah flash loans and additional temporary capital from Venus and an Aave-style pool.
- The helper enters a chain of Pancake V2/V3 and Uniswap-style flash-swap callbacks, then calls
LML.swapAndTrans(). - The attack path distorts the LML/USDT pair, including a pair transfer of
21481.670943088126592818LML to0xdead, which is consistent with the user’s “receiver = address(0)” manipulation hint. - While the manipulated market state is still live, 11 attacker-controlled claimant wallets send plain calls into APower, which triggers
_claimReward(account)for each wallet. - APower calls
StakeProxy.updatePrice(),StakeProxy.updateUser(account), andStakeProxy.claimReward(account)for those wallets in the same transaction. - Claimed LML is transferred out to the claimant wallets, then most of it is sent back into the attacker helper.
- The helper approves and sells the claimed LML through Pancake Router into the LML/USDT pair, realizes gross USDT proceeds at the distorted price, and then unwinds Venus, Aave, and Moolah financing legs.
Detailed Call Trace
EOA (0x982e1dc1...) → AttackerHelper (0x03811ea7...).Transfer() [0xd1398bee]
[PHASE 1: NESTED FLASH LOANS]
ListaMoolah.flashLoan(USDT, 8,211,014.02) [0xe0232b42]
→ USDT.transfer(AttackerHelper, 8,211,014.02 USDT)
→ AttackerHelper.onMoolahFlashLoan() [0x13a1a562]
ListaMoolah.flashLoan(WBNB, 382,768.85) [0xe0232b42]
→ WBNB.transfer(AttackerHelper, 382,768.85 WBNB)
→ AttackerHelper.onMoolahFlashLoan() [0x13a1a562]
[PHASE 2: AMPLIFY CAPITAL VIA LENDING]
Venus vWBNB.mint(314,768.85 WBNB) [0xa0712d68]
Venus vUSDT.borrow(91,670,118.22 USDT) [0xc5ebeaec]
Aave Pool.supply(USDT, ...) [0x617ba037]
Aave Pool.borrow(USDT, ...) [0xa415bcad]
[PHASE 3: FLASH-SWAP CASCADE (10 Pancake V3 + 3 V2 pairs)]
PancakeV4.unlock() → unlockCallback() [0x48c89491]
PancakeV3 (0x81c729...).flash() → uniswapV3FlashCallback()
PancakeV3 (0xe1acb4...).flash() → uniswapV3FlashCallback()
... 8 more nested V3 flash callbacks ...
PancakeV3 (0x9c4ee8...).flash() → pancakeV3FlashCallback()
Pair (0xcaaf3c...).swap(USDT) → pancakeCall() [0x022c0d9f]
Pair (0x999687...).swap(USDT) → pancakeCall()
Pair (0xb720ea...).swap(USDT) → pancakeCall()
[PHASE 4: PRICE MANIPULATION]
LML.swapAndTrans() [0xafe9d95c]
PancakeRouter.swapExact...(USDT → LML) [0x5c11d795]
PancakePairUSDTLML.swap() → pair reserves distorted
LML._transfer() → 21,481.67 LML burned to 0xdead
PancakeRouter.swapExact...(USDT → LML) [0x5c11d795]
PancakePairUSDTLML.swap() → second swap deepens distortion
[PHASE 5: REWARD CLAIM — 11 CLAIMANT WALLETS] ← EXPLOIT
[x11] AttackerHelper → Claimant.transfer(addr,addr) [0xba45b0b8]
→ Claimant calls APower with empty calldata
APower.receive() → _claimReward(claimant):
StakeProxy.updatePrice() [0x673a7e28] ← reads manipulated price
StakeProxy.updateUser(claimant) [0xed03b336] ← re-prices at manipulated rate
StakeProxy.users(claimant) [0xa87430ba]
LML.swapBack() [0x6ac5eeee] ← transfer hook fires
StakeProxy.claimReward(claimant) [0xd279c191] ← settles at manipulated price
→ LML paid from APower to claimant
→ Claimant sends LML back to AttackerHelper
Claimant wallets:
0xd7cf95d0... (idx 310) 0xe50e39cd... (idx 1443)
0xe8c28290... (idx 514) 0xfd89c599... (idx 1582)
0x07504be7... (idx 772) 0x052d79ff... (idx 1728)
0x7922d9ec... (idx 1036) 0x6bf576c9... (idx 1874)
0x053e493f... (idx 1300) 0x5ba6e85f... (idx 2039)
0x8ee66ca2... (idx 2212)
→ Total claimed: 56.59 LML | Total returned to helper: 60.15 LML
[PHASE 6: DUMP CLAIMED LML]
PancakeRouter.swapExact...(52.45 LML → USDT) [0x5c11d795]
LML._transfer() → StakeProxy.updatePrice() (transfer hook)
PancakePairUSDTLML.swap() → 310,718,697.25 USDT to helper
[PHASE 7: REPAY FLASH-SWAPS]
USDT.transfer → repay V2 pair (0xb720ea...), (0x999687...), (0xcaaf3c...)
... repay all 10 Pancake V3 flash loans ...
[PHASE 8: UNWIND LENDING + REPAY FLASH LOANS]
Aave Pool.repay(USDT) [0x573ade81]
Aave Pool.withdraw(USDT) [0x69328dec]
Venus vUSDT.repayBorrow(91,670,118.22 USDT) [0x0e752702]
Venus vWBNB.redeem() [0xdb006a75]
WBNB.transferFrom → ListaMoolah (repay 382,768.85 WBNB)
USDT.transferFrom → ListaMoolah (repay 8,211,014.02 USDT)
Financial Impact
Observable reward-claim leg:
- The trace contains 11
claimReward(address)attempts through APower and the stake proxy. - Receipt-derived transfer data shows 11 positive LML reward payouts from APower to claimant wallets, totaling
56.591788627072709517LML (on-chain verified; earlier draft figure of51.452744625963916443was an undercount). - Those claimant wallets then send
60.147591883570861171LML back to the attacker helper during the same transaction (on-chain verified; earlier draft figure of48.944359774500526415was an undercount).
Observable dump leg:
- The helper transfers
52.448700122473787159LML intoPancakePairUSDTLML. - The pair returns
310,718,697.249411714088249714gross USDT to the helper in the same manipulated window. - That implies a realized sale rate of roughly
5,924,240.191345973910021591986USDT per LML, which is plainly inconsistent with organic market behavior and is strong on-chain evidence of successful price distortion.
Net profit caveat:
funds_flow.jsonshows the helper’s net USDT delta as approximately zero by the end of the transaction because the same transaction also repays flash loans and unwinds debt positions.- This transaction therefore proves the over-claim and the inflated dump price, but it does not by itself isolate final attacker profit after all financing legs. The helper retains only dust-level residual balances on the tx-local net-change view.
- The safest financial statement is: the attacker over-claimed at least
56.591788627072709517LML during the manipulated claim window and realized310,718,697.249411714088249714gross USDT on the dump leg before repayment/unwind.
Assessment
Primary classification: price_manipulation
Secondary classification: flash_loan_abuse
Assessment details:
- The user hint about attacker-controlled claimant wallets is directly confirmed by the 11 APower receive-path calls followed by
claimReward(address)for those same wallets. - The claim flow is public and same-tx: a plain APower call with
msg.value < userMinis enough to enter_claimReward(account). - The exact internal stake implementation remains unverified, so statements about whether it uses a pure spot price, a lagged snapshot, or a derived checkpoint are an inference. What is proven on-chain is narrower and sufficient: the claim path refreshes price and settles reward inside the manipulated market window, which is enough to explain the exploit.
Remediation
- Remove any same-tx path that lets a user trigger
updatePrice()andclaimReward()atomically after manipulating the underlying market. - Replace manipulable AMM-derived pricing in reward settlement with manipulation-resistant oracle data or delayed checkpoints that cannot be changed and consumed in the same transaction.
- Separate “price update” and “reward claim” across time, or at minimum enforce a cooldown between them.
- Eliminate public zero-value and self-transfer claim triggers in APower; require explicit authenticated claim functions with stronger state gating.
- Review all LML transfer hooks that call
Stake.updatePrice()/updatePool()on every pair interaction, because they make reward state reactive to attacker-controlled spot moves.
Evidence
- Transaction hash:
0x805d273a63d905d7827d43f6dc051eafdcd0cb69a07c7eb74358c6a5c6255b47 - Block:
89867310 - Timestamp:
2026-03-31T20:39:02Z - Status: success (
0x1) - Gas used:
23,405,858 - Log count:
539
Selector verification:
0xd1398bee=Transfer(address,address,address,uint256)— AttackerHelper entrypoint0xe0232b42=flashLoan(address,uint256,bytes)— Moolah flash loans (4 calls)0x13a1a562=onMoolahFlashLoan(uint256,bytes)— flash loan callbacks (2 calls)0x490e6cbc=flash(address,uint256,uint256,bytes)— Pancake V3 flash loans (10 calls)0xa1d48336=pancakeV3FlashCallback(uint256,uint256,bytes)— V3 callbacks (8 calls)0xe9cbafb0=uniswapV3FlashCallback(uint256,uint256,bytes)— Uniswap V3 callbacks (2 calls)0x022c0d9f=swap(uint256,uint256,address,bytes)— PancakeSwap V2 swaps (7 calls)0x84800812=pancakeCall(address,uint256,uint256,bytes)— V2 flash-swap callbacks (3 calls)0xafe9d95c=swapAndTrans()— LML price manipulation trigger (1 call)0x5c11d795=swapExactTokensForTokensSupportingFeeOnTransferTokens(...)— PancakeRouter swaps (3 calls)0xba45b0b8=transfer(address,address)— claimant wallet APower triggers (11 calls)0x673a7e28=updatePrice()— StakeProxy price refresh (24 calls)0xed03b336=updateUser(address)— StakeProxy user update (22 calls)0xd279c191=claimReward(address)— StakeProxy reward claim (22 calls: 11 via APower + 11 delegatecall)0x6ac5eeee=swapBack()— LML internal swap hook (11 calls)0xa87430ba=users(address)— StakeProxy user query (618 calls)0xc3f909d4=getConfig()— config lookups (488 calls)
Validated call counts and amounts:
11claimant wallet triggers into APower (idx 310, 514, 772, 1036, 1300, 1443, 1582, 1728, 1874, 2039, 2212)11claimReward(address)calls to StakeProxy (idx 339, 543, 801, 1065, 1326, 1472, 1611, 1757, 1903, 2068, 2241)56.591788627072709517LML total claimed by 11 wallets (receipt-verified)60.147591883570861171LML sent back from claimant wallets to AttackerHelper (receipt-verified)52.448700122473780993LML dumped into PancakePairUSDTLML (receipt log 0x1f8)310,718,697.249411702156066895USDT received from pair (receipt log 0x201)21,481.670943088127387455LML burned to 0xdead during manipulation (receipt log 0x55)8,211,014.021716461258690486USDT flash-loaned from Moolah (first loan)382,768.850110561062762505WBNB flash-loaned from Moolah (second loan)
Code-path evidence:
- APower
_claimReward(account)callsupdatePrice(),updateUser(account),claimReward(account)in sequence — confirmed in0xb7b7.../APower_excerpt.sollines 72-88. - APower
receive()enters_claimReward()whenmsg.value < userMin— confirmed in source line 46-48. - LML
_transfer()callsSTAKE.updatePrice()andSTAKE.updatePool()on pair interactions — confirmed in0x737d.../0x737d....sollines 744-753. - StakeProxy delegates all calls to unverified implementation
0xbe97138647a993d9d1aabb25f10b3611c0adce19— confirmed byDELEGATECALLat trace depths 38 for everyupdatePrice/updateUser/claimReward.
Related URLs
- Transaction: https://bscscan.com/tx/0x805d273a63d905d7827d43f6dc051eafdcd0cb69a07c7eb74358c6a5c6255b47
- Attacker EOA: https://bscscan.com/address/0x982e1dc183313ae16002a88aff460c0e6a20e1b6
- AttackerHelper: https://bscscan.com/address/0x03811ea796a084a78356cdedc27de8676457dfa3
- APower: https://bscscan.com/address/0xb7b7631b97d93344b2a29e926e42578006794b3b
- StakeProxy: https://bscscan.com/address/0xae406f357541f45f01bec21f9f28c43757f202e4
- StakeProxy implementation: https://bscscan.com/address/0xbe97138647a993d9d1aabb25f10b3611c0adce19
- LML Token: https://bscscan.com/address/0x737d5b7a41749ab426f41c36a242959e4d0b9e46
- PancakePairUSDTLML: https://bscscan.com/address/0x4be51ecae1860e5a5fd6db56bc4486ab1e4afad7
- ListaMoolah (flash loan provider): https://bscscan.com/address/0x8f73b65b4caaf64fba2af91cc5d4a2a1318e5d8c
- Venus vWBNB: https://bscscan.com/address/0x6bca74586218db34cdb402295796b79663d816e9
- Venus vUSDT: https://bscscan.com/address/0xfd5840cd36d94d7229439859c0112a4185bc0255
- Aave Pool: https://bscscan.com/address/0x6807dc923806fe8fd134338eabca509979a7e0cb