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>
315 lines
12 KiB
Solidity
315 lines
12 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.0;
|
|
|
|
import "forge-std/Test.sol";
|
|
import "../../contracts/balancer/FlashLoanReceiverSecure.sol";
|
|
|
|
/// @title Integration Tests with Real Arbitrum Mainnet Data
|
|
/// @notice Tests against actual deployed contracts on Arbitrum
|
|
/// @dev Requires forking from Arbitrum mainnet
|
|
contract FlashLoanReceiverSecureIntegrationTest is Test {
|
|
FlashLoanReceiverSecure public flashLoan;
|
|
|
|
// Real Arbitrum mainnet addresses
|
|
address constant BALANCER_VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8;
|
|
address constant WETH = 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1;
|
|
address constant USDC = 0xaf88d065e77c8cC2239327C5EDb3A432268e5831;
|
|
address constant USDT = 0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9;
|
|
address constant ARB = 0x912CE59144191C1204E64559FE8253a0e49E6548;
|
|
address constant DAI = 0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1;
|
|
|
|
// Real DEX addresses
|
|
address constant UNISWAP_V3_ROUTER = 0xE592427A0AEce92De3Edee1F18E0157C05861564;
|
|
address constant SUSHISWAP_ROUTER = 0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506;
|
|
|
|
// Whale addresses with large token balances
|
|
address constant WETH_WHALE = 0xF977814e90dA44bFA03b6295A0616a897441aceC; // Binance
|
|
address constant USDC_WHALE = 0x47c031236e19d024b42f8AE6780E44A573170703; // Bridge
|
|
address constant ARB_WHALE = 0xF3FC178157fb3c87548bAA86F9d24BA38E649B58; // Arbitrum Foundation
|
|
|
|
address public owner;
|
|
address public user;
|
|
|
|
function setUp() public {
|
|
// Fork Arbitrum mainnet (requires ARBITRUM_RPC_URL env var)
|
|
string memory rpcUrl = vm.envString("ARBITRUM_RPC_URL");
|
|
uint256 forkId = vm.createFork(rpcUrl);
|
|
vm.selectFork(forkId);
|
|
|
|
// Verify we're on Arbitrum
|
|
require(block.chainid == 42161, "Not on Arbitrum mainnet");
|
|
|
|
owner = address(this);
|
|
user = address(0x123);
|
|
|
|
// Deploy FlashLoanReceiverSecure with real Balancer Vault
|
|
flashLoan = new FlashLoanReceiverSecure(BALANCER_VAULT);
|
|
|
|
// Fund contract with ETH for gas
|
|
vm.deal(address(flashLoan), 10 ether);
|
|
}
|
|
|
|
// Allow test contract to receive ETH
|
|
receive() external payable {}
|
|
|
|
/// ============================================
|
|
/// REAL CONTRACT INTERACTION TESTS
|
|
/// ============================================
|
|
|
|
function testIntegration_RealBalancerVault() public view {
|
|
// Verify we're using the real Balancer Vault
|
|
assertEq(address(flashLoan.vault()), BALANCER_VAULT);
|
|
|
|
// Verify Balancer Vault has code (is deployed)
|
|
uint256 size;
|
|
address vaultAddr = BALANCER_VAULT;
|
|
assembly {
|
|
size := extcodesize(vaultAddr)
|
|
}
|
|
assertGt(size, 0, "Balancer Vault not deployed");
|
|
}
|
|
|
|
function testIntegration_RealTokenBalances() public {
|
|
// Check real token balances of whales
|
|
uint256 wethBalance = IERC20(WETH).balanceOf(WETH_WHALE);
|
|
uint256 usdcBalance = IERC20(USDC).balanceOf(USDC_WHALE);
|
|
uint256 arbBalance = IERC20(ARB).balanceOf(ARB_WHALE);
|
|
|
|
assertGt(wethBalance, 0, "WETH whale has no balance");
|
|
assertGt(usdcBalance, 0, "USDC whale has no balance");
|
|
assertGt(arbBalance, 0, "ARB whale has no balance");
|
|
|
|
emit log_named_decimal_uint("WETH Whale Balance", wethBalance, 18);
|
|
emit log_named_decimal_uint("USDC Whale Balance", usdcBalance, 6);
|
|
emit log_named_decimal_uint("ARB Whale Balance", arbBalance, 18);
|
|
}
|
|
|
|
function testIntegration_RealTokenMetadata() public {
|
|
// Verify real token properties
|
|
assertEq(IERC20Metadata(WETH).decimals(), 18, "WETH decimals");
|
|
assertEq(IERC20Metadata(USDC).decimals(), 6, "USDC decimals");
|
|
assertEq(IERC20Metadata(USDT).decimals(), 6, "USDT decimals");
|
|
assertEq(IERC20Metadata(ARB).decimals(), 18, "ARB decimals");
|
|
assertEq(IERC20Metadata(DAI).decimals(), 18, "DAI decimals");
|
|
|
|
assertEq(IERC20Metadata(WETH).symbol(), "WETH", "WETH symbol");
|
|
assertEq(IERC20Metadata(USDC).symbol(), "USDC", "USDC symbol");
|
|
assertEq(IERC20Metadata(ARB).symbol(), "ARB", "ARB symbol");
|
|
}
|
|
|
|
/// ============================================
|
|
/// REAL FLASH LOAN TESTS
|
|
/// ============================================
|
|
|
|
function testIntegration_SmallFlashLoan() public {
|
|
// Borrow 1 WETH from Balancer
|
|
uint256 loanAmount = 1 ether;
|
|
|
|
// Prepare flash loan parameters
|
|
IERC20[] memory tokens = new IERC20[](1);
|
|
tokens[0] = IERC20(WETH);
|
|
|
|
uint256[] memory amounts = new uint256[](1);
|
|
amounts[0] = loanAmount;
|
|
|
|
// Create simple arbitrage path (just for testing repayment)
|
|
bytes memory userData = _createSimpleTestPath();
|
|
|
|
// Record initial balances
|
|
uint256 vaultWETHBefore = IERC20(WETH).balanceOf(BALANCER_VAULT);
|
|
|
|
// Execute flash loan
|
|
vm.prank(owner);
|
|
// Note: This will fail because we need a profitable path
|
|
// But it tests the real Balancer Vault interaction
|
|
vm.expectRevert(); // Will revert with "Insufficient funds for repayment"
|
|
flashLoan.executeArbitrage(tokens, amounts, userData);
|
|
|
|
// Verify vault balance unchanged (flash loan reverted)
|
|
assertEq(IERC20(WETH).balanceOf(BALANCER_VAULT), vaultWETHBefore);
|
|
}
|
|
|
|
function testIntegration_MultiTokenFlashLoan() public {
|
|
// Borrow multiple tokens
|
|
IERC20[] memory tokens = new IERC20[](2);
|
|
tokens[0] = IERC20(WETH);
|
|
tokens[1] = IERC20(USDC);
|
|
|
|
uint256[] memory amounts = new uint256[](2);
|
|
amounts[0] = 0.1 ether;
|
|
amounts[1] = 100 * 10**6; // 100 USDC
|
|
|
|
bytes memory userData = _createSimpleTestPath();
|
|
|
|
// Execute flash loan (will fail without profitable path)
|
|
vm.prank(owner);
|
|
vm.expectRevert();
|
|
flashLoan.executeArbitrage(tokens, amounts, userData);
|
|
}
|
|
|
|
/// ============================================
|
|
/// REAL TOKEN TRANSFER TESTS
|
|
/// ============================================
|
|
|
|
function testIntegration_ReceiveRealWETH() public {
|
|
uint256 amount = 1 ether;
|
|
|
|
// Impersonate WETH whale
|
|
vm.startPrank(WETH_WHALE);
|
|
|
|
// Transfer WETH to contract
|
|
IERC20(WETH).transfer(address(flashLoan), amount);
|
|
vm.stopPrank();
|
|
|
|
// Verify transfer
|
|
assertEq(IERC20(WETH).balanceOf(address(flashLoan)), amount);
|
|
}
|
|
|
|
function testIntegration_WithdrawRealWETH() public {
|
|
uint256 amount = 0.5 ether;
|
|
|
|
// Fund contract with real WETH
|
|
vm.prank(WETH_WHALE);
|
|
IERC20(WETH).transfer(address(flashLoan), amount);
|
|
|
|
uint256 ownerBalanceBefore = IERC20(WETH).balanceOf(owner);
|
|
|
|
// Withdraw WETH
|
|
flashLoan.withdrawProfit(WETH, amount);
|
|
|
|
assertEq(IERC20(WETH).balanceOf(owner), ownerBalanceBefore + amount);
|
|
assertEq(IERC20(WETH).balanceOf(address(flashLoan)), 0);
|
|
}
|
|
|
|
function testIntegration_EmergencyWithdrawRealTokens() public {
|
|
// Fund contract with multiple real tokens
|
|
vm.prank(WETH_WHALE);
|
|
IERC20(WETH).transfer(address(flashLoan), 0.1 ether);
|
|
|
|
vm.prank(USDC_WHALE);
|
|
IERC20(USDC).transfer(address(flashLoan), 100 * 10**6);
|
|
|
|
// Emergency withdraw WETH
|
|
uint256 wethAmount = IERC20(WETH).balanceOf(address(flashLoan));
|
|
flashLoan.emergencyWithdraw(WETH);
|
|
assertEq(IERC20(WETH).balanceOf(address(flashLoan)), 0);
|
|
assertGt(IERC20(WETH).balanceOf(owner), 0);
|
|
|
|
// Emergency withdraw USDC
|
|
uint256 usdcAmount = IERC20(USDC).balanceOf(address(flashLoan));
|
|
flashLoan.emergencyWithdraw(USDC);
|
|
assertEq(IERC20(USDC).balanceOf(address(flashLoan)), 0);
|
|
assertGt(IERC20(USDC).balanceOf(owner), 0);
|
|
}
|
|
|
|
/// ============================================
|
|
/// REAL DEX INTERACTION TESTS
|
|
/// ============================================
|
|
|
|
function testIntegration_UniswapV3RouterExists() public {
|
|
// Verify Uniswap V3 Router is deployed
|
|
uint256 size;
|
|
address router = UNISWAP_V3_ROUTER;
|
|
assembly {
|
|
size := extcodesize(router)
|
|
}
|
|
assertGt(size, 0, "Uniswap V3 Router not deployed");
|
|
}
|
|
|
|
function testIntegration_SushiswapRouterExists() public {
|
|
// Verify SushiSwap Router is deployed
|
|
uint256 size;
|
|
address router = SUSHISWAP_ROUTER;
|
|
assembly {
|
|
size := extcodesize(router)
|
|
}
|
|
assertGt(size, 0, "SushiSwap Router not deployed");
|
|
}
|
|
|
|
/// ============================================
|
|
/// REAL LIQUIDITY TESTS
|
|
/// ============================================
|
|
|
|
function testIntegration_BalancerHasLiquidity() public {
|
|
// Check Balancer Vault has significant WETH liquidity for flash loans
|
|
uint256 wethBalance = IERC20(WETH).balanceOf(BALANCER_VAULT);
|
|
uint256 usdcBalance = IERC20(USDC).balanceOf(BALANCER_VAULT);
|
|
|
|
assertGt(wethBalance, 100 ether, "Insufficient WETH liquidity");
|
|
assertGt(usdcBalance, 100_000 * 10**6, "Insufficient USDC liquidity");
|
|
|
|
emit log_named_decimal_uint("Balancer WETH Liquidity", wethBalance, 18);
|
|
emit log_named_decimal_uint("Balancer USDC Liquidity", usdcBalance, 6);
|
|
}
|
|
|
|
/// ============================================
|
|
/// REAL PRICE ORACLE TESTS
|
|
/// ============================================
|
|
|
|
function testIntegration_TokenPriceConsistency() public {
|
|
// Get real token balances from whales
|
|
uint256 wethBalance = IERC20(WETH).balanceOf(WETH_WHALE);
|
|
uint256 usdcBalance = IERC20(USDC).balanceOf(USDC_WHALE);
|
|
|
|
// Basic sanity checks
|
|
assertGt(wethBalance, 10 ether, "WETH whale balance too low");
|
|
assertGt(usdcBalance, 10_000 * 10**6, "USDC whale balance too low");
|
|
|
|
emit log("Real token balances verified");
|
|
}
|
|
|
|
/// ============================================
|
|
/// CONTRACT STATE VERIFICATION
|
|
/// ============================================
|
|
|
|
function testIntegration_ContractConfiguration() public view {
|
|
// Verify contract is properly configured
|
|
assertEq(flashLoan.owner(), owner);
|
|
assertEq(address(flashLoan.vault()), BALANCER_VAULT);
|
|
assertEq(flashLoan.MAX_SLIPPAGE_BPS(), 50);
|
|
assertEq(flashLoan.MAX_PATH_LENGTH(), 5);
|
|
assertEq(flashLoan.BASIS_POINTS(), 10000);
|
|
}
|
|
|
|
function testIntegration_ContractHasGas() public view {
|
|
// Verify contract has ETH for gas
|
|
assertGt(address(flashLoan).balance, 0);
|
|
}
|
|
|
|
/// ============================================
|
|
/// HELPER FUNCTIONS
|
|
/// ============================================
|
|
|
|
function _createSimpleTestPath() private pure returns (bytes memory) {
|
|
// Create minimal path structure for testing
|
|
// This is intentionally unprofitable to test flash loan mechanics
|
|
address[] memory tokens = new address[](2);
|
|
tokens[0] = WETH;
|
|
tokens[1] = USDC;
|
|
|
|
address[] memory exchanges = new address[](1);
|
|
exchanges[0] = UNISWAP_V3_ROUTER;
|
|
|
|
uint24[] memory fees = new uint24[](1);
|
|
fees[0] = 3000; // 0.3% fee tier
|
|
|
|
bool[] memory isV3 = new bool[](1);
|
|
isV3[0] = true;
|
|
|
|
return abi.encode(
|
|
tokens,
|
|
exchanges,
|
|
fees,
|
|
isV3,
|
|
0, // minProfit
|
|
50 // slippageBps
|
|
);
|
|
}
|
|
}
|
|
|
|
interface IERC20Metadata {
|
|
function decimals() external view returns (uint8);
|
|
function symbol() external view returns (string memory);
|
|
function name() external view returns (string memory);
|
|
}
|