Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BAL Hookathon - ArbitrageXV3 #88

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,10 @@ node_modules
.vscode
.idea
.vercel
.env
.env
cache
artifacts
env
.env.local
.env.development.local
.env.test.local
201 changes: 201 additions & 0 deletions contracts/FlashLoanArbitrage.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

interface IBalancerVault {
function flashLoan(
address recipient,
address[] memory tokens,
uint256[] memory amounts,
bytes memory userData
) external;

function swap(
SingleSwap memory singleSwap,
FundManagement memory funds,
uint256 limit,
uint256 deadline
) external returns (uint256);
}

interface IUniswapV2Router {
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external returns (uint[] memory amounts);
}

struct SingleSwap {
bytes32 poolId;
SwapKind kind;
address assetIn;
address assetOut;
uint256 amount;
bytes userData;
}

struct FundManagement {
address sender;
bool fromInternalBalance;
address payable recipient;
bool toInternalBalance;
}

enum SwapKind { GIVEN_IN, GIVEN_OUT }

contract BalancerV3ArbitrageBot is Ownable, ReentrancyGuard {
using SafeERC20 for IERC20;

IBalancerVault public balancerVault;
IUniswapV2Router public uniswapRouter;

event ArbitrageExecuted(address indexed token0, address indexed token1, uint256 profit);
event ArbitrageError(string reason);

constructor(address _balancerVault, address _uniswapRouter) {
balancerVault = IBalancerVault(_balancerVault);
uniswapRouter = IUniswapV2Router(_uniswapRouter);
}

event DebugLog(string message);
event DebugLogUint(string message, uint256 value);

function executeArbitrage(
address token0,
address token1,
uint256 flashLoanAmount,
uint256 minProfit,
bytes32 balancerPoolId
) external onlyOwner nonReentrant {
emit DebugLog("Executing arbitrage");
address[] memory tokens = new address[](1);
tokens[0] = token0;

uint256[] memory amounts = new uint256[](1);
amounts[0] = flashLoanAmount;

bytes memory userData = abi.encode(token1, minProfit, balancerPoolId);

try balancerVault.flashLoan(address(this), tokens, amounts, userData) {
emit DebugLog("Flash loan successful");
} catch Error(string memory reason) {
emit DebugLog(string(abi.encodePacked("Flash loan failed: ", reason)));
emit ArbitrageError(string(abi.encodePacked("Flash loan failed: ", reason)));
}
}

function receiveFlashLoan(
address[] memory tokens,
uint256[] memory amounts,
uint256[] memory feeAmounts,
bytes memory userData
) external {
emit DebugLog("Received flash loan");
require(msg.sender == address(balancerVault), "Unauthorized");

(address token1, uint256 minProfit, bytes32 balancerPoolId) = abi.decode(userData, (address, uint256, bytes32));

uint256 flashLoanAmount = amounts[0];
address token0 = tokens[0];

try this.performArbitrage(token0, token1, flashLoanAmount, feeAmounts[0], minProfit, balancerPoolId) {
emit DebugLog("Arbitrage successful");
} catch Error(string memory reason) {
emit DebugLog(string(abi.encodePacked("Arbitrage failed: ", reason)));
emit ArbitrageError(string(abi.encodePacked("Arbitrage failed: ", reason)));
// Ensure we repay the flash loan even if arbitrage fails
IERC20(token0).safeTransfer(address(balancerVault), flashLoanAmount + feeAmounts[0]);
}
}

function performArbitrage(
address token0,
address token1,
uint256 flashLoanAmount,
uint256 flashLoanFee,
uint256 minProfit,
bytes32 balancerPoolId
) external {
emit DebugLog("Performing arbitrage");
require(msg.sender == address(this), "Only internal call");

// Step 1: Swap token0 for token1 on Uniswap
uint256 token1Amount = swapOnUniswap(token0, token1, flashLoanAmount);
emit DebugLogUint("Uniswap swap result", token1Amount);

// Step 2: Swap token1 back to token0 on Balancer
uint256 token0Received = swapOnBalancer(token1, token0, token1Amount, balancerPoolId);
emit DebugLogUint("Balancer swap result", token0Received);

// Step 3: Calculate profit and repay flash loan
uint256 flashLoanRepayment = flashLoanAmount + flashLoanFee;
emit DebugLogUint("Flash loan repayment", flashLoanRepayment);
require(token0Received > flashLoanRepayment, "Arbitrage not profitable");

uint256 profit = token0Received - flashLoanRepayment;
emit DebugLogUint("Calculated profit", profit);
require(profit >= minProfit, "Profit below minimum threshold");

// Repay flash loan
IERC20(token0).safeTransfer(address(balancerVault), flashLoanRepayment);

// Transfer profit to contract owner
IERC20(token0).safeTransfer(owner(), profit);

emit ArbitrageExecuted(token0, token1, profit);
}


function swapOnUniswap(address tokenIn, address tokenOut, uint256 amountIn) internal returns (uint256) {
IERC20(tokenIn).safeApprove(address(uniswapRouter), amountIn);

address[] memory path = new address[](2);
path[0] = tokenIn;
path[1] = tokenOut;

uint[] memory amounts = uniswapRouter.swapExactTokensForTokens(
amountIn,
0, // We don't set a minimum as we'll check profitability later
path,
address(this),
block.timestamp
);

return amounts[1]; // Return the amount of tokenOut received
}

function swapOnBalancer(address tokenIn, address tokenOut, uint256 amountIn, bytes32 poolId) internal returns (uint256) {
IERC20(tokenIn).safeApprove(address(balancerVault), amountIn);

SingleSwap memory swap = SingleSwap({
poolId: poolId,
kind: SwapKind.GIVEN_IN,
assetIn: tokenIn,
assetOut: tokenOut,
amount: amountIn,
userData: ""
});

FundManagement memory funds = FundManagement({
sender: address(this),
fromInternalBalance: false,
recipient: payable(address(this)),
toInternalBalance: false
});

return balancerVault.swap(swap, funds, 0, block.timestamp);
}

// Function to rescue tokens stuck in the contract
function rescueTokens(address token) external onlyOwner {
uint256 balance = IERC20(token).balanceOf(address(this));
IERC20(token).safeTransfer(owner(), balance);
}
}
69 changes: 69 additions & 0 deletions contracts/mocks/MockBalancerVault.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract MockBalancerVault {
uint256 private mockAmountOut;

function setMockAmountOut(uint256 _amount) external {
mockAmountOut = _amount;
}

function flashLoan(
address recipient,
address[] memory tokens,
uint256[] memory amounts,
bytes memory userData
) external {
for (uint i = 0; i < tokens.length; i++) {
uint256 balanceBefore = IERC20(tokens[i]).balanceOf(address(this));
IERC20(tokens[i]).transfer(recipient, amounts[i]);

(bool success, ) = recipient.call(
abi.encodeWithSignature(
"receiveFlashLoan(address[],uint256[],uint256[],bytes)",
tokens,
amounts,
new uint256[](tokens.length), // Mock fee amounts
userData
)
);
require(success, "Flash loan callback failed");

uint256 balanceAfter = IERC20(tokens[i]).balanceOf(address(this));
require(balanceAfter >= balanceBefore, "Flash loan not repaid");
}
}

function swap(
SingleSwap memory singleSwap,
FundManagement memory funds,
uint256 limit,
uint256 deadline
) external returns (uint256) {
require(block.timestamp <= deadline, "Deadline exceeded");
require(mockAmountOut >= limit, "Limit not satisfied");

IERC20(singleSwap.assetIn).transferFrom(funds.sender, address(this), singleSwap.amount);
IERC20(singleSwap.assetOut).transfer(funds.recipient, mockAmountOut);

return mockAmountOut;
}
}

