blockchainethers.jshardhatreentrancy

Error: VM Exception while processing transaction: reverted with reason string 'BAL#400':REENTRANCY


I wrote following contract in order to execute cross flashloan between balancer and uniswapv3.The contract is not completed yet however I am stucked with REENTRANCY error which is probably caused by the section where I overwrited arbitrage logic on receiveFlashLoan hook.I put contract,test file and test result below:

Flashloan.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@balancer-labs/v2-interfaces/contracts/vault/IVault.sol";
import "@balancer-labs/v2-interfaces/contracts/vault/IFlashLoanRecipient.sol";
import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";

contract CrossFlashLoan is IFlashLoanRecipient {
    /*///////////////////////////////////////////////////////////////
                        FLASHLOAN CONSTANTS 
    //////////////////////////////////////////////////////////////*/
    IVault private constant vault = IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);//Balancer Vault address on Ethereum Mainnet
    /*///////////////////////////////////////////////////////////////
                        STATE VARIABLES
    //////////////////////////////////////////////////////////////*/
    /**
     * @dev flashloanTokens will be always borrowed tokens and only one token can be borrowed each time
     * Only 0th elementh of amount array will be used to determine the amount of loan that will be received
     * flashloanTokens array consists of only wrapped ether by default 
     * while strategyTokens consist of only bal by default 
     */
    ISwapRouter private  UNISWAP_ROUTER = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); // Uniswap Router address on Ethereum Mainnet
    address  BALANCER_POOL_ADDRESS =  0x9dDE0b1d39d0d2C6589Cde1BFeD3542d2a3C5b11; // Balancer Pool Address 
    IERC20[] flashLoanTokens=[IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2)];
    IERC20[] strategyTokens=[IERC20(IERC20(0xba100000625a3754423978a60c9317c58a424e3D))]; 
    uint256[]  amount;
    /*///////////////////////////////////////////////////////////////
                        CUSTOM ERRORS 
    //////////////////////////////////////////////////////////////*/
    error zeroAddressDetected();
    error onlyOneBorrowingTokenSupported();
    error callerIsNotContract();
    
    /*///////////////////////////////////////////////////////////////
                        EXTERNAL FUNCTIONS  
    //////////////////////////////////////////////////////////////*/
    /**
     * 
     * @param _amount The amount which will be borrowed by caller 
     */   
    function executeFlashLoan(
        uint256 _amount
    ) external{
      //check if executor is 0 address 
      if(msg.sender==address(0)){
        revert zeroAddressDetected();
      }
      uint256[] memory amounts = new uint256[](1);
      amounts[0] = _amount ;
      vault.flashLoan(this,flashLoanTokens,amounts,abi.encode(msg.sender));
    }
    // Function to be called by Balancer flash loan
    function receiveFlashLoan(
        IERC20[] memory tokens,
        uint256[] memory amounts,
        uint256[] memory feeAmounts,
        bytes memory userData
    ) external override{
        // Checks
        if(tokens.length!=1){
            revert onlyOneBorrowingTokenSupported();
        }
        if(address(tokens[0])==address(0)){
            revert zeroAddressDetected();
        }    
        // Arbitrage Logic
        // Ensure proper ERC-20 token handling
        address receipient;
        (receipient) = abi.decode(userData, (address));
        if(receipient == address(0)){
            revert zeroAddressDetected();
        }
    
        // Effects
        // 1. Swap borrowed WETH to BAL on Uniswap V3
        tokens[0].approve(address(UNISWAP_ROUTER), amounts[0]);
        ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
            tokenIn: address(tokens[0]),
            tokenOut: address(strategyTokens[0]),
            fee: 3000, // 0.3% fee
            recipient: address(this),
            deadline: block.timestamp,
            amountIn: amounts[0],
            amountOutMinimum: 0,
            sqrtPriceLimitX96: 0
        });
        UNISWAP_ROUTER.exactInputSingle(params);
    
        // Update state after external call
        amounts[0] = strategyTokens[0].balanceOf(address(this));
    
        // 2. Send BAL to Balancer Pool
        strategyTokens[0].approve(address(vault), amounts[0]);
    
        // 3. Swap BAL to WETH back on BalancerV2 then Repay the Loan
        vault.swap(
            IVault.SingleSwap(
                bytes32(abi.encodePacked(BALANCER_POOL_ADDRESS)),
                IVault.SwapKind.GIVEN_OUT,
                IAsset(address(strategyTokens[0])),
                IAsset(address(tokens[0])),
                amounts[0],
                ""
            ),
            IVault.FundManagement(
                address(this),
                true,
                payable(BALANCER_POOL_ADDRESS),
                true
            ),
            amounts[0] + feeAmounts[0],
            15 minutes
        );    
        // Interactions
        // Send funds to EOA (externally owned account)
        amounts[0] = flashLoanTokens[0].balanceOf(address(this));
        flashLoanTokens[0].approve(receipient,amounts[0]);
        tokens[0].transferFrom(address(this),receipient, amounts[0]);
    }
    
 
    

    /*///////////////////////////////////////////////////////////////
                        VIEW FUNCTIONS  
    //////////////////////////////////////////////////////////////*/

    function FlashloanToken() external view returns(address){
        return address(flashLoanTokens[0]);
    }
    function StrategyToken() external view returns(address){
        return address(strategyTokens[0]);
    }
}

