Skip to main content
This guide covers testing the Flash Loan Router contracts using Foundry’s Forge testing framework.

Running Tests

The project uses Forge for comprehensive smart contract testing.

Basic Test Execution

Run all tests in the project:
forge test
This command:
  • Compiles all contracts
  • Executes all test files in the test/ directory
  • Generates gas benchmarks (saved to snapshots/ folder)
  • Reports test results with gas usage

Verbose Output

For detailed test execution information:
# Show test names and results
forge test -v

# Show test names and console logs
forge test -vv

# Show stack traces for failures
forge test -vvv

# Show all traces and setup
forge test -vvvv
Use -vv to see console.log() output from your tests, which is helpful for debugging.

Excluding E2E Tests

Some tests require internet connectivity to interact with live networks. To run tests without these end-to-end tests:
forge test --no-match-path 'test/e2e/**/*'
This is useful for:
  • CI/CD pipelines without network access
  • Faster local development iterations
  • Running tests without external dependencies
E2E tests validate integration with real flash loan providers like Aave and Maker, so they should be run before production deployments.

Test Organization

Tests are typically organized in the following structure:
test/
├── e2e/                    # End-to-end integration tests
│   ├── AaveFlashLoan.t.sol
│   └── MakerFlashLoan.t.sol
├── unit/                   # Unit tests
│   ├── FlashLoanRouter.t.sol
│   ├── AaveBorrower.t.sol
│   └── ERC3156Borrower.t.sol
└── mocks/                  # Mock contracts for testing

Running Specific Tests

By Test File

forge test --match-path test/unit/FlashLoanRouter.t.sol

By Test Contract

forge test --match-contract FlashLoanRouterTest

By Test Function

forge test --match-test testFlashLoanAndSettle

Combined Filters

forge test --match-path test/unit/* --match-test testSuccess

Gas Benchmarking

The repository includes automated gas benchmarking for different flash loan providers.

Generate Benchmarks

Gas snapshots are automatically generated when running tests:
forge test
Benchmark data is saved to the snapshots/ directory:
snapshots/
├── FlashLoanRouterTest.snap
├── AaveBorrowerTest.snap
└── ERC3156BorrowerTest.snap

View Gas Report

Generate a detailed gas usage report:
forge test --gas-report
Example output:
| Contract          | Function           | min  | avg   | max   | calls |
|-------------------|--------------------|------|-------|-------|-------|
| FlashLoanRouter   | flashLoanAndSettle | 1234 | 12456 | 23456 | 45    |
| AaveBorrower      | flashLoan          | 456  | 5678  | 9876  | 30    |

Update Snapshots

To update gas snapshots after contract changes:
forge snapshot
Review gas snapshot changes carefully before committing. Significant increases may indicate performance regressions.

Test Configuration

Test settings are configured in foundry.toml:
[profile.default]
# Test-specific settings
isolate = true  # Execute each call as independent transaction

# File system permissions for tests
fs_permissions = [
  { access = "read", path = "./networks.json"}
]

Transaction Isolation

The isolate = true setting ensures:
  • Each function call in a test is an independent transaction
  • Transient storage is cleared between calls
  • More accurate simulation of real-world behavior
This is particularly important for testing transient storage features. See Foundry issue #6908 for details.

Writing Tests

Test Structure

Foundry tests use Solidity and follow this pattern:
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8;

import {Test} from "forge-std/Test.sol";
import {FlashLoanRouter} from "src/FlashLoanRouter.sol";

contract FlashLoanRouterTest is Test {
    FlashLoanRouter router;

    function setUp() public {
        // Deploy contracts and set up test environment
        router = new FlashLoanRouter(settlementContract);
    }

    function testFlashLoanSuccess() public {
        // Arrange: Set up test conditions

        // Act: Execute the function being tested

        // Assert: Verify expected outcomes
        assertEq(actual, expected);
    }

    function testFlashLoanRevert() public {
        // Test expected failures
        vm.expectRevert("Error message");
        router.flashLoanAndSettle(...);
    }
}

Common Test Patterns

function testOnlySolverCanCall() public {
    address solver = makeAddr("solver");
    address attacker = makeAddr("attacker");

    // Test as solver
    vm.prank(solver);
    router.flashLoanAndSettle(...);

    // Test as attacker (should fail)
    vm.prank(attacker);
    vm.expectRevert("Not a solver");
    router.flashLoanAndSettle(...);
}
function testEmitsEvent() public {
    vm.expectEmit(true, true, false, true);
    emit FlashLoanExecuted(lender, token, amount);

    router.flashLoanAndSettle(...);
}
function testFlashLoanWithFuzzedAmount(uint256 amount) public {
    vm.assume(amount > 0 && amount < type(uint128).max);

    // Test with random amounts
    router.flashLoanAndSettle(amount, ...);
}
function testWithMockLender() public {
    MockLender mockLender = new MockLender();

    // Set up mock behavior
    mockLender.setFlashLoanFee(100);

    // Test with mock
    router.flashLoanAndSettle(mockLender, ...);
}

Continuous Integration

For CI environments, use the CI profile with strict settings:
FOUNDRY_PROFILE=ci forge test --no-match-path 'test/e2e/**/*'
The CI profile (defined in foundry.toml):
[profile.ci]
deny_warnings = true  # Treat warnings as errors
fuzz.seed = '0'      # Deterministic fuzzing

