Calculated from recorded token losses using historical USD prices at the incident time.
0x557628123d137ea49564e4dccff5f5d1e508607e96dd20fe99a670519b679cb50xb8dc09eec82cab2e86c7edc8dd5882dd92d22411BSC0x556b9306565093c855aea9ae92a594704c2cd59eBSCOn BNB Chain block 30043574, transaction 0x557628123d137ea49564e4dccff5f5d1e508607e96dd20fe99a670519b679cb5 let an unprivileged adversary force StakedV3 pool 2 to unwind and recreate its shared Pancake V3 position at an attacker-controlled spot price. The attacker borrowed 12,000,001 BUSD through three public Pancake V3 flash loans, pushed the BUSD/USDT 0.01% pool at 0x4f3126d5de26413abdcf6948943fb9d0847d9818 out of the shared position's active range [-17, 17], called the public StakedV3.Invest entrypoint, then reversed the price move and repaid the flash liquidity.
The root cause is a public vault-wide rebalance path in StakedV3.Invest that trusts the instantaneous Pancake V3 spot price. Challenge(id) reads live slot0() and treats a one-sided concentrated-liquidity position as invalid, while wightReset(id) immediately recomputes target weights from a fresh manipulated quote. Because pool 2 is a shared strategy position rather than caller-isolated liquidity, the attack externalized the full rebalance loss onto StakedV3's vault and left the attacker with 31099528530032487542556 wei BUSD profit.
StakedV3 at 0xb8dc09eec82cab2e86c7edc8dd5882dd92d22411 manages shared Pancake V3 liquidity positions. For pool id 2, the protocol stores one shared inside ; it is not a per-user NFT. Historical RPC reads at block show:
tokenIdpools[2]30043573StakedV3.pools(2):
token0 = BUSD 0xe9e7cea3dedca5984780bafc599bd69add087d56
token1 = USDT 0x55d398326f99059ff775485246999027b3197955
pool = 0x4f3126d5de26413abdcf6948943fb9d0847d9818
farm = 0x556b9306565093c855aea9ae92a594704c2cd59e
fee = 100
point = 1000000
tokenId = 138703
MasterChefV3.userPositionInfos(138703):
liquidity = 22267725135386876207004445
tickLower = -17
tickUpper = 17
user = 0xb8dc09eec82cab2e86c7edc8dd5882dd92d22411
pid = 14
That structure matters because any public call that mutates pools[2] acts on the entire vault position. Concentrated-liquidity positions also become one-sided when the current spot moves outside the active tick range. Pancake V3 flash loans and swaps are permissionless, so a searcher can temporarily force that state and still revert the price within the same transaction.
This is an ATTACK-class incident: a public vault rebalance can be forced from a transient, manipulable AMM price. The vulnerability is not hidden in attacker-specific calldata or privileged access; it is in the StakedV3 strategy logic itself. Invest is callable by any address and operates directly on pools[id], which holds shared vault state. Challenge decides whether the current position is "valid" by decomposing the active NFT against the current Pancake V3 slot0() price and returns false when either side becomes zero. When that happens, Invest executes a full harvest, liquidity removal, reswap, NFT withdrawal, and weight reset before minting or appending liquidity again. wightReset then recalculates weights from _amountOut, which itself depends on the same manipulated price path. The resulting invariant break is that a vault-wide rebalance is triggered and parameterized by attacker-controlled spot data rather than a manipulation-resistant price source.
The decisive logic is visible in the verified StakedV3 source.
Origin: verified StakedV3 source on BscScan
function Invest(
uint id,
uint amount,
uint quoteAmount,
uint investType,
uint cycle,
uint deadline
) public payable nonReentrant {
...
if(isFarm[id]) {
(bool pass,PoolToken memory tokens) = Challenge(id);
if(!pass) {
_harvest(id);
_remove(id,tokens,deadline);
_reSwap(id,tokens);
_withdraw(id);
wightReset(id);
}
...
if(!pass) {
(quoteAmount,) = _amountOut(id,pools[id].token0,pools[id].token1,amount0,false);
}
Swap(id,pools[id].token0,pools[id].token1,amount0,quoteAmount,0);
if(pools[id].tokenId == 0) {
Mint(id,deadline);
} else {
Append(id,tokens,deadline);
}
}
}
Origin: verified StakedV3 source on BscScan
function wightReset(uint id) private {
(uint amountOut,) = _amountOut(id,pools[id].token0,pools[id].token1,10 ** 18,false);
pools[id].wight0 = 10 ** 18;
pools[id].wight1 = amountOut;
}
function Challenge(uint id) public view returns (bool result,PoolToken memory tokens) {
...
(sqrtPriceX96,,,,,,) = IUniswapV3Pool(pools[id].pool).slot0();
...
(amount0,amount1) = ICompute(compute).getAmountsForLiquidity(
sqrtPriceX96,
sqrtRatioAX96,
sqrtRatioBX96,
tokenPosition.liquidity
);
if(amount0 == 0 || amount1 == 0) {
result = false;
} else {
result = true;
}
}
These functions establish the explicit invariant and breakpoint:
if (!pass) branch in Invest, where a manipulated Challenge(id) result forces _harvest, _remove, _reSwap, _withdraw, and wightReset.MasterChefV3 confirms what the unwind does to the shared NFT.
Origin: verified MasterChefV3 source on BscScan
function withdraw(uint256 _tokenId, address _to) external nonReentrant returns (uint256 reward) {
if (_to == address(this) || _to == address(0)) revert WrongReceiver();
UserPositionInfo storage positionInfo = userPositionInfos[_tokenId];
if (positionInfo.user != msg.sender) revert NotOwner();
reward = harvestOperation(positionInfo, _tokenId, _to);
...
delete userPositionInfos[_tokenId];
...
nonfungiblePositionManager.safeTransferFrom(address(this), _to, _tokenId);
emit Withdraw(msg.sender, _to, pid, _tokenId);
}
Because userPositionInfos[138703].user was StakedV3 before the exploit, StakedV3 was authorized to remove the shared NFT once Invest entered that branch.
The seed transaction is a single-transaction ACT sequence.
0x3a10408fd7a2b2a43bd14a17c0d4568430b93132 called helper contract 0x18703a4fd7b3688607abf25424b6ab304def2512.0x22536030b9ce783b6ddfb9a39ac7f439f568e5e6, 0x85faac652b707fdf6907ef726751087f9e0b6687, and 0x369482c78bad380a036cab827fe677c1903d1523, totaling 12000001000000000000000000 wei BUSD.12000000000000000000000000 wei BUSD into the attacked BUSD/USDT pool and pushed the tick to 29796, far outside the original [-17, 17] range.StakedV3.Invest(2, 1000000000000000000, 2, 1, 7, 1689612419).Invest unwound shared tokenId 138703, withdrew it from MasterChefV3, recalculated weights from the manipulated price, minted tokenId 182823, and redeposited that new NFT back into MasterChefV3 for StakedV3.Origin: collector seed trace
emit Swap(... amount0: -11822760359241603066372096, amount1: 12000000000000000000000000, ... tick: 29796 ...)
0xB8dC09Eec82CaB2E86C7EdC8DD5882dd92d22411::Invest(2, 1000000000000000000, 2, 1, 7, 1689612419)
0x556B9306565093C855AEA9AE92A594704c2Cd59e::harvest(138703, 0xB8dC09Eec82CaB2E86C7EdC8DD5882dd92d22411)
0x556B9306565093C855AEA9AE92A594704c2Cd59e::decreaseLiquidity((138703, 22267725135386876207004445, ...))
0x556B9306565093C855AEA9AE92A594704c2Cd59e::withdraw(138703, 0xB8dC09Eec82CaB2E86C7EdC8DD5882dd92d22411)
emit Transfer(... to: 0xB8dC09Eec82CaB2E86C7EdC8DD5882dd92d22411, param2: 138703)
emit Transfer(... to: 0xB8dC09Eec82CaB2E86C7EdC8DD5882dd92d22411, param2: 182823)
0x46A15B0b27311cedF172AB29E4f4766fbE7F4364::safeTransferFrom(0xB8dC09..., 0x556B930..., 182823)
emit Transfer(from: PancakeV3Pool: [0x4f3126d5...], to: 0x18703A4f..., value: 12034115342258351564409378)
emit Transfer(from: 0x18703A4f..., to: 0x3A10408f..., value: 31099528530032487542556)
Post-state RPC reads at block 30043574 are consistent with the trace: StakedV3.pools(2).tokenId == 182823, MasterChefV3.userPositionInfos(182823).user == StakedV3, MasterChefV3.userPositionInfos(138703) is cleared, and NonfungiblePositionManager.ownerOf(138703) == StakedV3.
The measurable loss is the attacker's positive terminal BUSD extraction from the forced rebalance:
310995285300324875425561831,099.528530032487542556 BUSDOrigin: collector balance diff
{
"native_balance_deltas": [
{
"address": "0x3a10408fd7a2b2a43bd14a17c0d4568430b93132",
"delta_wei": "-25548873000000000"
}
],
"erc20_balance_deltas": [
{
"token": "0xe9e7cea3dedca5984780bafc599bd69add087d56",
"holder": "0x3a10408fd7a2b2a43bd14a17c0d4568430b93132",
"delta": "31099528530032487542556"
}
]
}
The strategy also lost its original MasterChefV3 position 138703 and replaced it with newly minted tokenId 182823 priced from the manipulated market state. That is the concrete vault-side state transition that realized the loss.
0x557628123d137ea49564e4dccff5f5d1e508607e96dd20fe99a670519b679cb50xb8dc09eec82cab2e86c7edc8dd5882dd92d224110x556b9306565093c855aea9ae92a594704c2cd59e0x46a15b0b27311cedf172ab29e4f4766fbe7f43640x4f3126d5de26413abdcf6948943fb9d0847d9818/workspace/session/artifacts/collector/seed/56/0x557628123d137ea49564e4dccff5f5d1e508607e96dd20fe99a670519b679cb5/