Flashloan_test.ts

import { expect } from "chai";
import { ethers } from "hardhat"
describe("FlashLoan Test",()=>{
  let FlashLoan:any,flashloan:any;
  before("Deploy Contract",async()=>{
    const [deployer] = await ethers.getSigners();
    FlashLoan = await ethers.getContractFactory("CrossFlashLoan",deployer);
    flashloan = await FlashLoan.deploy();
  })
  it("Default flashloan token is wrapped ether",async()=>{
    let [owner]= await ethers.getSigners()
    expect(await flashloan.connect(owner).FlashloanToken()).is.eq('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2');
  })
  it("Default strategy Token is balancer",async()=>{
    let [owner]= await ethers.getSigners()
    expect(await flashloan.connect(owner).StrategyToken()).is.eq('0xba100000625a3754423978a60c9317c58a424e3D');
  })
  it("Execute flashloan",async()=>{
    let [owner]= await ethers.getSigners()
    expect(await flashloan.connect(owner).executeFlashLoan(ethers.parseEther('1')));
  })
})

Test Results FlashLoan Test

   ` ✔ Default flashloan token is wrapped ether
    ✔ Default strategy Token is balancer
    1) Execute flashloan


  2 passing (5s)
  1 failing

  1) FlashLoan Test
       Execute flashloan:
     Error: VM Exception while processing transaction: reverted with reason string 'BAL#400'
    at <UnrecognizedContract>.<unknown> (0xba12222222228d8ba445958a75a0704d566bf2c8)
    at CrossFlashLoan.receiveFlashLoan (contracts/Flashloan.sol:99)
    at <UnrecognizedContract>.<unknown> (0xba12222222228d8ba445958a75a0704d566bf2c8)
    at CrossFlashLoan.executeFlashLoan (contracts/Flashloan.sol:53)
    at async HardhatNode._mineBlockWithPendingTxs (node_modules\hardhat\src\internal\hardhat-network\provider\node.ts:1866:23)
    at async HardhatNode.mineBlock (node_modules\hardhat\src\internal\hardhat-network\provider\node.ts:524:16)
    at async EthModule._sendTransactionAndReturnHash (node_modules\hardhat\src\internal\hardhat-network\provider\modules\eth.ts:1482:18)   
    at async HardhatNetworkProvider.request (node_modules\hardhat\src\internal\hardhat-network\provider\provider.ts:124:18)
    at async HardhatEthersSigner.sendTransaction (node_modules\@nomicfoundation\hardhat-ethers\src\signers.ts:125:18)
    at async send (node_modules\ethers\src.ts\contract\contract.ts:313:20)
    at async Proxy.executeFlashLoan (node_modules\ethers\src.ts\contract\contract.ts:352:16)

I reviewed all the checks in both receiveFlashLoan and Execute flashloan functions. And read the balancer documentation several times. Additionaly I checked following url which has definition of flashLoan function : https://github.com/balancer/balancer-v2-monorepo/blob/master/pkg/vault/contracts/FlashLoans.sol however I could not fix the bug in my code. Mycode calls multiple times flashloan function but I don't know what is the reason causes it. I want to be sure about the reason of this bug and find a way to fix it.


Solution

  • I tried using uniswap and pancakeswap, changing approve to safeApprove e.t.c and convert code to this:

    pragma solidity ^0.8.0;
    
    import "@balancer-labs/v2-interfaces/contracts/vault/IVault.sol";
    import "@balancer-labs/v2-interfaces/contracts/vault/IFlashLoanRecipient.sol";
    import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
    import "@balancer-labs/v2-solidity-utils/contracts/openzeppelin/SafeERC20.sol";
    import "hardhat/console.sol";
    
    contract CrossFlashLoan is IFlashLoanRecipient{
        using SafeERC20 for IERC20;
        /*///////////////////////////////////////////////////////////////
                            FLASHLOAN CONSTANTS 
        //////////////////////////////////////////////////////////////*/
        IVault private immutable vault = IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);//Balancer Vault address on Ethereum Mainnet
        /*///////////////////////////////////////////////////////////////
                            STATE VARIABLES
        //////////////////////////////////////////////////////////////*/
        /**
         * @dev flashloanTokens will be always borrowed tokens and only one token can be borrowed each time
         * Only 0th elementh of amount array will be used to determine the amount of loan that will be received
         * flashloanTokens array consists of only wrapped ether by default 
         * while strategyTokens consist of only bal by default 
         */
        ISwapRouter private PANCAKESWAP_ROUTER=ISwapRouter(0x1b81D678ffb9C0263b24A97847620C99d213eB14);
        ISwapRouter private  UNISWAP_ROUTER = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); // Uniswap Router address on Ethereum Mainnet
        address  BALANCER_POOL_ADDRESS =  0x9dDE0b1d39d0d2C6589Cde1BFeD3542d2a3C5b11; // Balancer Pool Address 
        IERC20[] flashLoanTokens=[IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2)];
        IERC20[] strategyTokens=[IERC20(IERC20(0xba100000625a3754423978a60c9317c58a424e3D))]; 
        uint256[]  amount;
        address caller;
        /*///////////////////////////////////////////////////////////////
                            CUSTOM ERRORS 
        //////////////////////////////////////////////////////////////*/
        error zeroAddressDetected();
        error onlyOneBorrowingTokenSupported();
        error callerIsNotContract();
        
        /*///////////////////////////////////////////////////////////////
                            EXTERNAL FUNCTIONS  
        //////////////////////////////////////////////////////////////*/
        /**
         * 
         * @param _amount The amount which will be borrowed by caller 
         */   
        function executeFlashLoan(
            uint256 _amount
        ) external{
          //check if executor is 0 address 
    
          if(msg.sender==address(0)){
            revert zeroAddressDetected();
          }
          uint256[] memory amounts = new uint256[](1);
          amounts[0] = _amount ;
          caller=msg.sender;
          vault.flashLoan(this,flashLoanTokens,amounts,abi.encode(amounts[0]));
        }
        // Function to be called by Balancer flash loan
        function receiveFlashLoan(
            IERC20[] memory tokens,
            uint256[] memory amounts,
            uint256[] memory feeAmounts,
            bytes memory userData
        ) external override {
            uint256 debt;
            (debt)=abi.decode(userData,(uint256));
            // Checks
            if(tokens.length!=1){
                revert onlyOneBorrowingTokenSupported();
            }
            if(address(tokens[0])==address(0)){
                revert zeroAddressDetected();
            }   
            console.log(amounts[0]); 
            if(msg.sender == address(0)){
                revert zeroAddressDetected();
            }
            // Arbitrage Logic
            // 1. Swap borrowed WETH to BAL on Uniswap V3
            uint256 totalDebt= debt+feeAmounts[0];
            uint256 amountOut;
            tokens[0].safeApprove(address(UNISWAP_ROUTER), amounts[0]);
    
            amountOut=UNISWAP_ROUTER.exactInputSingle( ISwapRouter.ExactInputSingleParams({
                tokenIn: address(tokens[0]),
                tokenOut: address(strategyTokens[0]),
                fee: 3000, // 0.3% fee
                recipient: address(this),
                deadline: block.timestamp+5 seconds,
                amountIn: amounts[0],
                amountOutMinimum: totalDebt,
                sqrtPriceLimitX96: 0
            }));
            amounts[0] = strategyTokens[0].balanceOf(address(this));
            // 2. Swap back BAL to WETH on PANCAKESWAP V3
            strategyTokens[0].safeApprove(address(PANCAKESWAP_ROUTER), amounts[0]);
            ISwapRouter.ExactInputSingleParams memory _params = ISwapRouter.ExactInputSingleParams({
                tokenIn: address(strategyTokens[0]),
                tokenOut: address(tokens[0]),
                fee: 3000, // 0.3% fee
                recipient: address(this),
                deadline: block.timestamp+5 seconds,
                amountIn: amounts[0],
                amountOutMinimum: totalDebt,
                sqrtPriceLimitX96: 0
            });
            amountOut=PANCAKESWAP_ROUTER.exactInputSingle(_params);
            // Interactions
            // Repay debt
            // Send profit to EOA (externally owned account)
            
            tokens[0].safeTransfer(address(vault),totalDebt);
            tokens[0].safeApprove(msg.sender,tokens[0].balanceOf(address(this)));
            tokens[0].safeTransferFrom(address(this),caller,tokens[0].balanceOf(address(this)));
    
        }
        /*///////////////////////////////////////////////////////////////
                            VIEW FUNCTIONS  
        //////////////////////////////////////////////////////////////*/
    
        function FlashloanToken() external view returns(address){
            return address(flashLoanTokens[0]);
        }
        function StrategyToken() external view returns(address){
            return address(strategyTokens[0]);
        }
    } 
    

    In a nutshell I tried all related and unrelated methods to improve my code and solve the error but I saw that the error was not related to any of these changes.I learned it when I found following github issue https://github.com/balancer/balancer-v2-monorepo/issues/2346 and it answered my question.The reentrancy error occurs because of calling vault.swap function and the answer was different than what I thought.I understood that I cannot use simply something like uniswap,pancakeswap e.t.c in order to solve it or cannot use another swap function of balancer neither.For example the code above will cause following problem Error: Transaction reverted: function returned an unexpected amount of data.Instead of these,I learnt that only two methods solve this issue.The first method is taking out a flashloan from balancer v2 and performing swaps using balancer v1, and second one is using another protocol for flashloan.In the end of my research I understood that the best option is using another protocol in order to take out a flashloan and perform swaps as second method suggested.