Test Coverage

Generate test coverage reports:
# Generate coverage
forge coverage

# Generate detailed LCOV report
forge coverage --report lcov

# View coverage in browser (requires lcov)
genhtml lcov.info -o coverage
open coverage/index.html
Aim for >90% code coverage, with 100% coverage for critical security functions.

Debugging Tests

Debug Mode

Run a specific test in debug mode:
forge test --match-test testFlashLoanAndSettle --debug
This opens an interactive debugger where you can:
  • Step through transactions
  • Inspect state variables
  • View stack traces
  • Examine memory and storage

Console Logging

Add debug output to your tests:
import {console} from "forge-std/console.sol";

function testDebug() public {
    console.log("Router address:", address(router));
    console.log("Balance:", token.balanceOf(address(this)));

    router.flashLoanAndSettle(...);

    console.log("Final balance:", token.balanceOf(address(this)));
}
Run with -vv to see console output:
forge test --match-test testDebug -vv

Common Testing Scenarios

Unit Tests

Test individual contract functions in isolation:
  • FlashLoanRouter authentication
  • Borrower flash loan triggers
  • Approval management
  • Access control

Integration Tests

Test contract interactions:
  • Router + Borrower coordination
  • Multiple flash loans in sequence
  • Settlement execution flow
  • Token transfers

E2E Tests

Test with real protocols:
  • Aave flash loans
  • Maker flash mints
  • CoW Protocol settlements
  • Multi-network deployments

Security Tests

Test security properties:
  • Reentrancy protection
  • Access control validation
  • Flash loan repayment
  • Invariant testing

Best Practices

  1. Test organization: Group related tests in descriptive contracts
  2. Clear test names: Use descriptive names like testFlashLoanRevertsWithInsufficientApproval
  3. Setup isolation: Each test should be independent and reproducible
  4. Edge cases: Test boundary conditions, zero values, and maximum amounts
  5. Error cases: Test expected failures with vm.expectRevert()
  6. Gas optimization: Monitor gas usage with benchmarks
  7. Documentation: Comment complex test scenarios

Troubleshooting

Common causes:
  • Different Forge versions
  • Network-dependent tests not excluded
  • Non-deterministic behavior (use fixed seeds)
  • Missing environment variables
Solution: Use FOUNDRY_PROFILE=ci locally to reproduce CI environment.
Increase the gas limit for tests:
function testHighGas() public {
    vm.txGasPrice(1 gwei);
    // Your test code
}
Or in foundry.toml:
gas_limit = 10000000
The isolate = true setting handles transient storage. If you encounter issues:
  1. Verify isolate = true in foundry.toml
  2. Update to latest Forge version
  3. Check Foundry issue #6908

Next Steps

Last modified on March 4, 2026