Calculated from recorded token losses using historical USD prices at the incident time.
0x70f367b9420ac2654a5223cc311c7f9c361736a39fd4e7dff9ed1b85bab7ad540x633fa755a83b015cccdc451f82c57ea0bd32b4b4BSC0xa386f30853a7eb7e6a25ec8389337a5c6973421dBSCThe seed transaction on BSC block 16008281 exploited Paraluni's ParaProxy at 0x633fa755a83b015cccdc451f82c57ea0bd32b4b4, which delegates to MasterChef implementation 0xa386f30853a7eb7e6a25ec8389337a5c6973421d. The attacker used depositByAddLiquidity(18, ...) to obtain pool-18 LP credit that did not come from the add-liquidity action itself. Instead, an attacker-controlled token reentered the victim during the router call and injected unrelated pool-18 LP into the proxy just before the victim measured its post-call balance. MasterChef then credited that unrelated balance increase to the attacker, enabling a duplicate withdrawal path that was burned into USDT and BUSD.
The root cause is an accounting bug in addLiquidityInternal: the contract snapshots IERC20(pool.lpToken).balanceOf(address(this)), performs an external paraRouter.addLiquidity on caller-chosen tokens, then sets credited liquidity to newBalance - oldBalance. That logic neither binds the router token pair to pool.lpToken nor defends the measured LP balance against reentrant mutation.
Pool 18 is configured to use LP token 0x3fd4fbd7a83062942b6589a2e9e2436dd8e134d4, the USDT/BUSD pair that ultimately bore the loss. ParaProxy is a delegatecall proxy, so both token balances and MasterChef storage live in the proxy state at . The vulnerable entrypoint, , is public and accepts arbitrary caller-supplied token addresses and token amounts.
userInfo0x633f...depositByAddLiquidityThat design matters because the function chooses two different asset identities at once. The pool determines the credited LP token through poolInfo[_pid].lpToken, but the router leg is driven by _tokens supplied by the caller. Once those identities diverge, any externally induced change to the proxy's balance of the pool LP token can be mistaken for freshly minted router liquidity.
The attacker prepared three contracts before the seed transaction. Creation metadata ties EOA 0x94bc1d555e63eea23fe7fdbf937ef3f9ac5fcf8f to harness 0x4770b5cb9d51ecb7ad5b14f0d4f2cee8e5563645, helper/token 0xca2ca459ec6e4f58ad88aeb7285d2e41747b9134, and auxiliary token 0xbc5db89ce5ab8035a71c6cd1cd0f0721ad28b508. The helper contract both behaves like an ERC20 in the attacker-controlled pair and holds real pool-18 LP that can be deposited back into the victim during reentrancy.
The vulnerable path is depositByAddLiquidity -> depositByAddLiquidityInternal -> addLiquidityInternal. In verified MasterChef code, the contract approves the router for caller-chosen _tokens, records oldBalance for _lpAddress = address(pool.lpToken), calls paraRouter.addLiquidity, then requires newBalance > oldBalance and overwrites vars.liquidity with vars.newBalance.sub(vars.oldBalance).
That implementation violates the intended invariant: the amount credited to pool pid should equal only the LP token minted by the immediately preceding add-liquidity action for that same pool. Instead, the code accepts arbitrary router tokens and trusts an externally mutable token balance as its accounting source of truth. Because the router call performs external token transfers, an attacker-controlled token can reenter before newBalance is read. If the attacker increases the proxy's balance of pool.lpToken during that window, the victim interprets the unrelated LP transfer as fresh liquidity and records it in userInfo.
The seed trace shows exactly that sequence. Helper 0xca2ca... is asked to transferFrom the proxy into the attacker pair and, during that callback, approves pool-18 LP to the proxy and calls ParaProxy.deposit(18, 155935695765009852802486). After control returns, the router mints only 1000000000000000000 LP of the attacker pair 0xf0da..., yet the victim later emits Deposit(user: 0x4770..., pid: 18, amount: 155935695765009852802486). The credited amount therefore tracks the helper's injected victim LP, not the router's actual output.
The exploit starts from a public, permissionless call to depositByAddLiquidity(18, _tokens, _amounts). The function transfers in attacker-chosen tokens and forwards them to depositByAddLiquidityInternal. Because pool 18 has no ticket gating, the code immediately calls:
uint liquidity = addLiquidityInternal(address(pool.lpToken), _user, _tokens, _amounts);
_deposit(_pid, liquidity, _user);
Inside addLiquidityInternal, the bug is visible in the balance-delta pattern:
vars.oldBalance = IERC20(_lpAddress).balanceOf(address(this));
(vars.amountA, vars.amountB, vars.liquidity) =
paraRouter.addLiquidity(_tokens[0], _tokens[1], _amounts[0], _amounts[1], 1, 1, address(this), block.timestamp + 600);
vars.newBalance = IERC20(_lpAddress).balanceOf(address(this));
require(vars.newBalance > vars.oldBalance, "B:E");
vars.liquidity = vars.newBalance.sub(vars.oldBalance);
The breakpoint is the final assignment to vars.liquidity. _lpAddress is the pool-18 LP token, but _tokens are arbitrary caller inputs. The router call performs token transfers against attacker-controlled contracts before newBalance is sampled, so the measured pool-18 LP balance can be changed for reasons unrelated to router minting.
The seed trace demonstrates the reentrant mutation. During 0xca2ca...::transferFrom(ParaProxy, attackerPair, 1e18), the helper reads its own balance of pool-18 LP, approves ParaProxy, and reenters with ParaProxy.deposit(18, 155935695765009852802486). The nested deposit transfers exactly that many units of pool-18 LP into the proxy and emits a real deposit for helper 0xca2ca.... Only after the nested call returns does the router mint 1e18 LP of the attacker-controlled pair 0xf0da....
When addLiquidityInternal resumes, IERC20(pool18Lp).balanceOf(ParaProxy) has risen by the helper-injected amount, so MasterChef treats the helper's deposit as router-created liquidity. The subsequent Deposit(user: 0x4770..., pid: 18, amount: 155935695765009852802486) records forged credit for the attacker harness. That is the exact invariant break: one real LP deposit now backs two separate claims in MasterChef accounting.
The final realization path confirms the duplicated credit. Helper 0xca2ca... first withdraws the LP it truly deposited. The harness 0x4770... then withdraws the forged credit again, receiving 155156017286184803538474 units of the same pool-18 LP after fees. The harness removes liquidity through router 0x48Bb5f07e78f32Ac7039366533D620C72c389797 and forwards 154942497864342441726890 USDT plus 155165183709915785547955 BUSD back to the attacker EOA.
The attacker lifecycle has three on-chain stages. First, EOA 0x94bc... deployed the auxiliary token, the helper/token with the reentrancy hook, and the exploit harness in transactions 0xc54ebd32..., 0xfcea2763..., and 0xb83f9044.... Those deployments prepared the attacker-controlled token pair and the helper that already held victim LP before the seed transaction.
Second, the seed transaction 0x70f367b9420ac2654a5223cc311c7f9c361736a39fd4e7dff9ed1b85bab7ad54 invoked the public add-liquidity entrypoint. The attacker pair mint was intentionally small; the trace shows the router minted only 1e18 LP for pair 0xf0da.... The important action happened inside the helper callback, where the helper deposited 155935695765009852802486 units of the real pool-18 LP into the proxy while the victim was still between its two balanceOf reads.
Third, the attacker realized profit by exercising both claims separately. Helper 0xca2ca... withdrew the LP it had genuinely deposited, proving the original stake remained available to its owner. The harness then withdrew the forged credit, burned the duplicated USDT/BUSD LP, and sent the resulting stablecoins to EOA 0x94bc.... The balance diff shows the attacker EOA ended the transaction with a stablecoin gain while only paying BNB gas.
The measurable loss is borne by the victim pool-18 LP token 0x3fd4..., which lost underlying reserves during the harness's removeLiquidity call. The on-chain balance diff shows a decrease of 155414869127000112047085 units of USDT and 155638233872136192808551 units of BUSD from the pool contract, both with 18 decimals on BSC.
The attacker EOA correspondingly gained 154942497864342441726890 USDT and 155165183709915785547955 BUSD, while paying 319349518000000000 wei in BNB gas. The exploit is therefore an ACT profit-taking incident: the adversary used only public entrypoints, attacker-deployed contracts, and canonical on-chain state to convert duplicated LP credit into stablecoin profit.
0x70f367b9420ac2654a5223cc311c7f9c361736a39fd4e7dff9ed1b85bab7ad54 on BSC block 16008281.0x633fa755a83b015cccdc451f82c57ea0bd32b4b4; MasterChef implementation: 0xa386f30853a7eb7e6a25ec8389337a5c6973421d.0x3fd4fbd7a83062942b6589a2e9e2436dd8e134d4 (USDT/BUSD pool-18 LP).artifacts/collector/iter_1/contract/56/0xa386f30853a7eb7e6a25ec8389337a5c6973421d/src/Contract.sol.artifacts/collector/seed/56/0x70f367b9420ac2654a5223cc311c7f9c361736a39fd4e7dff9ed1b85bab7ad54/trace.cast.log.artifacts/collector/seed/56/0x70f367b9420ac2654a5223cc311c7f9c361736a39fd4e7dff9ed1b85bab7ad54/balance_diff.json.artifacts/collector/iter_1/contract/56/creation_batch_1.json.