Skip to content
Merged
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
57 changes: 57 additions & 0 deletions broadcast/DeployVaultFactory.s.sol/84532/run-1739011371.json

Large diffs are not rendered by default.

42 changes: 21 additions & 21 deletions broadcast/DeployVaultFactory.s.sol/84532/run-latest.json

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion script/DeployVaultFactory.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ contract DeployVaultFactory is Script {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);

// Deploy VaultFactory with the owner set to msg.sender
VaultFactory vaultFactory = new VaultFactory();

vm.stopBroadcast();
Expand Down
166 changes: 105 additions & 61 deletions src/Arbitrage.sol
Original file line number Diff line number Diff line change
@@ -1,37 +1,31 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.22;
pragma solidity ^0.8.22;

// Import necessary interfaces for Balancer Vault, Flash Loans, and Uniswap V3
import "@balancer/balancer-v2-monorepo/pkg/interfaces/contracts/vault/IVault.sol";
import "@balancer/balancer-v2-monorepo/pkg/interfaces/contracts/vault/IFlashLoanRecipient.sol";
import {ISwapRouter} from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
import {IQuoterV2} from "@uniswap/v3-periphery/contracts/interfaces/IQuoterV2.sol";

/**
* @title Arbitrage
* @author CentoAI
* @author Cento-AI
* @notice Executes arbitrage opportunities between two Uniswap-like DEXs using Balancer V2 Flash Loans.
* @notice This contract is inherited by the Vault contract, and can be only used by the Vault contract.
* @dev This contract borrows a flash loan, performs two consecutive swaps (e.g., DEX A → DEX B),
* repays the loan, and sends profits to the contract owner.
* Note: The current implementation does not account for Balancer's flash loan fees (see critical warning).
* repays the loan, and sends profits to the contract owner.
* @dev Note: The current implementation does not account for Balancer's flash loan fees (see critical warning).
*/
contract Arbitrage is IFlashLoanRecipient {
/// @notice Balancer V2 Vault address (Ethereum mainnet)
contract ArbitrageContract is IFlashLoanRecipient {
/// @notice Balancer V2 Vault address (Base Sepolia)
IVault private constant vault = IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);

/// @notice Owner address to receive arbitrage profits
address public owner;

/**
* @dev Struct to encapsulate swap parameters for a single trade path.
* @param routerPath Array with two elements: [firstSwapRouter, secondSwapRouter]
* @param quoterPath Placeholder for future quoter integration (unused in current code)
* @param tokenPath Array with two elements: [tokenToBorrow, tokenToSwap]
* @param fee Pool fee tier for Uniswap V3 swaps (e.g., 3000 = 0.3%)
*/
struct Trade {
address[] routerPath;
address[] quoterPath;
address[] tokenPath;
uint24 fee;
}
Expand All @@ -45,39 +39,30 @@ contract Arbitrage is IFlashLoanRecipient {
*/
event TokensSwapped(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut);

/// @dev Sets the contract deployer as the owner
constructor() {
owner = msg.sender;
}
constructor() {}

/**
* @notice Initiates a flash loan for arbitrage execution
* @dev Can be called by any external address (profits go to owner)
* @dev Can only be called by the Vault contract
* @param _routerPath [firstSwapRouter, secondSwapRouter] - Uniswap-compatible router addresses
* @param _quoterPath Placeholder parameter (unused in current implementation)
* @param _tokenPath [tokenToBorrow, tokenToSwap] - Token addresses for the arbitrage path
* @param _fee Uniswap V3 pool fee tier for swaps
* @param _flashAmount Amount of tokenToBorrow to flash loan
*/
function executeTrade(
address[] memory _routerPath,
address[] memory _quoterPath,
address[] memory _tokenPath,
uint24 _fee,
uint256 _flashAmount
) external {
// Encode trade parameters for flash loan callback
bytes memory data =
abi.encode(Trade({routerPath: _routerPath, quoterPath: _quoterPath, tokenPath: _tokenPath, fee: _fee}));
function executeTrade(address[] memory _routerPath, address[] memory _tokenPath, uint24 _fee, uint256 _flashAmount)
internal
{
/// @dev Encode trade parameters for flash loan callback
bytes memory data = abi.encode(Trade({routerPath: _routerPath, tokenPath: _tokenPath, fee: _fee}));

// Configure flash loan parameters
/// @dev Configure flash loan parameters
IERC20[] memory tokens = new IERC20[](1);
tokens[0] = IERC20(_tokenPath[0]); // Token to borrow

uint256[] memory amounts = new uint256[](1);
amounts[0] = _flashAmount; // Loan amount

// Trigger Balancer flash loan
/// @dev Trigger Balancer flash loan
vault.flashLoan(this, tokens, amounts, data);
}

Expand All @@ -97,39 +82,95 @@ contract Arbitrage is IFlashLoanRecipient {
) external override {
require(msg.sender == address(vault), "Unauthorized: Only Balancer Vault");

// Decode trade parameters from userData
/// @dev Decode trade parameters from userData
Trade memory trade = abi.decode(userData, (Trade));
uint256 flashAmount = amounts[0];

// First swap: Borrowed token → Intermediate token
/// @dev First swap: Borrowed token → Intermediate token
_swapOnV3(
trade.routerPath[0], // First DEX router (e.g., Uniswap)
trade.tokenPath[0], // Borrowed token address
flashAmount, // Use entire flash loan amount
trade.tokenPath[1], // Token to receive
0, // No minimum out (assuming optimistic arbitrage)
trade.fee // Pool fee tier
trade.routerPath[0],
/// @param routerPath[0] First DEX router (e.g., Uniswap)
trade.tokenPath[0],
/// @param tokenPath[0] Borrowed token address
flashAmount,
/// @param flashAmount Use entire flash loan amount
trade.tokenPath[1],
/// @param tokenPath[1] Token to receive
0,
/// @param 0 No minimum out (assuming optimistic arbitrage)
trade.fee
);
/// @param fee Pool fee tier

// Second swap: Intermediate token → Borrowed token
/// @dev Second swap: Intermediate token → Borrowed token
_swapOnV3(
trade.routerPath[1], // Second DEX router (e.g., Sushiswap)
trade.tokenPath[1], // Intermediate token
IERC20(trade.tokenPath[1]).balanceOf(address(this)), // Swap entire balance
trade.tokenPath[0], // Borrowed token (to repay loan)
flashAmount, // Minimum required to repay loan (slippage protection)
trade.fee // Pool fee tier
trade.routerPath[1],
/// @param routerPath[1] Second DEX router (e.g., Sushiswap)
trade.tokenPath[1],
/// @param tokenPath[1] Intermediate token
IERC20(trade.tokenPath[1]).balanceOf(address(this)),
/// @param IERC20(trade.tokenPath[1]).balanceOf(address(this)) Swap entire balance
trade.tokenPath[0],
/// @param trade.tokenPath[0] Borrowed token (to repay loan)
flashAmount,
/// @param flashAmount Minimum required to repay loan (slippage protection)
trade.fee
);
/// @param fee Pool fee tier

// Repay flash loan principal (WARNING: Missing fee repayment - see note)
IERC20(trade.tokenPath[0]).transfer(address(vault), flashAmount);

// Transfer remaining balance (profits) to owner
uint256 profit = IERC20(trade.tokenPath[0]).balanceOf(address(this));
IERC20(trade.tokenPath[0]).transfer(owner, profit);
/// @dev Repay flash loan principal (WARNING: Missing fee repayment - see note)
IERC20(trade.tokenPath[0]).transfer(address(this), flashAmount);
}

// ========== INTERNAL FUNCTIONS ========== //
/**
* @notice Executes a single exact-input swap arbitrage on Uniswap V3
* @dev This function is used to execute an arbitrage without a flash loan
* @dev Emits TokensSwapped event on success
* @param _routerPath [firstSwapRouter, secondSwapRouter] - Uniswap-compatible router addresses
* @param _tokenPath [tokenToBorrow, tokenToSwap] - Token addresses for the arbitrage path
* @param _fee Uniswap V3 pool fee tier for swaps
* @param _amount Amount of tokenToBorrow to swap
*/
function ArbitrageWithoutFlashLoan(
address[] memory _routerPath,
address[] memory _tokenPath,
uint24 _fee,
uint256 _amount
) internal {
Trade memory trade = Trade({routerPath: _routerPath, tokenPath: _tokenPath, fee: _fee});

/// @dev First swap: Borrowed token → Intermediate token
_swapOnV3(
trade.routerPath[0],
/// @param routerPath[0] First DEX router (e.g., Uniswap)
trade.tokenPath[0],
/// @param tokenPath[0] Borrowed token address
_amount,
/// @param _amount Use entire flash loan amount
trade.tokenPath[1],
/// @param trade.tokenPath[1] Token to receive
0,
/// @param 0 No minimum out (assuming optimistic arbitrage)
trade.fee
);
/// @param fee Pool fee tier

/// @dev Second swap: Intermediate token → Borrowed token
_swapOnV3(
trade.routerPath[1],
/// @param routerPath[1] Second DEX router (e.g., Sushiswap)
trade.tokenPath[1],
/// @param tokenPath[1] Intermediate token
IERC20(trade.tokenPath[1]).balanceOf(address(this)),
/// @param IERC20(trade.tokenPath[1]).balanceOf(address(this)) Swap entire balance
trade.tokenPath[0],
/// @param trade.tokenPath[0] Borrowed token (to repay loan)
_amount,
/// @param _amount Minimum required to repay loan (slippage protection)
trade.fee
);
/// @param fee Pool fee tier
}

/**
* @notice Executes a single exact-input swap on Uniswap V3
Expand All @@ -149,24 +190,27 @@ contract Arbitrage is IFlashLoanRecipient {
uint256 _amountOut,
uint24 _fee
) internal {
// Approve router to spend input tokens
/// @dev Approve router to spend input tokens
IERC20(_tokenIn).approve(_router, _amountIn);

// Configure single-hop swap parameters
/// @dev Configure single-hop swap parameters
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
tokenIn: _tokenIn,
tokenOut: _tokenOut,
fee: _fee,
recipient: address(this), // Send output tokens to this contract
deadline: block.timestamp, // Expire after current block
recipient: address(this),
/// @param recipient Send output tokens to this contract
deadline: block.timestamp,
/// @param deadline Expire after current block
amountIn: _amountIn,
amountOutMinimum: _amountOut, // Minimum output for successful swap
sqrtPriceLimitX96: 0 // No price limit (accept any slippage)
amountOutMinimum: _amountOut,
/// @param amountOutMinimum Minimum output for successful swap
sqrtPriceLimitX96: 0
});
/// @param sqrtPriceLimitX96 No price limit (accept any slippage)

// Execute swap on specified router
/// @dev Execute swap on specified router
ISwapRouter(_router).exactInputSingle(params);

emit TokensSwapped(_tokenIn, _tokenOut, _amountIn, _amountOut);
}
}
Loading
Loading