struct SingleSwap {
bytes32 poolId;
uint8 kind;
address assetIn;
address assetOut;
uint256 amount;
bytes userData;
}

struct FundManagement {
address sender;
bool fromInternalBalance;
address payable recipient;
bool toInternalBalance;
}
10 changes: 10 additions & 0 deletions contracts/mocks/MockToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MockToken is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
_mint(msg.sender, 1000000 * 10**decimals());
}
}
33 changes: 33 additions & 0 deletions contracts/mocks/MockUniswapRouter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract MockUniswapRouter {
uint256 private mockAmountOut;

function setMockAmountOut(uint256 _amount) external {
mockAmountOut = _amount;
}

function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external returns (uint[] memory amounts) {
require(path.length >= 2, "Invalid path");
require(block.timestamp <= deadline, "Deadline exceeded");
require(mockAmountOut >= amountOutMin, "Insufficient output amount");

IERC20(path[0]).transferFrom(msg.sender, address(this), amountIn);
IERC20(path[path.length - 1]).transfer(to, mockAmountOut);

amounts = new uint[](path.length);
amounts[0] = amountIn;
amounts[path.length - 1] = mockAmountOut;

return amounts;
}
}
17 changes: 17 additions & 0 deletions hardhat.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
require("dotenv").config();
// require("@nomicfoundation/hardhat-toolbox");
// require("@nomicfoundation/hardhat-chai-matchers");
require("@nomiclabs/hardhat-waffle");
require("@nomiclabs/hardhat-ethers");
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: "0.8.18",
networks: {
hardhat: {
forking: {
url: `https://sepolia.infura.io/v3/${process.env.INFURA_PROJECT_ID}`,
blockNumber: 3250000 // Use a fixed block number for better performance
},
},
},
};
Loading