// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import {Test, console} from "forge-std/Test.sol"; import "../../../contracts/bridge/trustless/BondManager.sol"; import "../../../contracts/bridge/trustless/ChallengeManager.sol"; import "../../../contracts/bridge/trustless/InboxETH.sol"; import "../../../contracts/bridge/trustless/LiquidityPoolETH.sol"; import "../../../contracts/bridge/trustless/EnhancedSwapRouter.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract MockERC20 is ERC20 { constructor(string memory name, string memory symbol) ERC20(name, symbol) { _mint(msg.sender, 1000000 ether); } } contract FuzzTests is Test { BondManager public bondManager; ChallengeManager public challengeManager; LiquidityPoolETH public liquidityPool; InboxETH public inbox; EnhancedSwapRouter public swapRouter; MockERC20 public weth; MockERC20 public usdt; MockERC20 public usdc; MockERC20 public dai; address public deployer = address(0xDE0001); address public relayer = address(0x1111); address public lp = address(0x2222); uint256 public constant BOND_MULTIPLIER = 11000; // 110% in basis points uint256 public constant MIN_BOND = 1 ether; uint256 public constant CHALLENGE_WINDOW = 30 minutes; // Mock protocol addresses address public uniswapV3Router = address(0x1111111111111111111111111111111111111111); address public curve3Pool = address(0x2222222222222222222222222222222222222222); address public dodoexRouter = address(0x3333333333333333333333333333333333333333); address public balancerVault = address(0x4444444444444444444444444444444444444444); address public oneInchRouter = address(0x5555555555555555555555555555555555555555); function setUp() public { vm.startPrank(deployer); weth = new MockERC20("Wrapped Ether", "WETH"); usdt = new MockERC20("Tether USD", "USDT"); usdc = new MockERC20("USD Coin", "USDC"); dai = new MockERC20("Dai Stablecoin", "DAI"); bondManager = new BondManager(BOND_MULTIPLIER, MIN_BOND); challengeManager = new ChallengeManager(address(bondManager), CHALLENGE_WINDOW); liquidityPool = new LiquidityPoolETH(address(weth), 5, 11000); inbox = new InboxETH(address(bondManager), address(challengeManager), address(liquidityPool)); swapRouter = new EnhancedSwapRouter( uniswapV3Router, curve3Pool, dodoexRouter, balancerVault, oneInchRouter, address(weth), address(usdt), address(usdc), address(dai) ); liquidityPool.authorizeRelease(address(inbox)); swapRouter.grantRole(swapRouter.ROUTING_MANAGER_ROLE(), deployer); vm.deal(relayer, 10000 ether); vm.deal(lp, 100000 ether); vm.warp(1000); // Provide liquidity vm.stopPrank(); vm.prank(lp); liquidityPool.provideLiquidity{value: 1000 ether}(LiquidityPoolETH.AssetType.ETH); } /// @notice Fuzz test: Bond calculation should always be >= MIN_BOND function testFuzz_BondCalculation(uint256 amount) public { // Bound amount to reasonable range (1 wei to 1000000 ether) amount = bound(amount, 1, 1000000 ether); uint256 requiredBond = bondManager.getRequiredBond(amount); // Bond should always be at least MIN_BOND assertGe(requiredBond, MIN_BOND, "Bond should be at least MIN_BOND"); // Bond should be at least amount * multiplier uint256 minExpectedBond = (amount * BOND_MULTIPLIER) / 10000; assertGe(requiredBond, minExpectedBond, "Bond should respect multiplier"); } /// @notice Fuzz test: Multiple claims with random amounts function testFuzz_MultipleClaims( uint256[10] memory amounts, uint256[10] memory depositIds ) public { // Bound amounts to reasonable range for (uint256 i = 0; i < 10; i++) { amounts[i] = bound(amounts[i], 1 ether, 100 ether); depositIds[i] = bound(depositIds[i], 1, type(uint256).max); uint256 bond = bondManager.getRequiredBond(amounts[i]); // Only proceed if relayer has enough ETH if (bond <= address(relayer).balance) { vm.prank(relayer); try inbox.submitClaim{value: bond}( depositIds[i], address(0), amounts[i], address(0x3333), "" ) { // Verify bond was posted (, uint256 bondAmount, , , ) = bondManager.bonds(depositIds[i]); assertGe(bondAmount, MIN_BOND, "Bond should be posted"); } catch { // Some reverts are acceptable (e.g., duplicate depositId) } } } } /// @notice Fuzz test: Routing configuration with random providers function testFuzz_RoutingConfig( uint8 provider1, uint8 provider2, uint8 provider3 ) public { // Bound to valid provider enum values (0-4) provider1 = uint8(bound(provider1, 0, 4)); provider2 = uint8(bound(provider2, 0, 4)); provider3 = uint8(bound(provider3, 0, 4)); EnhancedSwapRouter.SwapProvider[] memory providers = new EnhancedSwapRouter.SwapProvider[](3); providers[0] = EnhancedSwapRouter.SwapProvider(provider1); providers[1] = EnhancedSwapRouter.SwapProvider(provider2); providers[2] = EnhancedSwapRouter.SwapProvider(provider3); vm.prank(deployer); try swapRouter.setRoutingConfig(0, providers) { // Success - config was set assertTrue(true); } catch { // Revert is acceptable for invalid configurations assertTrue(true); } } /// @notice Fuzz test: Provider enable/disable with random toggles function testFuzz_ProviderToggle(uint8 providerIndex, bool enabled) public { providerIndex = uint8(bound(providerIndex, 0, 4)); EnhancedSwapRouter.SwapProvider provider = EnhancedSwapRouter.SwapProvider(providerIndex); vm.prank(deployer); swapRouter.setProviderEnabled(provider, enabled); assertEq(swapRouter.providerEnabled(provider), enabled, "Provider state should match"); } /// @notice Fuzz test: Balancer pool ID with random values function testFuzz_BalancerPoolId( address tokenIn, address tokenOut, bytes32 poolId ) public { // Avoid zero addresses vm.assume(tokenIn != address(0)); vm.assume(tokenOut != address(0)); vm.prank(deployer); swapRouter.setBalancerPoolId(tokenIn, tokenOut, poolId); assertEq(swapRouter.balancerPoolIds(tokenIn, tokenOut), poolId, "Pool ID should be set"); } /// @notice Fuzz test: Quote requests with random amounts function testFuzz_GetQuotes(uint256 amount) public { // Bound amount to reasonable range amount = bound(amount, 1 wei, 1000000 ether); // This should not revert (even if quotes are empty) try swapRouter.getQuotes(address(usdt), amount) returns ( EnhancedSwapRouter.SwapProvider[] memory providers, uint256[] memory amounts ) { // Arrays should have same length assertEq(providers.length, amounts.length, "Arrays should match length"); } catch { // Revert is acceptable if amount causes issues assertTrue(true); } } /// @notice Fuzz test: Bond release with random deposit IDs function testFuzz_BondRelease(uint256 depositId, uint256 amount) public { // Bound values depositId = bound(depositId, 1, type(uint256).max); amount = bound(amount, 1 ether, 100 ether); uint256 bond = bondManager.getRequiredBond(amount); if (bond <= address(relayer).balance) { // Submit claim vm.prank(relayer); try inbox.submitClaim{value: bond}(depositId, address(0), amount, address(0x3333), "") { // Wait for challenge window vm.warp(block.timestamp + CHALLENGE_WINDOW + 1); // Finalize challengeManager.finalizeClaim(depositId); // Try to release try bondManager.releaseBond(depositId) { // Verify bond was released (, , , , bool released) = bondManager.bonds(depositId); assertTrue(released, "Bond should be released"); } catch { // Some reverts are acceptable } } catch { // Claim submission failure is acceptable } } } /// @notice Fuzz test: Size-based routing with random amounts function testFuzz_SizeBasedRouting(uint256 amount) public { // Bound to reasonable range amount = bound(amount, 1 wei, 10000000 ether); // Test that routing logic handles various sizes // Small: < 10k, Medium: 10k-100k, Large: > 100k // This is tested indirectly through swapToStablecoin try swapRouter.getQuotes(address(usdt), amount) { // Function should not revert assertTrue(true); } catch { // Revert is acceptable for extreme values assertTrue(true); } } /// @notice Fuzz test: Concurrent operations with random parameters function testFuzz_ConcurrentOperations( uint256 numOperations, uint256 baseAmount ) public { // Bound to reasonable values numOperations = bound(numOperations, 1, 50); baseAmount = bound(baseAmount, 1 ether, 10 ether); // Perform multiple operations for (uint256 i = 0; i < numOperations; i++) { uint256 depositId = i + 1; uint256 amount = baseAmount + (i * 1 ether); uint256 bond = bondManager.getRequiredBond(amount); if (bond <= address(relayer).balance) { vm.prank(relayer); try inbox.submitClaim{value: bond}( depositId, address(0), amount, address(0x3333), "" ) { // Operation succeeded } catch { // Some failures are acceptable } } } // System should still be functional assertTrue(true); } }