Testing Guide
This comprehensive guide covers testing strategies, frameworks, and best practices for IXFI Protocol smart contracts and integrations.
Overview
IXFI Protocol testing encompasses multiple layers:
Unit Tests: Individual contract function testing
Integration Tests: Multi-contract interaction testing
End-to-End Tests: Full protocol workflow testing
Cross-Chain Tests: Multi-blockchain integration testing
Security Tests: Vulnerability and attack vector testing
Performance Tests: Gas optimization and load testing
Testing Environment Setup
Prerequisites
# Core dependencies
npm install --save-dev @nomicfoundation/hardhat-toolbox
npm install --save-dev @nomicfoundation/hardhat-network-helpers
npm install --save-dev @nomicfoundation/hardhat-chai-matchers
npm install --save-dev @typechain/hardhat
npm install --save-dev solidity-coverage
npm install --save-dev hardhat-gas-reporter
# Testing frameworks
npm install --save-dev chai mocha
npm install --save-dev @openzeppelin/test-helpers
npm install --save-dev ethereum-waffle
# Advanced testing tools
npm install --save-dev @tenderly/hardhat-tenderly
npm install --save-dev hardhat-tracer
npm install --save-dev hardhat-contract-sizer
Hardhat Configuration
// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
require("@nomicfoundation/hardhat-network-helpers");
require("solidity-coverage");
require("hardhat-gas-reporter");
require("hardhat-contract-sizer");
module.exports = {
solidity: {
version: "0.8.19",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
},
networks: {
hardhat: {
chainId: 31337,
forking: {
url: process.env.ETHEREUM_RPC_URL,
blockNumber: 18500000 // Pin for consistency
},
// Increase gas limit for complex tests
gas: 30000000,
gasPrice: 20000000000,
// Enable console.log in contracts
loggingEnabled: true
},
localhost: {
url: "http://127.0.0.1:8545",
chainId: 31337
}
},
gasReporter: {
enabled: process.env.REPORT_GAS !== undefined,
currency: "USD",
gasPrice: 20,
coinmarketcap: process.env.COINMARKETCAP_API_KEY
},
contractSizer: {
alphaSort: true,
disambiguatePaths: false,
runOnCompile: true,
strict: true
},
mocha: {
timeout: 60000 // 60 seconds
}
};
Unit Testing
Basic Contract Testing
// test/unit/IXFI.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");
describe("IXFI Token", function () {
async function deployIXFIFixture() {
const [owner, addr1, addr2] = await ethers.getSigners();
const IXFI = await ethers.getContractFactory("IXFI");
const ixfi = await IXFI.deploy();
return { ixfi, owner, addr1, addr2 };
}
describe("Deployment", function () {
it("Should set the right name and symbol", async function () {
const { ixfi } = await loadFixture(deployIXFIFixture);
expect(await ixfi.name()).to.equal("IXFI Protocol");
expect(await ixfi.symbol()).to.equal("IXFI");
});
it("Should set the right decimals", async function () {
const { ixfi } = await loadFixture(deployIXFIFixture);
expect(await ixfi.decimals()).to.equal(18);
});
it("Should assign the total supply to the owner", async function () {
const { ixfi, owner } = await loadFixture(deployIXFIFixture);
const ownerBalance = await ixfi.balanceOf(owner.address);
const totalSupply = await ixfi.totalSupply();
expect(ownerBalance).to.equal(totalSupply);
});
});
describe("Transfers", function () {
it("Should transfer tokens between accounts", async function () {
const { ixfi, owner, addr1, addr2 } = await loadFixture(deployIXFIFixture);
// Transfer 50 tokens from owner to addr1
await expect(ixfi.transfer(addr1.address, 50))
.to.changeTokenBalances(ixfi, [owner, addr1], [-50, 50]);
// Transfer 50 tokens from addr1 to addr2
await expect(ixfi.connect(addr1).transfer(addr2.address, 50))
.to.changeTokenBalances(ixfi, [addr1, addr2], [-50, 50]);
});
it("Should fail if sender doesn't have enough tokens", async function () {
const { ixfi, owner, addr1 } = await loadFixture(deployIXFIFixture);
const initialOwnerBalance = await ixfi.balanceOf(owner.address);
await expect(
ixfi.connect(addr1).transfer(owner.address, 1)
).to.be.revertedWith("ERC20: transfer amount exceeds balance");
expect(await ixfi.balanceOf(owner.address)).to.equal(initialOwnerBalance);
});
it("Should emit Transfer events", async function () {
const { ixfi, owner, addr1 } = await loadFixture(deployIXFIFixture);
await expect(ixfi.transfer(addr1.address, 50))
.to.emit(ixfi, "Transfer")
.withArgs(owner.address, addr1.address, 50);
});
});
describe("Allowances", function () {
it("Should approve and transfer from", async function () {
const { ixfi, owner, addr1, addr2 } = await loadFixture(deployIXFIFixture);
// Approve addr1 to spend 100 tokens
await ixfi.approve(addr1.address, 100);
expect(await ixfi.allowance(owner.address, addr1.address)).to.equal(100);
// Transfer 50 tokens from owner to addr2 via addr1
await expect(ixfi.connect(addr1).transferFrom(owner.address, addr2.address, 50))
.to.changeTokenBalances(ixfi, [owner, addr2], [-50, 50]);
// Check remaining allowance
expect(await ixfi.allowance(owner.address, addr1.address)).to.equal(50);
});
it("Should fail transferFrom without allowance", async function () {
const { ixfi, owner, addr1, addr2 } = await loadFixture(deployIXFIFixture);
await expect(
ixfi.connect(addr1).transferFrom(owner.address, addr2.address, 50)
).to.be.revertedWith("ERC20: insufficient allowance");
});
});
});
Cross-Chain Aggregator Testing
// test/unit/CrossChainAggregator.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");
describe("CrossChainAggregator", function () {
async function deployAggregatorFixture() {
const [owner, user1, user2, feeRecipient] = await ethers.getSigners();
// Deploy mock contracts
const MockGateway = await ethers.getContractFactory("MockAxelarGateway");
const gateway = await MockGateway.deploy();
const MockGasService = await ethers.getContractFactory("MockAxelarGasService");
const gasService = await MockGasService.deploy();
const MockOracle = await ethers.getContractFactory("MockDIAOracle");
const oracle = await MockOracle.deploy();
const MockERC20 = await ethers.getContractFactory("MockERC20");
const tokenA = await MockERC20.deploy("Token A", "TKA", 18);
const tokenB = await MockERC20.deploy("Token B", "TKB", 18);
// Deploy router mocks
const MockRouter = await ethers.getContractFactory("MockRouter");
const uniswapV2Router = await MockRouter.deploy();
const sushiswapRouter = await MockRouter.deploy();
// Deploy aggregator
const CrossChainAggregator = await ethers.getContractFactory("CrossChainAggregator");
const aggregator = await CrossChainAggregator.deploy(
gateway.address,
gasService.address,
oracle.address,
[uniswapV2Router.address, sushiswapRouter.address],
[0, 1] // Router types
);
// Setup initial state
await tokenA.mint(user1.address, ethers.utils.parseEther("1000"));
await tokenB.mint(user1.address, ethers.utils.parseEther("1000"));
await aggregator.setFeeRecipient(feeRecipient.address);
await aggregator.setProtocolFee(30); // 0.3%
return {
aggregator,
gateway,
gasService,
oracle,
tokenA,
tokenB,
uniswapV2Router,
sushiswapRouter,
owner,
user1,
user2,
feeRecipient
};
}
describe("Deployment", function () {
it("Should set correct initial parameters", async function () {
const { aggregator, gateway, gasService, oracle } = await loadFixture(deployAggregatorFixture);
expect(await aggregator.axelarGateway()).to.equal(gateway.address);
expect(await aggregator.axelarGasService()).to.equal(gasService.address);
expect(await aggregator.gasOracle()).to.equal(oracle.address);
});
it("Should set router configurations", async function () {
const { aggregator, uniswapV2Router, sushiswapRouter } = await loadFixture(deployAggregatorFixture);
const router0 = await aggregator.routers(0);
const router1 = await aggregator.routers(1);
expect(router0).to.equal(uniswapV2Router.address);
expect(router1).to.equal(sushiswapRouter.address);
});
});
describe("Swap Execution", function () {
it("Should execute simple swap", async function () {
const { aggregator, tokenA, tokenB, user1 } = await loadFixture(deployAggregatorFixture);
const amountIn = ethers.utils.parseEther("10");
const minAmountOut = ethers.utils.parseEther("9");
// Approve tokens
await tokenA.connect(user1).approve(aggregator.address, amountIn);
// Execute swap
const swapParams = {
tokenIn: tokenA.address,
tokenOut: tokenB.address,
amountIn: amountIn,
minAmountOut: minAmountOut,
routerType: 0,
to: user1.address,
deadline: Math.floor(Date.now() / 1000) + 300,
swapData: "0x"
};
await expect(aggregator.connect(user1).executeSwap(swapParams))
.to.emit(aggregator, "SwapExecuted")
.withArgs(
user1.address,
tokenA.address,
tokenB.address,
amountIn,
ethers.utils.parseEther("9.5"), // Mock router returns 95% of input
0
);
});
it("Should fail with insufficient slippage protection", async function () {
const { aggregator, tokenA, tokenB, user1 } = await loadFixture(deployAggregatorFixture);
const amountIn = ethers.utils.parseEther("10");
const minAmountOut = ethers.utils.parseEther("10"); // Too high
await tokenA.connect(user1).approve(aggregator.address, amountIn);
const swapParams = {
tokenIn: tokenA.address,
tokenOut: tokenB.address,
amountIn: amountIn,
minAmountOut: minAmountOut,
routerType: 0,
to: user1.address,
deadline: Math.floor(Date.now() / 1000) + 300,
swapData: "0x"
};
await expect(
aggregator.connect(user1).executeSwap(swapParams)
).to.be.revertedWith("Insufficient output amount");
});
it("Should collect protocol fees", async function () {
const { aggregator, tokenA, tokenB, user1, feeRecipient } = await loadFixture(deployAggregatorFixture);
const amountIn = ethers.utils.parseEther("10");
const minAmountOut = ethers.utils.parseEther("9");
await tokenA.connect(user1).approve(aggregator.address, amountIn);
const swapParams = {
tokenIn: tokenA.address,
tokenOut: tokenB.address,
amountIn: amountIn,
minAmountOut: minAmountOut,
routerType: 0,
to: user1.address,
deadline: Math.floor(Date.now() / 1000) + 300,
swapData: "0x"
};
const initialFeeRecipientBalance = await tokenB.balanceOf(feeRecipient.address);
await aggregator.connect(user1).executeSwap(swapParams);
const finalFeeRecipientBalance = await tokenB.balanceOf(feeRecipient.address);
expect(finalFeeRecipientBalance).to.be.gt(initialFeeRecipientBalance);
});
});
describe("Quote System", function () {
it("Should return accurate quotes", async function () {
const { aggregator, tokenA, tokenB } = await loadFixture(deployAggregatorFixture);
const amountIn = ethers.utils.parseEther("10");
const [amountOut, gasEstimate] = await aggregator.getQuote(
tokenA.address,
tokenB.address,
amountIn,
0
);
expect(amountOut).to.equal(ethers.utils.parseEther("9.5"));
expect(gasEstimate).to.be.gt(0);
});
it("Should return quotes from all routers", async function () {
const { aggregator, tokenA, tokenB } = await loadFixture(deployAggregatorFixture);
const amountIn = ethers.utils.parseEther("10");
const quotes = await aggregator.getAllQuotes(tokenA.address, tokenB.address, amountIn);
expect(quotes).to.have.length(2);
expect(quotes[0].success).to.be.true;
expect(quotes[1].success).to.be.true;
});
});
describe("Cross-Chain Operations", function () {
it("Should initiate cross-chain swap", async function () {
const { aggregator, tokenA, tokenB, user1 } = await loadFixture(deployAggregatorFixture);
const amountIn = ethers.utils.parseEther("10");
await tokenA.connect(user1).approve(aggregator.address, amountIn);
const crossChainParams = {
sourceSwap: {
tokenIn: tokenA.address,
tokenOut: tokenB.address,
amountIn: amountIn,
minAmountOut: ethers.utils.parseEther("9"),
routerType: 0,
to: aggregator.address,
deadline: Math.floor(Date.now() / 1000) + 300,
swapData: "0x"
},
destinationChain: "polygon",
destinationToken: tokenB.address,
destinationReceiver: user1.address,
minDestinationAmount: ethers.utils.parseEther("8"),
destinationSwapData: "0x"
};
await expect(
aggregator.connect(user1).crossChainSwap(crossChainParams, { value: ethers.utils.parseEther("0.01") })
).to.emit(aggregator, "CrossChainSwapInitiated");
});
});
describe("Access Control", function () {
it("Should only allow owner to set parameters", async function () {
const { aggregator, user1 } = await loadFixture(deployAggregatorFixture);
await expect(
aggregator.connect(user1).setProtocolFee(50)
).to.be.revertedWith("Ownable: caller is not the owner");
});
it("Should allow owner to pause contract", async function () {
const { aggregator, owner } = await loadFixture(deployAggregatorFixture);
await aggregator.connect(owner).pause();
expect(await aggregator.paused()).to.be.true;
});
});
});
Integration Testing
Multi-Contract Integration
// test/integration/full-protocol.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { loadFixture, time } = require("@nomicfoundation/hardhat-network-helpers");
describe("Full Protocol Integration", function () {
async function deployFullProtocolFixture() {
const [owner, user1, user2, relayer, feeRecipient] = await ethers.getSigners();
// Deploy all contracts
const IXFI = await ethers.getContractFactory("IXFI");
const ixfi = await IXFI.deploy();
const MockGateway = await ethers.getContractFactory("MockAxelarGateway");
const gateway = await MockGateway.deploy();
const MockGasService = await ethers.getContractFactory("MockAxelarGasService");
const gasService = await MockGasService.deploy();
const MockOracle = await ethers.getContractFactory("MockDIAOracle");
const oracle = await MockOracle.deploy();
const MockRouter = await ethers.getContractFactory("MockRouter");
const router = await MockRouter.deploy();
const CrossChainAggregator = await ethers.getContractFactory("CrossChainAggregator");
const aggregator = await CrossChainAggregator.deploy(
gateway.address,
gasService.address,
oracle.address,
[router.address],
[0]
);
const MetaTxGasCreditVault = await ethers.getContractFactory("MetaTxGasCreditVault");
const gasCreditVault = await MetaTxGasCreditVault.deploy(ixfi.address, oracle.address);
const MetaTxGateway = await ethers.getContractFactory("MetaTxGateway");
const metaTxGateway = await MetaTxGateway.deploy(
aggregator.address,
gasCreditVault.address,
gateway.address,
gasService.address
);
const IXFIExecutable = await ethers.getContractFactory("IXFIExecutable");
const executable = await IXFIExecutable.deploy(
gateway.address,
gasService.address,
aggregator.address
);
// Setup tokens
const MockERC20 = await ethers.getContractFactory("MockERC20");
const usdc = await MockERC20.deploy("USD Coin", "USDC", 6);
const usdt = await MockERC20.deploy("Tether USD", "USDT", 6);
// Initial setup
await ixfi.transfer(user1.address, ethers.utils.parseEther("1000"));
await usdc.mint(user1.address, "1000000000"); // 1000 USDC (6 decimals)
await usdt.mint(user1.address, "1000000000"); // 1000 USDT (6 decimals)
// Configure contracts
await aggregator.setFeeRecipient(feeRecipient.address);
await aggregator.setProtocolFee(30);
await metaTxGateway.setRelayerStatus(relayer.address, true);
await oracle.setPrice("gwei", ethers.utils.parseUnits("20", "gwei"));
return {
ixfi,
aggregator,
metaTxGateway,
gasCreditVault,
executable,
usdc,
usdt,
oracle,
owner,
user1,
user2,
relayer,
feeRecipient
};
}
describe("Basic Swap Flow", function () {
it("Should complete full swap workflow", async function () {
const { aggregator, usdc, usdt, user1 } = await loadFixture(deployFullProtocolFixture);
const amountIn = "100000000"; // 100 USDC
const minAmountOut = "95000000"; // 95 USDT
// 1. User approves tokens
await usdc.connect(user1).approve(aggregator.address, amountIn);
// 2. Get quote
const [amountOut, gasEstimate] = await aggregator.getQuote(
usdc.address,
usdt.address,
amountIn,
0
);
expect(amountOut).to.be.gte(minAmountOut);
// 3. Execute swap
const swapParams = {
tokenIn: usdc.address,
tokenOut: usdt.address,
amountIn: amountIn,
minAmountOut: minAmountOut,
routerType: 0,
to: user1.address,
deadline: Math.floor(Date.now() / 1000) + 300,
swapData: "0x"
};
const initialUSDCBalance = await usdc.balanceOf(user1.address);
const initialUSDTBalance = await usdt.balanceOf(user1.address);
await aggregator.connect(user1).executeSwap(swapParams);
const finalUSDCBalance = await usdc.balanceOf(user1.address);
const finalUSDTBalance = await usdt.balanceOf(user1.address);
expect(finalUSDCBalance).to.equal(initialUSDCBalance.sub(amountIn));
expect(finalUSDTBalance).to.be.gt(initialUSDTBalance);
});
});
describe("Meta-Transaction Flow", function () {
it("Should execute gasless transaction", async function () {
const { aggregator, metaTxGateway, gasCreditVault, ixfi, usdc, usdt, user1, relayer } =
await loadFixture(deployFullProtocolFixture);
// 1. User deposits gas credits
const gasDepositAmount = ethers.utils.parseEther("100");
await ixfi.connect(user1).approve(gasCreditVault.address, gasDepositAmount);
await gasCreditVault.connect(user1).depositGasCredit(gasDepositAmount);
// 2. Prepare meta-transaction
const swapAmount = "50000000"; // 50 USDC
await usdc.connect(user1).approve(aggregator.address, swapAmount);
const metaTxParams = {
user: user1.address,
tokenIn: usdc.address,
tokenOut: usdt.address,
amountIn: swapAmount,
minAmountOut: "47500000", // 47.5 USDT
routerType: 0,
deadline: Math.floor(Date.now() / 1000) + 300,
swapData: "0x"
};
// 3. Relayer executes meta-transaction
const initialUserBalance = await usdt.balanceOf(user1.address);
const initialGasCredit = await gasCreditVault.getGasCredit(user1.address);
await metaTxGateway.connect(relayer).executeMetaSwap(metaTxParams);
const finalUserBalance = await usdt.balanceOf(user1.address);
const finalGasCredit = await gasCreditVault.getGasCredit(user1.address);
expect(finalUserBalance).to.be.gt(initialUserBalance);
expect(finalGasCredit).to.be.lt(initialGasCredit);
});
});
describe("Cross-Chain Integration", function () {
it("Should handle cross-chain message execution", async function () {
const { executable, aggregator, usdc, usdt, user1 } = await loadFixture(deployFullProtocolFixture);
// Simulate cross-chain message execution
const sourceChain = "ethereum";
const sourceAddress = "0x1234567890123456789012345678901234567890";
const swapData = ethers.utils.defaultAbiCoder.encode(
["address", "address", "uint256", "uint256", "uint8", "address"],
[usdc.address, usdt.address, "100000000", "95000000", 0, user1.address]
);
// Mint tokens to executable contract for the swap
await usdc.mint(executable.address, "100000000");
const initialBalance = await usdt.balanceOf(user1.address);
// Execute cross-chain swap
await executable._testExecute(sourceChain, sourceAddress, swapData);
const finalBalance = await usdt.balanceOf(user1.address);
expect(finalBalance).to.be.gt(initialBalance);
});
});
describe("Fee Distribution", function () {
it("Should correctly distribute protocol fees", async function () {
const { aggregator, usdc, usdt, user1, feeRecipient } = await loadFixture(deployFullProtocolFixture);
const amountIn = "1000000000"; // 1000 USDC
await usdc.connect(user1).approve(aggregator.address, amountIn);
const swapParams = {
tokenIn: usdc.address,
tokenOut: usdt.address,
amountIn: amountIn,
minAmountOut: "950000000",
routerType: 0,
to: user1.address,
deadline: Math.floor(Date.now() / 1000) + 300,
swapData: "0x"
};
const initialFeeBalance = await usdt.balanceOf(feeRecipient.address);
await aggregator.connect(user1).executeSwap(swapParams);
const finalFeeBalance = await usdt.balanceOf(feeRecipient.address);
// Check that fees were collected (0.3% of output)
const expectedFee = ethers.BigNumber.from("950000000").mul(30).div(10000);
expect(finalFeeBalance.sub(initialFeeBalance)).to.be.closeTo(expectedFee, "1000000");
});
});
describe("Emergency Scenarios", function () {
it("Should handle contract pause", async function () {
const { aggregator, usdc, usdt, user1, owner } = await loadFixture(deployFullProtocolFixture);
// Pause contract
await aggregator.connect(owner).pause();
const swapParams = {
tokenIn: usdc.address,
tokenOut: usdt.address,
amountIn: "100000000",
minAmountOut: "95000000",
routerType: 0,
to: user1.address,
deadline: Math.floor(Date.now() / 1000) + 300,
swapData: "0x"
};
await expect(
aggregator.connect(user1).executeSwap(swapParams)
).to.be.revertedWith("Pausable: paused");
// Unpause and try again
await aggregator.connect(owner).unpause();
await usdc.connect(user1).approve(aggregator.address, "100000000");
await expect(
aggregator.connect(user1).executeSwap(swapParams)
).to.emit(aggregator, "SwapExecuted");
});
});
});
Fork Testing
Mainnet Fork Tests
// test/fork/mainnet-integration.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { impersonateAccount, setBalance } = require("@nomicfoundation/hardhat-network-helpers");
describe("Mainnet Fork Integration", function () {
const USDC_ADDRESS = "0xA0b86a33E6441e1a02c4e4670dd96EA0f25A632";
const USDT_ADDRESS = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
const WETH_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
const UNISWAP_V2_ROUTER = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D";
const UNISWAP_V3_ROUTER = "0xE592427A0AEce92De3Edee1F18E0157C05861564";
let aggregator;
let usdc, usdt, weth;
let usdcWhale, user;
before(async function () {
// Fork mainnet at specific block
await network.provider.request({
method: "hardhat_reset",
params: [{
forking: {
jsonRpcUrl: process.env.ETHEREUM_RPC_URL,
blockNumber: 18500000
}
}]
});
// Get signers
[user] = await ethers.getSigners();
// Impersonate USDC whale
const usdcWhaleAddress = "0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503"; // Binance wallet
await impersonateAccount(usdcWhaleAddress);
await setBalance(usdcWhaleAddress, ethers.utils.parseEther("100"));
usdcWhale = await ethers.getSigner(usdcWhaleAddress);
// Get token contracts
usdc = await ethers.getContractAt("IERC20", USDC_ADDRESS);
usdt = await ethers.getContractAt("IERC20", USDT_ADDRESS);
weth = await ethers.getContractAt("IERC20", WETH_ADDRESS);
// Deploy aggregator with real router addresses
const MockGateway = await ethers.getContractFactory("MockAxelarGateway");
const gateway = await MockGateway.deploy();
const MockGasService = await ethers.getContractFactory("MockAxelarGasService");
const gasService = await MockGasService.deploy();
const MockOracle = await ethers.getContractFactory("MockDIAOracle");
const oracle = await MockOracle.deploy();
const CrossChainAggregator = await ethers.getContractFactory("CrossChainAggregator");
aggregator = await CrossChainAggregator.deploy(
gateway.address,
gasService.address,
oracle.address,
[UNISWAP_V2_ROUTER, UNISWAP_V3_ROUTER],
[0, 10] // Uniswap V2 and V3
);
// Transfer USDC from whale to user
const transferAmount = ethers.utils.parseUnits("10000", 6); // 10,000 USDC
await usdc.connect(usdcWhale).transfer(user.address, transferAmount);
});
it("Should execute real Uniswap V2 swap", async function () {
const amountIn = ethers.utils.parseUnits("1000", 6); // 1000 USDC
const minAmountOut = ethers.utils.parseUnits("950", 6); // 950 USDT (5% slippage)
await usdc.connect(user).approve(aggregator.address, amountIn);
const initialUSDCBalance = await usdc.balanceOf(user.address);
const initialUSDTBalance = await usdt.balanceOf(user.address);
const swapParams = {
tokenIn: USDC_ADDRESS,
tokenOut: USDT_ADDRESS,
amountIn: amountIn,
minAmountOut: minAmountOut,
routerType: 0, // Uniswap V2
to: user.address,
deadline: Math.floor(Date.now() / 1000) + 300,
swapData: "0x"
};
await aggregator.connect(user).executeSwap(swapParams);
const finalUSDCBalance = await usdc.balanceOf(user.address);
const finalUSDTBalance = await usdt.balanceOf(user.address);
expect(finalUSDCBalance).to.equal(initialUSDCBalance.sub(amountIn));
expect(finalUSDTBalance).to.be.gt(initialUSDTBalance.add(minAmountOut));
});
it("Should execute real Uniswap V3 swap", async function () {
const amountIn = ethers.utils.parseUnits("1000", 6); // 1000 USDC
const minAmountOut = ethers.utils.parseUnits("950", 6); // 950 USDT
await usdc.connect(user).approve(aggregator.address, amountIn);
const swapParams = {
tokenIn: USDC_ADDRESS,
tokenOut: USDT_ADDRESS,
amountIn: amountIn,
minAmountOut: minAmountOut,
routerType: 10, // Uniswap V3
to: user.address,
deadline: Math.floor(Date.now() / 1000) + 300,
swapData: "0x"
};
await expect(aggregator.connect(user).executeSwap(swapParams))
.to.emit(aggregator, "SwapExecuted");
});
it("Should compare quotes from different DEXes", async function () {
const amountIn = ethers.utils.parseUnits("1000", 6);
const quotes = await aggregator.getAllQuotes(USDC_ADDRESS, USDT_ADDRESS, amountIn);
expect(quotes).to.have.length.greaterThan(0);
// Check that we get different quotes from different routers
const v2Quote = quotes.find(q => q.routerType === 0);
const v3Quote = quotes.find(q => q.routerType === 10);
if (v2Quote && v3Quote) {
console.log(`Uniswap V2 quote: ${ethers.utils.formatUnits(v2Quote.amountOut, 6)} USDT`);
console.log(`Uniswap V3 quote: ${ethers.utils.formatUnits(v3Quote.amountOut, 6)} USDT`);
}
});
it("Should handle large swaps efficiently", async function () {
const largeAmount = ethers.utils.parseUnits("5000", 6); // 5000 USDC
await usdc.connect(user).approve(aggregator.address, largeAmount);
const gasEstimate = await aggregator.connect(user).estimateGas.executeSwap({
tokenIn: USDC_ADDRESS,
tokenOut: USDT_ADDRESS,
amountIn: largeAmount,
minAmountOut: ethers.utils.parseUnits("4750", 6),
routerType: 0,
to: user.address,
deadline: Math.floor(Date.now() / 1000) + 300,
swapData: "0x"
});
console.log(`Gas estimate for large swap: ${gasEstimate.toString()}`);
expect(gasEstimate).to.be.lt(300000); // Should be efficient
});
});
Security Testing
Vulnerability Testing
// test/security/security.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");
describe("Security Tests", function () {
// ... fixture setup ...
describe("Reentrancy Protection", function () {
it("Should prevent reentrancy attacks", async function () {
// Deploy malicious contract that attempts reentrancy
const MaliciousContract = await ethers.getContractFactory("MaliciousReentrant");
const malicious = await MaliciousContract.deploy();
// ... test reentrancy attack prevention ...
});
});
describe("Access Control", function () {
it("Should enforce owner-only functions", async function () {
const { aggregator, user1 } = await loadFixture(deployAggregatorFixture);
const ownerOnlyFunctions = [
"setProtocolFee",
"setFeeRecipient",
"addRouter",
"removeRouter",
"pause",
"unpause"
];
for (const funcName of ownerOnlyFunctions) {
await expect(
aggregator.connect(user1)[funcName](...getDefaultArgs(funcName))
).to.be.revertedWith("Ownable: caller is not the owner");
}
});
});
describe("Input Validation", function () {
it("Should validate swap parameters", async function () {
const { aggregator, tokenA, tokenB, user1 } = await loadFixture(deployAggregatorFixture);
// Test zero amount
await expect(
aggregator.connect(user1).executeSwap({
tokenIn: tokenA.address,
tokenOut: tokenB.address,
amountIn: 0,
minAmountOut: 0,
routerType: 0,
to: user1.address,
deadline: Math.floor(Date.now() / 1000) + 300,
swapData: "0x"
})
).to.be.revertedWith("Invalid amount");
// Test same token
await expect(
aggregator.connect(user1).executeSwap({
tokenIn: tokenA.address,
tokenOut: tokenA.address,
amountIn: ethers.utils.parseEther("1"),
minAmountOut: ethers.utils.parseEther("1"),
routerType: 0,
to: user1.address,
deadline: Math.floor(Date.now() / 1000) + 300,
swapData: "0x"
})
).to.be.revertedWith("Same token");
// Test expired deadline
await expect(
aggregator.connect(user1).executeSwap({
tokenIn: tokenA.address,
tokenOut: tokenB.address,
amountIn: ethers.utils.parseEther("1"),
minAmountOut: ethers.utils.parseEther("0.95"),
routerType: 0,
to: user1.address,
deadline: Math.floor(Date.now() / 1000) - 300, // Past deadline
swapData: "0x"
})
).to.be.revertedWith("Expired");
});
});
describe("Slippage Protection", function () {
it("Should protect against front-running", async function () {
const { aggregator, tokenA, tokenB, user1 } = await loadFixture(deployAggregatorFixture);
const amountIn = ethers.utils.parseEther("100");
const minAmountOut = ethers.utils.parseEther("99"); // Very tight slippage
await tokenA.connect(user1).approve(aggregator.address, amountIn);
// This should fail due to slippage (mock router returns 95%)
await expect(
aggregator.connect(user1).executeSwap({
tokenIn: tokenA.address,
tokenOut: tokenB.address,
amountIn: amountIn,
minAmountOut: minAmountOut,
routerType: 0,
to: user1.address,
deadline: Math.floor(Date.now() / 1000) + 300,
swapData: "0x"
})
).to.be.revertedWith("Insufficient output amount");
});
});
describe("Gas Griefing Protection", function () {
it("Should limit gas consumption", async function () {
// Test that contract operations don't consume excessive gas
const { aggregator, tokenA, tokenB, user1 } = await loadFixture(deployAggregatorFixture);
const amountIn = ethers.utils.parseEther("1");
await tokenA.connect(user1).approve(aggregator.address, amountIn);
const gasEstimate = await aggregator.connect(user1).estimateGas.executeSwap({
tokenIn: tokenA.address,
tokenOut: tokenB.address,
amountIn: amountIn,
minAmountOut: ethers.utils.parseEther("0.95"),
routerType: 0,
to: user1.address,
deadline: Math.floor(Date.now() / 1000) + 300,
swapData: "0x"
});
expect(gasEstimate).to.be.lt(200000); // Reasonable gas limit
});
});
});
Performance Testing
Gas Optimization Tests
// test/performance/gas-optimization.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Gas Optimization Tests", function () {
let aggregator, tokenA, tokenB, user;
beforeEach(async function () {
// Setup contracts...
});
it("Should optimize gas for batch operations", async function () {
const batchSize = 10;
const amounts = Array(batchSize).fill(ethers.utils.parseEther("1"));
// Test individual operations
let totalGasIndividual = ethers.BigNumber.from(0);
for (let i = 0; i < batchSize; i++) {
const gasEstimate = await aggregator.estimateGas.executeSwap(/* params */);
totalGasIndividual = totalGasIndividual.add(gasEstimate);
}
// Test batch operation
const batchGasEstimate = await aggregator.estimateGas.executeBatchSwap(/* batch params */);
// Batch should be more efficient
expect(batchGasEstimate).to.be.lt(totalGasIndividual.mul(8).div(10)); // 20% savings
});
it("Should measure gas consumption across routers", async function () {
const amountIn = ethers.utils.parseEther("100");
const routerTypes = [0, 1, 10, 30]; // Different router types
const gasUsage = {};
for (const routerType of routerTypes) {
const gasEstimate = await aggregator.estimateGas.executeSwap({
tokenIn: tokenA.address,
tokenOut: tokenB.address,
amountIn: amountIn,
minAmountOut: ethers.utils.parseEther("95"),
routerType: routerType,
to: user.address,
deadline: Math.floor(Date.now() / 1000) + 300,
swapData: "0x"
});
gasUsage[routerType] = gasEstimate.toNumber();
}
console.log("Gas usage by router type:", gasUsage);
// Verify all are within reasonable bounds
Object.values(gasUsage).forEach(gas => {
expect(gas).to.be.lt(300000);
});
});
});
Continuous Integration
GitHub Actions Workflow
# .github/workflows/test.yml
name: Test Suite
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Compile contracts
run: npx hardhat compile
- name: Run unit tests
run: npx hardhat test test/unit/
env:
ETHEREUM_RPC_URL: ${{ secrets.ETHEREUM_RPC_URL }}
- name: Run integration tests
run: npx hardhat test test/integration/
env:
ETHEREUM_RPC_URL: ${{ secrets.ETHEREUM_RPC_URL }}
- name: Run security tests
run: npx hardhat test test/security/
- name: Generate coverage report
run: npx hardhat coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
- name: Contract size check
run: npx hardhat size-contracts
- name: Gas report
run: npx hardhat test --reporter gas
env:
REPORT_GAS: true
COINMARKETCAP_API_KEY: ${{ secrets.COINMARKETCAP_API_KEY }}
Test Utilities
Custom Matchers
// test/utils/matchers.js
const { expect } = require("chai");
// Custom matcher for checking token balances
expect.extend({
toChangeTokenBalance(received, token, accounts, expectedChanges) {
// Implementation for checking token balance changes
},
toBeWithinSlippage(received, expected, slippageBps) {
// Implementation for checking slippage tolerance
}
});
Test Helpers
// test/utils/helpers.js
const { ethers } = require("hardhat");
async function setupTokens(amount = "1000") {
const MockERC20 = await ethers.getContractFactory("MockERC20");
const tokenA = await MockERC20.deploy("Token A", "TKA", 18);
const tokenB = await MockERC20.deploy("Token B", "TKB", 18);
return { tokenA, tokenB };
}
async function fundAccount(token, account, amount) {
await token.mint(account.address, ethers.utils.parseEther(amount));
}
function getRandomAddress() {
return ethers.Wallet.createRandom().address;
}
module.exports = {
setupTokens,
fundAccount,
getRandomAddress
};
Resources
Last updated