This is a lower bound: only assets with reliable historical USD prices are counted, so the actual loss may be higher.
0xb64ae25b0d836c25d115a9368319902c972a0215bd108ae17b1b9617dfb93af80x3de669c4f1f167a8afbc9993e4753b84b576426fBSC0xe4ae305ebe1abe663f261bc00534067c80ad677cBSCSpartan Protocol's WBNB/SPARTA Pool V1 was exploited in BSC transaction 0xb64ae25b0d836c25d115a9368319902c972a0215bd108ae17b1b9617dfb93af8 at block 7048833. The attacker used a public PancakeSwap flash swap to borrow WBNB, manipulated the Spartan pool price with repeated swaps, minted LP units at an inflated rate, then burned those same LP units for more SPARTA and WBNB than the deposit justified. The root cause is an accounting mismatch: liquidity minting uses Utils.calcLiquidityUnits, which overvalues manipulated asymmetric deposits, while liquidity removal uses Utils.calcLiquidityShare, which redeems a direct pro-rata claim on actual reserves.
Spartan Pool V1 tracks mutable pool reserves in baseAmount and tokenAmount, with SPARTA as BASE and WBNB as TOKEN for the victim pool. Liquidity addition and removal are both public. That matters because an unprivileged actor can first distort reserves via swaps and then immediately present those manipulated reserve values to the minting logic. The victim contract emits AddLiquidity, RemoveLiquidity, and Swapped events, which makes the exploit path directly observable in the collector trace.
The core victim entrypoints are:
function addLiquidityForMember(address member) public returns(uint liquidityUnits){
uint256 _actualInputBase = _getAddedBaseAmount();
uint256 _actualInputToken = _getAddedTokenAmount();
liquidityUnits = _DAO().UTILS().calcLiquidityUnits(
_actualInputBase, baseAmount, _actualInputToken, tokenAmount, totalSupply
);
_incrementPoolBalances(_actualInputBase, _actualInputToken);
_mint(member, liquidityUnits);
}
function removeLiquidityForMember(address member) public returns (uint outputBase, uint outputToken) {
uint units = balanceOf(address(this));
outputBase = _DAO().UTILS().calcLiquidityShare(units, BASE, address(this), member);
outputToken = _DAO().UTILS().calcLiquidityShare(units, TOKEN, address(this), member);
_decrementPoolBalances(outputBase, outputToken);
_burn(address(this), units);
}
The vulnerability is a pool-share inflation bug in liquidity accounting. calcLiquidityUnits computes newly minted LP with an arithmetic expression based on the current reserve ratio, plus a slip adjustment, rather than a conservative bound tied to actual contributed value. Because the attacker can skew reserves just before minting, this formula can issue LP units that represent an outsized claim on the pool. The burn path then ignores the manipulated mint context and simply redeems a proportional share of current balances through calcLiquidityShare. As a result, an add-then-immediate-burn round trip can return more assets than were added. This violates the expected invariant that newly minted LP units must not let a user extract existing pool reserves absent explicit rewards. The exploit is therefore a deterministic accounting flaw, not a privileged-access issue.
The code-level breakpoint sits in the Utils liquidity mint formula:
function calcLiquidityUnits(uint b, uint B, uint t, uint T, uint P) public view returns (uint units){
if(P == 0){
return b;
} else {
uint slipAdjustment = getSlipAdustment(b, B, t, T);
uint part1 = t.mul(B);
uint part2 = T.mul(b);
uint part3 = T.mul(B).mul(2);
uint _units = (P.mul(part1.add(part2))).div(part3);
return _units.mul(slipAdjustment).div(one);
}
}
Here, b and t are the newly added SPARTA and WBNB amounts, while B and T are the live pool reserves at the manipulated moment. The attacker first used public WBNB-for-SPARTA swaps to distort B:T. addLiquidityForMember then consumed those distorted reserves when calling calcLiquidityUnits. That produced LP issuance that was too large for the actual economic value contributed.
The collector trace and the root-cause data show the mismatch concretely on the first cycle. The attacker deposited 2536613206101067206978364 SPARTA and 11853332738790033677468 WBNB, received 933350959891510782264802 LP units, and immediately redeemed those same units for 2538199153113548855179986 SPARTA and 20694059368262615067224 WBNB. The round trip therefore returned more WBNB than the cycle deposited, even before later loops compounded the drain.
That outcome is exactly what the burn logic enables. calcLiquidityShare is purely proportional:
function calcLiquidityShare(uint units, address token, address pool, address member) public view returns (uint share){
uint amount = iBEP20(token).balanceOf(pool);
uint totalSupply = iBEP20(pool).totalSupply();
return(amount.mul(units)).div(totalSupply);
}
Once over-minted LP exists, redeeming it immediately converts the accounting inflation into real reserve withdrawals. The exploit does not require governance rights, private keys, or hidden state. It only requires public flash liquidity, public swaps, and public pool liquidity functions.
The attacker cluster consists of EOA 0x3b6e77722e2bbe97c1cfa337b42c0939aeb83671 and helper contract 0x288315639c1145f523af6d7a5e4ccf8238cd6a51. Pre-incident transactions show the EOA deployed the helper and approved WBNB to it. In the exploit transaction, the helper contract initiated a PancakeSwap flash swap from pair 0x0ed7e52944161450477ee417de9cd3a859b14fd0 and received 100000 WBNB.
From there, the helper repeatedly performed the same loop:
calcLiquidityUnits to over-mint LP.removeLiquidityForMember to burn it immediately.The trace contains the corresponding victim-observed events, including repeated Swapped, AddLiquidity, and RemoveLiquidity emissions. At the end of the transaction, the helper transferred 129488952978095874286361 WBNB and 3232641006646789854925049 SPARTA to the attacker EOA, and the EOA repaid 100260000000000000000000 WBNB to the flash-loan pair.
The victim Spartan pool lost both reserve assets. The collector balance diff shows the pool's SPARTA balance decreased by 3232641006646789854925049 units, and the attacker EOA received exactly that SPARTA amount. The validated incident summary also attributes 29228952978095874286361 wei of WBNB net profit to the attacker after flash-loan repayment. Existing LPs were diluted because the exploit converted accounting over-minting into withdrawals of live reserves.
0xb64ae25b0d836c25d115a9368319902c972a0215bd108ae17b1b9617dfb93af80x3de669c4f1f167a8afbc9993e4753b84b576426f0xe4ae305ebe1abe663f261bc00534067c80ad677c0x0ed7e52944161450477ee417de9cd3a859b14fd0Pool.addLiquidityForMember, Pool.removeLiquidityForMemberUtils.calcLiquidityUnits, Utils.calcLiquidityShare