Completed clean root directory structure: - Root now contains only: .git, .env, docs/, orig/ - Moved all remaining files and directories to orig/: - Config files (.claude, .dockerignore, .drone.yml, etc.) - All .env variants (except active .env) - Git config (.gitconfig, .github, .gitignore, etc.) - Tool configs (.golangci.yml, .revive.toml, etc.) - Documentation (*.md files, @prompts) - Build files (Dockerfiles, Makefile, go.mod, go.sum) - Docker compose files - All source directories (scripts, tests, tools, etc.) - Runtime directories (logs, monitoring, reports) - Dependency files (node_modules, lib, cache) - Special files (--delete) - Removed empty runtime directories (bin/, data/) V2 structure is now clean: - docs/planning/ - V2 planning documents - orig/ - Complete V1 codebase preserved - .env - Active environment config (not in git) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
189 lines
6.1 KiB
Solidity
189 lines
6.1 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.0;
|
|
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
|
|
interface IBalancerVault {
|
|
function flashLoan(
|
|
address recipient,
|
|
IERC20[] memory tokens,
|
|
uint256[] memory amounts,
|
|
bytes memory userData
|
|
) external;
|
|
}
|
|
|
|
interface IUniswapV2Router {
|
|
function swapExactTokensForTokens(
|
|
uint256 amountIn,
|
|
uint256 amountOutMin,
|
|
address[] calldata path,
|
|
address to,
|
|
uint256 deadline
|
|
) external returns (uint256[] memory amounts);
|
|
}
|
|
|
|
interface IUniswapV3Router {
|
|
struct ExactInputSingleParams {
|
|
address tokenIn;
|
|
address tokenOut;
|
|
uint24 fee;
|
|
address recipient;
|
|
uint256 deadline;
|
|
uint256 amountIn;
|
|
uint256 amountOutMinimum;
|
|
uint160 sqrtPriceLimitX96;
|
|
}
|
|
|
|
function exactInputSingle(ExactInputSingleParams calldata params)
|
|
external
|
|
payable
|
|
returns (uint256 amountOut);
|
|
}
|
|
|
|
/// @title Balancer Flash Loan Receiver for Arbitrage Execution
|
|
/// @notice Receives flash loans from Balancer and executes arbitrage paths
|
|
contract FlashLoanReceiver {
|
|
address public owner;
|
|
IBalancerVault public immutable vault;
|
|
|
|
struct ArbitragePath {
|
|
address[] tokens; // Token path
|
|
address[] exchanges; // DEX addresses
|
|
uint24[] fees; // Uniswap V3 fees (0 for V2)
|
|
bool[] isV3; // true if Uniswap V3, false if V2
|
|
uint256 minProfit; // Minimum profit required
|
|
}
|
|
|
|
event ArbitrageExecuted(
|
|
address indexed initiator,
|
|
uint256 profit,
|
|
uint8 pathLength
|
|
);
|
|
|
|
modifier onlyOwner() {
|
|
require(msg.sender == owner, "Not owner");
|
|
_;
|
|
}
|
|
|
|
constructor(address _vault) {
|
|
owner = msg.sender;
|
|
vault = IBalancerVault(_vault);
|
|
}
|
|
|
|
/// @notice Execute arbitrage using Balancer flash loan
|
|
/// @param tokens Token addresses to borrow
|
|
/// @param amounts Amounts to borrow
|
|
/// @param path Encoded arbitrage path
|
|
function executeArbitrage(
|
|
IERC20[] memory tokens,
|
|
uint256[] memory amounts,
|
|
bytes memory path
|
|
) external onlyOwner {
|
|
// Request flash loan from Balancer Vault
|
|
vault.flashLoan(address(this), tokens, amounts, path);
|
|
}
|
|
|
|
/// @notice Callback from Balancer Vault after flash loan
|
|
/// @param tokens Tokens received
|
|
/// @param amounts Amounts received
|
|
/// @param feeAmounts Fee amounts (always 0 for Balancer)
|
|
/// @param userData Encoded arbitrage path
|
|
function receiveFlashLoan(
|
|
IERC20[] memory tokens,
|
|
uint256[] memory amounts,
|
|
uint256[] memory feeAmounts,
|
|
bytes memory userData
|
|
) external {
|
|
require(msg.sender == address(vault), "Only vault can call");
|
|
|
|
// Decode arbitrage path
|
|
ArbitragePath memory path = abi.decode(userData, (ArbitragePath));
|
|
|
|
// Execute arbitrage swaps
|
|
uint256 currentAmount = amounts[0];
|
|
address currentToken = address(tokens[0]);
|
|
|
|
for (uint256 i = 0; i < path.tokens.length - 1; i++) {
|
|
address tokenIn = path.tokens[i];
|
|
address tokenOut = path.tokens[i + 1];
|
|
address exchange = path.exchanges[i];
|
|
|
|
// Approve token for exchange
|
|
IERC20(tokenIn).approve(exchange, currentAmount);
|
|
|
|
if (path.isV3[i]) {
|
|
// Uniswap V3 swap
|
|
IUniswapV3Router.ExactInputSingleParams memory params = IUniswapV3Router
|
|
.ExactInputSingleParams({
|
|
tokenIn: tokenIn,
|
|
tokenOut: tokenOut,
|
|
fee: path.fees[i],
|
|
recipient: address(this),
|
|
deadline: block.timestamp,
|
|
amountIn: currentAmount,
|
|
amountOutMinimum: 0, // Accept any amount (risky in production!)
|
|
sqrtPriceLimitX96: 0
|
|
});
|
|
|
|
currentAmount = IUniswapV3Router(exchange).exactInputSingle(params);
|
|
} else {
|
|
// Uniswap V2 swap
|
|
address[] memory swapPath = new address[](2);
|
|
swapPath[0] = tokenIn;
|
|
swapPath[1] = tokenOut;
|
|
|
|
uint256[] memory swapAmounts = IUniswapV2Router(exchange)
|
|
.swapExactTokensForTokens(
|
|
currentAmount,
|
|
0, // Accept any amount (risky in production!)
|
|
swapPath,
|
|
address(this),
|
|
block.timestamp
|
|
);
|
|
|
|
currentAmount = swapAmounts[swapAmounts.length - 1];
|
|
}
|
|
|
|
currentToken = tokenOut;
|
|
}
|
|
|
|
// Calculate profit
|
|
uint256 loanAmount = amounts[0];
|
|
uint256 totalRepayment = loanAmount + feeAmounts[0]; // feeAmounts is 0 for Balancer
|
|
require(currentAmount >= totalRepayment, "Insufficient profit");
|
|
|
|
uint256 profit = currentAmount - totalRepayment;
|
|
require(profit >= path.minProfit, "Profit below minimum");
|
|
|
|
// Repay flash loan (transfer tokens back to vault)
|
|
for (uint256 i = 0; i < tokens.length; i++) {
|
|
tokens[i].transfer(address(vault), amounts[i] + feeAmounts[i]);
|
|
}
|
|
|
|
// Emit event
|
|
emit ArbitrageExecuted(owner, profit, uint8(path.tokens.length));
|
|
|
|
// Keep profit in contract
|
|
}
|
|
|
|
/// @notice Withdraw profits
|
|
/// @param token Token to withdraw
|
|
/// @param amount Amount to withdraw
|
|
function withdrawProfit(address token, uint256 amount) external onlyOwner {
|
|
IERC20(token).transfer(owner, amount);
|
|
}
|
|
|
|
/// @notice Emergency withdraw
|
|
/// @param token Token address (or 0x0 for ETH)
|
|
function emergencyWithdraw(address token) external onlyOwner {
|
|
if (token == address(0)) {
|
|
payable(owner).transfer(address(this).balance);
|
|
} else {
|
|
uint256 balance = IERC20(token).balanceOf(address(this));
|
|
IERC20(token).transfer(owner, balance);
|
|
}
|
|
}
|
|
|
|
receive() external payable {}
|
|
}
|