-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
8,628 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}, | ||
}, | ||
}, | ||
}; |
Oops, something went wrong.