CenturionDEX
Launch App

Testing with Foundry

This guide shows how to write integration tests for CenturionDEX v3 by forking Ethereum mainnet with Foundry.

Setup

# Install Foundry
curl -L https://foundry.paradigm.xyz | bash
foundryup
 
# Create a new project
forge init centurion-test
cd centurion-test

Add CenturionDEX v3 interfaces to foundry.toml:

[profile.default]
src = "src"
out = "out"
libs = ["lib"]
eth_rpc_url = "https://eth.llamarpc.com"

The Test Contract

Create test/SwapTest.t.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
 
import "forge-std/Test.sol";
 
interface ISwapRouter {
    struct ExactInputSingleParams {
        address tokenIn;
        address tokenOut;
        uint24 fee;
        address recipient;
        uint256 amountIn;
        uint256 amountOutMinimum;
        uint160 sqrtPriceLimitX96;
    }
    function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut);
}
 
interface IERC20 {
    function balanceOf(address) external view returns (uint256);
    function approve(address, uint256) external returns (bool);
}
 
interface IWCTN {
    function deposit() external payable;
}
 
contract SwapTest is Test {
    ISwapRouter constant ROUTER = ISwapRouter(0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45);
    address constant WCTN = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
    uint24 constant FEE = 3000;
 
    function setUp() public {
        // Fork mainnet at a specific block for deterministic tests
        vm.createSelectFork("https://eth.llamarpc.com", 19_000_000);
    }
 
    function test_swapExactInput() public {
        uint256 amountIn = 1 ether;
 
        // Get WCTN by wrapping CTN
        IWCTN(WCTN).deposit{value: amountIn}();
 
        // Approve the router
        IERC20(WCTN).approve(address(ROUTER), amountIn);
 
        // Record USDC balance before
        uint256 usdcBefore = IERC20(USDC).balanceOf(address(this));
 
        // Execute the swap
        uint256 amountOut = ROUTER.exactInputSingle(
            ISwapRouter.ExactInputSingleParams({
                tokenIn: WCTN,
                tokenOut: USDC,
                fee: FEE,
                recipient: address(this),
                amountIn: amountIn,
                amountOutMinimum: 0, // No minimum for testing
                sqrtPriceLimitX96: 0
            })
        );
 
        // Verify we received USDC
        uint256 usdcAfter = IERC20(USDC).balanceOf(address(this));
        assertGt(amountOut, 0, "Should receive USDC");
        assertEq(usdcAfter - usdcBefore, amountOut, "Balance should match output");
 
        // Log the result
        emit log_named_decimal_uint("USDC received", amountOut, 6);
    }
 
    function test_swapRevertsOnSlippage() public {
        uint256 amountIn = 1 ether;
 
        IWCTN(WCTN).deposit{value: amountIn}();
        IERC20(WCTN).approve(address(ROUTER), amountIn);
 
        // Set an impossibly high minimum output — swap should revert
        vm.expectRevert("Too little received");
        ROUTER.exactInputSingle(
            ISwapRouter.ExactInputSingleParams({
                tokenIn: WCTN,
                tokenOut: USDC,
                fee: FEE,
                recipient: address(this),
                amountIn: amountIn,
                amountOutMinimum: type(uint256).max,
                sqrtPriceLimitX96: 0
            })
        );
    }
 
    // Allow receiving CTN
    receive() external payable {}
}

Running Tests

# Run all tests with verbose output
forge test -vvv
 
# Run a specific test
forge test --match-test test_swapExactInput -vvv
 
# Run with gas reporting
forge test --gas-report

Testing Tips

Further Reading