We do not have a reliable USD price for the recorded assets yet.
0x6bbef6df8db12667ae88519090984e4f871e5febBSC0x3200be834b791d09017bd924c71174e47959b087BSC0xbe2f4d0c39416c7c4157ebfdccb65cc2ff5fb2c4BSC0x6f9070d449798f4a77d43b80ddfaacabd456d50fBSCThis incident involves a liquidity-mining protocol on BSC where an unprivileged attacker abused LPMine 0x6BBeF6DF8db12667aE88519090984e4F871e5feb and its associated reward pool to over-claim WTO rewards. The attacker used a helper contract and a large USDT flash loan to temporarily inflate AMM reserves, then repeatedly called LPMine::extractReward(1) while prices and reserves were distorted. Because LPMine’s reward logic was both flash-loan-sensitive and mis-accounted rewards across WTO and COAR LP legs, the reward pool 0x3200Be834b791D09017Bd924c71174e47959b087 distributed more than 4.0e26 WTO in a single exploit transaction.
At a high level:
coarLpAmount with a fresh reward timestamp.5,000,000 USDT via a Pancake V3 flash loan and pushes it into the ZF/USDT pool, temporarily inflating reserves and spot prices.LPMine::extractReward(1) many times. Each call computes WTO rewards using inflated reserves and double-counts contributions from both WTO and COAR LP legs, but only updates the WTO reward timestamp.402,289,684,537,844,701,832,616,528 WTO out to the helper, referral/admin addresses, and the WTO/USDT pair.33.815349128109987855 BNB after gas and flash-loan fees.0xf33f40ee0da9edebdb5cb463b37ef55df38e09690d7f68e333cf4a63046dd4cdThe root cause is a protocol bug in LPMine’s reward calculation: it values LP positions off flash-loan-sensitive reserves via getRemoveTokens and getEachReward, aggregates WTO rewards across both WTO and COAR LP legs, and allows extractReward(1) to repeatedly tap COAR-leg WTO rewards without updating coarRewardTime. All calls are available to any unprivileged user.
0x6BBeF6DF8db12667aE88519090984e4F871e5feb) is a liquidity-mining contract on BSC. It accepts LP tokens for two tokens (WTO and COAR) and rewards stakers with WTO and COAR over time. For each user it tracks:
wtoLpAmount and coarLpAmount (LP token balances),depositTime, wtoLpBackTime, coarLpBackTime,wtoRewardTime and coarRewardTime.0x3200Be834b791D09017Bd924c71174e47959b087) is a TokenDistributor-style contract created by LPMine’s constructor. It stores rewards (including WTO) and exposes claimToken(token, amount, to), callable only by its owner/admin (LPMine), to transfer reward tokens to stakers and invitees.0x692097F0d3Bd0dFBbbbb0EE35000729F05d598f5) and ZF (0x259A9FB74d6A81eE9b3a3D4EC986F08fbb42121A) are ERC20 tokens with fee-on-transfer mechanics when interacting with their Pancake pairs. Verified sources show WTO has a burn pool and owner-controlled fee parameters, but no anti-flash-loan safeguards at the token level.PancakeRouter::getAmountsOut:
0xBE2F4D0C39416C7C4157eBFdccB65cc2FF5fb2C4.0x6F9070D449798f4a77d43B80ddfAAcabD456d50f.0x36696169C63e42cd08ce11f5deeBbCeBae652050 is later used to borrow USDT via flash loans.0x593749d5414d8a735cda16e4b47cc9bfa47d5683
0x11c1ef2c..., 0x00c5a772..., 0xf33f40ee...).0x0557f67b2D5Dc575fe3e433E7caf71eA523979fD
LPMine::extractReward calls.Origin: collected LPMine source (verified on explorer) and embedded TokenDistributor for contract 0x6BBeF6DF....
contract TokenDistributor {
address public _owner;
address public _admin;
constructor (address admin) {
_owner = msg.sender;
_admin = admin;
}
function claimToken(address token, uint256 amount, address to) external {
require(msg.sender == _admin || msg.sender == _owner);
IERC20(token).transfer(to, amount);
}
}
contract LPMine is Ownable {
using SafeMath for uint256;
address private immutable usdtAddress;
IUniswapV2Router02 private immutable uniswapV2Router;
TokenDistributor public immutable rewardPool;
// ...
struct PledgeInfo {
uint256 wtoLpAmount;
uint256 coarLpAmount;
uint256 depositTime;
uint256 wtoLpBackTime;
uint256 coarLpBackTime;
uint256 wtoRewardTime;
uint256 coarRewardTime;
}
mapping(address => PledgeInfo) public userPledge;
// ...
}
Caption: LPMine deploys an internal TokenDistributor reward pool and tracks per-user WTO/COAR LP positions and reward timestamps used in the faulty reward calculation.
At block height 45,583,892 (0x2b78e14), the relevant public BSC state is:
0x6BBeF6DF... is deployed with WTO and COAR tokens registered, monthFee configured, and rewardPool 0x3200Be83... funded with WTO.0xBE2F4D0... and WTO/USDT pair 0x6F9070D4... exist with non-zero liquidity.This is supported by:
0x11c1ef2c... at this block.Contract.sol.The attack unfolds through three adversary-crafted transactions on chain BSC (56):
Tx 1 – LP priming (index 1, 0x11c1ef2c61f5a2e41d570a1547d2d891bf916853ddd94e32097e86bcdd21cb4c)
0x593749d5... sends 1 BNB to helper contract 0x0557f67b... and calls a pledge(uint256)-style entrypoint.0xBE2F4D0....partakeAddLp(2, ..., 0x114FAA79...), creating a large coarLpAmount for user 0x0557f67b... with coarRewardTime set to the block timestamp.Tx 2 – Core exploit (index 2, 0x00c5a772a58b117f142b2cbc8721b80d145ef7a910043ad08439863d0e78e300)
0x0557f67b....PancakeV3Pool::flash from 0x36696169..., a permissionless V3 pool.0xBE2F4D0..., inflating reserves and spot prices.LPMine::extractReward(1) repeatedly, using the bugged reward logic to over-claim WTO from rewardPool.LPMine::extractReward(1) and rewardPool::claimToken are available to any user with gas.Tx 3 – Profit realization (index 3, 0xf33f40ee0da9edebdb5cb463b37ef55df38e09690d7f68e333cf4a63046dd4cd)
0x1A0A18AC4BECDDbd6389559687d1A73d8927E416.permit on a helper token 0x31c2F6fc... to authorize spending of the EOA’s USDT.0x172fcd41... and 0xf2688Fb5..., unwraps WBNB to BNB, and returns BNB to the EOA.The exploit predicate is a profit-based condition in the reference asset BNB, measured on the attacker EOA 0x593749d5... across the three transactions.
0x593749d5....value_before_in_reference_asset and value_after_in_reference_asset are marked “unknown”), but the net delta is computed exactly from traces.From balance_diff.json files:
0x11c1ef2c...)
-1,001,691,652,000,000,000 wei (≈ -1.001691652 BNB).0x00c5a772...)
-129,234,354,000,000,000 wei (≈ -0.129234354 BNB).0xf33f40ee...)
+34,946,275,134,109,987,855 wei (≈ +34.946275134109987855 BNB).Summing these yields:
ΔBNB_EOA = -1,001,691,652,000,000,000
+ -129,234,354,000,000,000
+ 34,946,275,134,109,987,855
= 33,815,349,128,109,987,855 wei
≈ +33.815349128109987855 BNB
Thus the attacker EOA’s net profit, after gas and flash-loan fees, is +33.815349128109987855 BNB.
LPMine’s reward calculation is implemented through a combination of:
getCanClaimed(address _user) – computes pending WTO and COAR rewards for a user based on stored LP amounts, reward timestamps, and current pool reserves.getRemoveTokens(address _pair, address _usdtAddress, address _tokenAddress, uint256 _liquidity) – approximates the USDT and token amounts backing a given LP position using current AMM reserves.getEachReward(uint256 _valueU, uint256 _monthFee, address _wtoAddress, address _coarAddress, address _usdtAddress) – converts a USDT-denominated notional value into per-second WTO and COAR accrual rates using PancakeRouter::getAmountsOut.extractReward(uint256 _tokenId) – initiates reward claims for a user and updates reward timestamps.The defect arises from:
wtoRewardTime when claiming WTO), leaving the COAR-leg WTO component untouched and re-claimable.Within getCanClaimed:
function getCanClaimed(address _user) public view returns (uint256 _wtoAmount, uint256 _coarAmount) {
PledgeInfo memory _pledge = userPledge[_user];
Token memory _wtoToken = tokens[wtoTokenId];
Token memory _coarToken = tokens[coarTokenId];
if (_pledge.wtoLpAmount > 0) {
(uint256 _removeUsdt,) = getRemoveTokens(_wtoToken.pair, usdtAddress, _wtoToken.tokenAddress, _pledge.wtoLpAmount);
uint256 _valueU = _removeUsdt.mul(2);
uint256 _rewardTime = block.timestamp.sub(_pledge.wtoRewardTime);
(uint256 _secondWtoAmount, uint256 _secondCoarAmount) =
getEachReward(_valueU, monthFee, _wtoToken.tokenAddress, _coarToken.tokenAddress, usdtAddress);
_wtoAmount += _rewardTime.mul(_secondWtoAmount);
_coarAmount += _rewardTime.mul(_secondCoarAmount);
}
// COAR leg handled similarly...
}
Origin: collected LPMine source for 0x6BBeF6DF....
getRemoveTokens itself reads the current reserves of the AMM pair:
function getRemoveTokens(address _pair, address _usdtAddress, address _tokenAddress, uint256 _liquidity)
private
view
returns (uint256 _removeUsdt, uint256 _removeToken)
{
uint _usdtAmount = IERC20(_usdtAddress).balanceOf(_pair);
uint _tokenAmount = IERC20(_tokenAddress).balanceOf(_pair);
uint _totalSupply = IERC20(_pair).totalSupply();
_removeUsdt = _liquidity.mul(_usdtAmount) / _totalSupply;
_removeToken = _liquidity.mul(_tokenAmount) / _totalSupply;
}
Because _usdtAmount and _tokenAmount come from the current pool balances, any temporary injection of USDT (such as a flash loan deposited into the pair) directly increases _removeUsdt and thus _valueU = 2 * _removeUsdt. This is true even if the user’s LP position and the underlying economics have not changed.
getEachReward converts _valueU into per-second reward rates using current spot prices:
function getEachReward(
uint256 _valueU,
uint256 _monthFee,
address _wtoAddress,
address _coarAddress,
address _usdtAddress
) public view returns (uint256, uint256) {
uint256 _monthFeeAmount = calculateFee(_valueU, _monthFee);
(, uint256 _outWtoAmount) = getAmountOut(_usdtAddress, _wtoAddress, _monthFeeAmount);
(, uint256 _outCoarAmount) = getAmountOut(_usdtAddress, _coarAddress, _monthFeeAmount);
uint256 _secondWtoAmount = _outWtoAmount / 30 days;
uint256 _secondCoarAmount = _outCoarAmount / 30 days;
return (_secondWtoAmount, _secondCoarAmount);
}
getAmountOut uses PancakeRouter::getAmountsOut between USDT and the target token (WTO or COAR), again at spot prices with no TWAP or bounds:
function getAmountOut(address _token0, address _token1, uint256 _amountIn)
internal
view
returns (address[] memory, uint256)
{
address[] memory _path = new address[](2);
_path[0] = _token0;
_path[1] = _token1;
if (IUniswapV2Factory(uniswapV2Router.factory()).getPair(_token0, _token1) == address(0)) {
return (_path, 0);
}
uint256[] memory _amountOut = uniswapV2Router.getAmountsOut(_amountIn, _path);
uint256 _out = _amountOut[1];
return (_path, _out);
}
When reserves and prices are transiently inflated by a flash loan, _outWtoAmount and _outCoarAmount become artificially large, and dividing by 30 days produces outsized per-second accrual rates.
For a user with both wtoLpAmount and coarLpAmount:
_wtoAmount and _coarAmount based on wtoLpAmount and wtoRewardTime._wtoAmount and _coarAmount based on coarLpAmount and coarRewardTime.Thus _wtoAmount aggregates contributions from both LP legs. In the exploit, the attacker’s large COAR LP position dominates WTO entitlement.
extractRewardextractReward reads the combined rewards and then updates only one timestamp, depending on which token is being claimed:
function extractReward(uint256 _tokenId) external {
(uint256 _wtoAmount, uint256 _coarAmount) = getCanClaimed(_msgSender());
PledgeInfo storage _pledge = userPledge[_msgSender()];
uint256 _canReward;
address _tokenAddress;
if (_tokenId == wtoTokenId) {
_canReward = _wtoAmount;
_tokenAddress = tokens[wtoTokenId].tokenAddress;
_pledge.wtoRewardTime = block.timestamp;
}
if (_tokenId == coarTokenId) {
_canReward = _coarAmount;
_tokenAddress = tokens[coarTokenId].tokenAddress;
_pledge.coarRewardTime = block.timestamp;
}
if (_canReward > 0) {
rewardPool.claimToken(_tokenAddress, _canReward, _msgSender());
rewardParent(_tokenId, _tokenAddress, _canReward, _msgSender());
}
}
When a user calls extractReward(1) (WTO):
_wtoAmount includes contributions from both WTO and COAR legs.wtoRewardTime is updated; coarRewardTime is not, leaving the COAR-leg WTO portion effectively untouched and re-claimable.This asymmetric update is the key bug: it permits repeated extractReward(1) calls to re-consume COAR-leg WTO rewards, especially when _secondWtoAmount is temporarily inflated by manipulated reserves.
The root cause is the interaction of:
getRemoveTokens + getEachReward using current AMM reserves and spot prices).extractReward, which fails to advance coarRewardTime when WTO is claimed via tokenId == 1.RewardPool’s claimToken function simply transfers whatever amount LPMine instructs it to, with no hard cap tied to deposits or global emission schedules, so once the per-user reward computation is compromised, over-distribution directly drains WTO from the pool.
Adversary cluster
0x593749d5...
0x11c1ef2c..., 0x00c5a772..., 0xf33f40ee...).0x0557f67b...
LPMine::partakeAddLp and LPMine::extractReward repeatedly.Victim-side contracts and pools
0x6BBeF6DF...) – verified mining contract whose reward logic is exploited.0x3200Be83...) – holds WTO on behalf of LPMine and is drained via claimToken.0xBE2F4D0C...) – AMM whose reserves are inflated by the USDT flash loan to distort valuation.0x6F9070D4...) – AMM that receives WTO as part of reward distribution and subsequent swaps.Other addresses:
0x114FAA79... and 0xa6184d66... receive substantial WTO allocations during the exploit through referral logic and/or admin participation.0x11c1ef2c..., Block 45,583,892)In the first stage, the attacker prepares a large COAR LP position in LPMine:
0x593749d5... funds the helper contract 0x0557f67b... with 1 BNB and triggers its pledge-like entrypoint.0xBE2F4D0....LPMine::partakeAddLp(2, 2116514175087740339695220908, 348709159477963095424, 0x114FAA79157c6Ba61818CE2A383841e56B20250B)
Origin: seed transaction trace for 0x11c1ef2c....
The trace records the following event:
emit AddLP(
account: 0x0557f67b2D5Dc575fe3e433E7caf71eA523979fD,
tokenAddress: ZF: [0x259A9FB74d6A81eE9b3a3D4EC986F08fbb42121A],
lpAmount: 700983951491979097527157,
time: 1736332913
)
Caption: Seed transaction trace showing the helper contract staking ZF/USDT LP tokens into LPMine, establishing a large coarLpAmount with a fresh coarRewardTime.
This establishes:
coarLpAmount for user 0x0557f67b....coarRewardTime ≈ block timestamp of this transaction.0x00c5a772..., Block 45,586,395)The second stage is the core exploit:
5,000,000 USDT from PancakeV3Pool 0x36696169...:PancakeV3Pool::flash(
0x0557f67b2D5Dc575fe3e433E7caf71eA523979fD,
5000000000000000000000000,
0,
0x...03e8
)
Origin: exploit transaction trace for 0x00c5a772....
0xBE2F4D0..., dramatically increasing USDT reserves and distorting the price of ZF and WTO relative to USDT.LPMine::extractReward(1) in a very tight loop. The trace shows hundreds of such calls:LPMine::extractReward(1)
LPMine::extractReward(1)
...
Origin: exploit transaction trace with repeated LPMine::extractReward(1) calls.
getCanClaimed and getEachReward recompute WTO and COAR rewards using the inflated reserves from getRemoveTokens and getAmountsOut.extractReward(1) updates only wtoRewardTime, the COAR-leg WTO component based on coarLpAmount and coarRewardTime remains effectively un-reset and is re-counted in subsequent calls.rewardPool.claimToken(WTO, amount, to) transfers WTO out to:
0x0557f67b... (helper),0x114FAA79... and 0xa6184d66...,0x6F9070D4....From the high-resolution balance_diff.json for tx 0x00c5a772...:
{
"token": "0x692097f0d3bd0dfbbbbb0ee35000729f05d598f5",
"holder": "0x3200be834b791d09017bd924c71174e47959b087",
"before": "402632180519764450575928594",
"after": "342495981919748743312066",
"delta": "-402289684537844701832616528",
"contract_name": "WTO"
}
Caption: Exploit transaction balance diff showing RewardPool’s WTO balance decreasing by 402,289,684,537,844,701,832,616,528 tokens.
Companion entries show the corresponding WTO increases for the recipients:
{
"holder": "0x114faa79157c6ba61818ce2a383841e56b20250b",
"delta": "22771114219123285009393176",
"contract_name": "WTO"
}
{
"holder": "0xa6184d66bf2065b37d00d66774f25b383c9e99f7",
"delta": "11385557109561642504696700",
"contract_name": "WTO"
}
{
"holder": "0x6f9070d449798f4a77d43b80ddfaacabd456d50f",
"delta": "368133013209159774318526652",
"contract_name": "WTO"
}
Caption: WTO deltas to referral/admin addresses and the WTO/USDT pair, matching the claimed over-distribution path.
After securing WTO, the helper swaps a large portion into USDT and transfers exactly 24,281,504,512,615,756,400,792 USDT to the attacker EOA.
0xf33f40ee..., Block 45,586,459)In the final stage, the EOA monetizes the USDT:
0x1A0A18AC4BECDDbd6389559687d1A73d8927E416, passing calldata that:
0x31c2F6fcFf4F8759b3Bd5Bf0e1084A055615c768::permit to allow UniversalRouter to spend its USDT.From the trace:
UniversalRouter::execute(...)
├─ 0x31c2F6fc...::permit(0x593749D5..., ..., UniversalRouter, ...)
├─ PancakeV3Pool::swap(...)
│ ├─ BEP20USDT::transferFrom(0x593749D5..., PancakeV3Pool 0x172fcd41..., 20812718153670648343536)
│ ├─ BEP20USDT::transferFrom(0x593749D5..., 0x92b7807b..., 3468786358945108057256)
├─ WBNB::withdraw(...)
└─ fallback{value: ...}(0x593749D5...)
Origin: iter_2 trace.cast.log for tx 0xf33f40ee....
The corresponding native balance diff shows:
{
"address": "0x593749d5414d8a735cda16e4b47cc9bfa47d5683",
"before_wei": "867743108000000000",
"after_wei": "35814018242109987855",
"delta_wei": "34946275134109987855"
}
Caption: Aggregator transaction balance diff confirming a BNB gain of 34.946275134109987855 for the attacker EOA.
When combined with the earlier negative deltas for the setup and exploit txs, this yields the net profit of +33.815349128109987855 BNB.
The primary loss is in WTO:
402,289,684,537,844,701,832,616,528 WTO.0x00c5a772... and far exceeds any legitimate earnings from the attacker’s LP deposit.From the exploit balance diff and traces:
0x3200Be83... sends WTO to:
0x0557f67b....0x114FAA79... and 0xa6184d66....0x6F9070D4....+33.815349128109987855 BNB, computed as the net native balance delta of the EOA across the three attacker-crafted transactions.0x11c1ef2c..., 0x00c5a772..., and 0xf33f40ee....The analysis is supported by the following key artifacts:
0x6BBeF6DF..., including the reward calculation functions and embedded TokenDistributor.balance_diff.json document the initialization of the COAR LP position via LPMine::partakeAddLp and the associated AddLP event.balance_diff.json show the flash loan, repeated LPMine::extractReward(1) calls, TokenDistributor::claimToken executions, and the full WTO/USDT redistribution.balance_diff.json reconstruct the UniversalRouter call tree, USDT transferFrom operations from the EOA, WBNB unwrap, and the final BNB credit that yields the +34.946275134109987855 BNB delta for the EOA.Taken together, these artifacts deterministically support the conclusion that a protocol bug in LPMine’s reward accounting, when combined with flash-loan-based reserve manipulation, allowed an unprivileged attacker to over-claim WTO from RewardPool and realize a substantial BNB profit.