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:
// 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]);
}
}
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')));
})
})
` ✔ 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.
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.