Testing Guide

Last modified:

Testing with Foundry

This guide shows how to write integration tests for CenturionDEX v3 by forking Centurion 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"]
 
[rpc_endpoints]
centurion = "https://rpc.centuriondex.org"

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 ICRC20 {
    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(vm.rpcUrl("centurion"), 19_000_000);
    }
 
    function test_swapExactInput() public {
        uint256 amountIn = 1e18;
 
        // Get WCTN by wrapping CTN
        IWCTN(WCTN).deposit{value: amountIn}();
 
        // Approve the router
        ICRC20(WCTN).approve(address(ROUTER), amountIn);
 
        // Record USDC balance before
        uint256 usdcBefore = ICRC20(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 = ICRC20(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 = 1e18;
 
        IWCTN(WCTN).deposit{value: amountIn}();
        ICRC20(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

  • Pin the fork block (vm.createSelectFork(url, blockNumber)) for deterministic results — pool state and prices won't change between runs
  • Use deal() to set arbitrary token balances: deal(USDC, address(this), 10_000e6)
  • Use vm.prank() to impersonate any address
  • Test edge cases: zero amounts, expired deadlines, insufficient allowance, pool doesn't exist
  • Gas optimization: Use forge snapshot to track gas changes between code versions

Further Reading