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-testAdd 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-reportTesting 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 snapshotto track gas changes between code versions
Further Reading
- Local Environment Setup — Hardhat-based setup
- Architecture Overview — how the contracts relate
- Foundry Book — complete Foundry documentation