Testing Guide
Comprehensive testing guide for developing and integrating with the CMX Protocol.
Testing Overview
The CMX Protocol uses a multi-layered testing approach:
Unit Tests - Individual contract function testing
Integration Tests - Cross-contract interaction testing
End-to-End Tests - Complete workflow testing
Security Tests - Vulnerability and exploit testing
Gas Optimization Tests - Performance and cost analysis
Prerequisites
Development Environment
# Install Foundry (required version 1.1.0+)
curl -L https://foundry.paradigm.xyz | bash
foundryup
# Install Node.js dependencies
npm install
# Install testing dependencies
npm install --save-dev @types/mocha @types/chai hardhat
Test Configuration
# Copy test environment
cp .env.test.example .env.test
# Configure test settings
FORK_BLOCK_NUMBER=latest
FORK_URL=https://sepolia.base.org
TEST_MNEMONIC="test test test test test test test test test test test junk"
Unit Testing
Foundry Unit Tests
Located in test/unit/
, these test individual contract functions in isolation.
Running Unit Tests
# Run all unit tests
forge test
# Run specific test file
forge test --match-path test/unit/AssetFactory.t.sol
# Run specific test function
forge test --match-test testCreateShareClass
# Run with verbosity
forge test -vvv
# Run with gas reporting
forge test --gas-report
Example Unit Test
// test/unit/AssetFactory.t.sol
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../../src/Tokenization/factory/AssetFactory.sol";
import "../../src/Authorization/gam/GlobalAccessManager.sol";
contract AssetFactoryTest is Test {
AssetFactory public factory;
GlobalAccessManager public accessManager;
address public admin = makeAddr("admin");
address public user = makeAddr("user");
function setUp() public {
vm.startPrank(admin);
accessManager = new GlobalAccessManager(admin);
factory = new AssetFactory(address(accessManager));
// Grant necessary roles
accessManager.grantRole(ASSET_CREATOR_ROLE, user, 0);
vm.stopPrank();
}
function testCreateShareClass() public {
vm.startPrank(user);
ShareClassConfig memory config = ShareClassConfig({
votingRights: true,
dividendRights: true,
transferRestrictions: true,
complianceRequired: true
});
address shareClass = factory.createShareClass(
"Example Corp Class A",
"EXMP-A",
1000000,
config
);
assertEq(factory.getAssetCount(), 1);
assertTrue(factory.isValidAsset(shareClass));
vm.stopPrank();
}
function testCreateShareClassUnauthorized() public {
vm.startPrank(makeAddr("unauthorized"));
ShareClassConfig memory config = ShareClassConfig({
votingRights: true,
dividendRights: true,
transferRestrictions: true,
complianceRequired: true
});
vm.expectRevert("AccessManager: account missing role");
factory.createShareClass(
"Example Corp Class A",
"EXMP-A",
1000000,
config
);
vm.stopPrank();
}
function testFuzzCreateShareClassTotalShares(uint256 totalShares) public {
vm.assume(totalShares > 0 && totalShares <= 1e18);
vm.startPrank(user);
ShareClassConfig memory config = ShareClassConfig({
votingRights: true,
dividendRights: true,
transferRestrictions: false,
complianceRequired: false
});
address shareClass = factory.createShareClass(
"Test Corp",
"TEST",
totalShares,
config
);
assertEq(IShareClass(shareClass).totalSupply(), totalShares);
vm.stopPrank();
}
}
Contract Interaction Tests
Testing Diamond Upgrades
// test/unit/DiamondUpgrade.t.sol
contract DiamondUpgradeTest is Test {
Diamond public diamond;
DiamondCutFacet public diamondCut;
TestFacet public testFacet;
function testAddFacet() public {
// Deploy new facet
testFacet = new TestFacet();
// Prepare diamond cut
FacetCut[] memory cuts = new FacetCut[](1);
cuts[0] = FacetCut({
facetAddress: address(testFacet),
action: FacetCutAction.Add,
functionSelectors: generateSelectors("TestFacet")
});
// Execute diamond cut
diamondCut.diamondCut(cuts, address(0), "");
// Verify facet was added
address[] memory facets = DiamondLoupeFacet(address(diamond)).facets();
assertTrue(contains(facets, address(testFacet)));
}
}
Integration Testing
Cross-Contract Integration Tests
Located in test/integration/
, these test complete workflows across multiple contracts.
Example Integration Test
// test/integration/AssetCreationFlow.t.sol
contract AssetCreationFlowTest is Test {
// All contracts needed for the flow
AssetFactory public assetFactory;
AttestationRegistry public attestationRegistry;
DocumentRegistry public documentRegistry;
GlobalAccessManager public accessManager;
function testCompleteAssetCreationFlow() public {
// 1. Set up KYC attestation
bytes32 kycSchema = attestationRegistry.createSchema(
"address verified, uint256 level, uint256 expiry",
true
);
// 2. Create KYC attestation for user
bytes32 attestationId = attestationRegistry.attest(
kycSchema,
user,
abi.encode(user, 2, block.timestamp + 365 days)
);
// 3. Register asset documentation
bytes32 documentHash = documentRegistry.registerDocument(
keccak256("asset-prospectus"),
"QmProspectusHash",
DocumentType.PROSPECTUS
);
// 4. Create tokenized asset
vm.startPrank(user);
address asset = assetFactory.createShareClass(
"Example Corp Class A",
"EXMP-A",
1000000,
ShareClassConfig({
votingRights: true,
dividendRights: true,
transferRestrictions: true,
complianceRequired: true
})
);
vm.stopPrank();
// 5. Verify complete setup
assertTrue(assetFactory.isValidAsset(asset));
assertTrue(attestationRegistry.isValidAttestation(attestationId));
assertTrue(documentRegistry.isValidDocument(documentHash));
// 6. Test compliant transfer
vm.prank(user);
IShareClass(asset).transfer(anotherUser, 1000);
assertEq(IShareClass(asset).balanceOf(anotherUser), 1000);
}
}
Fund Management Integration
// test/integration/FundWorkflow.t.sol
contract FundWorkflowTest is Test {
function testCompleteFundWorkflow() public {
// 1. Create investment fund
address fund = fundFactory.createUniversalFund(
"Growth Equity Fund I",
"GEF-I",
FundConfig({
managementFee: 200, // 2%
performanceFee: 2000, // 20%
lockupPeriod: 365 days,
minimumInvestment: 1000000e6 // $1M USDC
})
);
// 2. Investor deposits
vm.startPrank(investor);
usdc.approve(fund, 5000000e6); // $5M USDC
uint256 shares = IUniversalFund(fund).deposit(5000000e6);
vm.stopPrank();
// 3. Fund makes investments
vm.startPrank(fundManager);
IUniversalFund(fund).invest(
assetAddress,
2000000e6 // $2M investment
);
vm.stopPrank();
// 4. Time passes and performance occurs
vm.warp(block.timestamp + 365 days);
// Simulate 20% gain
vm.mockCall(
priceOracle,
abi.encodeWithSelector(IPriceOracle.getPrice.selector, assetAddress),
abi.encode(2400000e6) // $2.4M value
);
// 5. Calculate and distribute fees
IUniversalFund(fund).calculateNAV();
IUniversalFund(fund).processPerformanceFees();
// 6. Investor redeems
vm.startPrank(investor);
uint256 redemptionAmount = IUniversalFund(fund).withdraw(shares);
vm.stopPrank();
// Verify net performance after fees
assertGt(redemptionAmount, 5000000e6); // Positive return
assertLt(redemptionAmount, 6000000e6); // Reasonable after fees
}
}
End-to-End Testing
Full System Tests
Trading Workflow Test
// test/e2e/TradingWorkflow.t.sol
contract TradingWorkflowTest is Test {
function testCompleteSecuritiesTrading() public {
// 1. Set up investors with KYC
setupInvestorWithKYC(investor1);
setupInvestorWithKYC(investor2);
// 2. Company creates share class
address shareClass = createCompanyShares();
// 3. Company conducts securities offering
address offering = createRule506bOffering(shareClass);
// 4. Investors participate in offering
participateInOffering(offering, investor1, 1000000e6);
participateInOffering(offering, investor2, 500000e6);
// 5. Offering closes and shares are distributed
closeOffering(offering);
// 6. Secondary trading begins
address market = createSecondaryMarket(shareClass);
// 7. Place and execute trades
uint256 sellOrderId = placeSellOrder(market, investor1, 1000, 105e6);
uint256 buyOrderId = placeBuyOrder(market, investor2, 500, 105e6);
// 8. Verify trade execution
assertEq(IShareClass(shareClass).balanceOf(investor1),
initialBalance - 500); // Sold 500 shares
assertEq(IShareClass(shareClass).balanceOf(investor2),
initialBalance + 500); // Bought 500 shares
// 9. Dividend distribution
distributeDividend(shareClass, 10e6); // $10 per share
// 10. Governance voting
uint256 proposalId = createGovernanceProposal(shareClass);
vote(proposalId, investor1, true);
vote(proposalId, investor2, false);
executeProposal(proposalId);
}
function setupInvestorWithKYC(address investor) internal {
// Create KYC attestation
vm.startPrank(kycProvider);
bytes32 attestationId = attestationRegistry.attest(
kycSchemaId,
investor,
abi.encode(investor, 2, block.timestamp + 365 days)
);
vm.stopPrank();
// Verify attestation
assertTrue(attestationRegistry.isValidAttestation(attestationId));
}
}
Security Testing
Vulnerability Tests
Access Control Tests
// test/security/AccessControl.t.sol
contract AccessControlTest is Test {
function testUnauthorizedAccess() public {
address attacker = makeAddr("attacker");
// Try to create asset without permission
vm.startPrank(attacker);
vm.expectRevert("AccessManager: account missing role");
assetFactory.createShareClass("Hack Corp", "HACK", 1000000, config);
vm.stopPrank();
// Try to upgrade diamond without permission
vm.startPrank(attacker);
vm.expectRevert("AccessManager: account missing role");
diamondCut.diamondCut(maliciousCuts, address(0), "");
vm.stopPrank();
}
function testPrivilegeEscalation() public {
// User with limited role tries to grant themselves admin
vm.startPrank(limitedUser);
vm.expectRevert("AccessManager: account missing role");
accessManager.grantRole(ADMIN_ROLE, limitedUser, 0);
vm.stopPrank();
}
}
Reentrancy Tests
// test/security/Reentrancy.t.sol
contract ReentrancyTest is Test {
function testReentrancyProtection() public {
// Deploy malicious contract
MaliciousReentrant attacker = new MaliciousReentrant(address(fund));
// Attempt reentrancy attack
vm.expectRevert("ReentrancyGuard: reentrant call");
attacker.attack();
}
}
contract MaliciousReentrant {
IUniversalFund public target;
constructor(address _target) {
target = IUniversalFund(_target);
}
function attack() external {
target.deposit(1000e6);
target.withdraw(target.balanceOf(address(this)));
}
// Attempt to reenter during withdraw
receive() external payable {
if (address(target).balance > 0) {
target.withdraw(target.balanceOf(address(this)));
}
}
}
Fuzzing Tests
Property-Based Testing
// test/security/Fuzzing.t.sol
contract FuzzingTest is Test {
function testFuzzTransferInvariants(
address from,
address to,
uint256 amount
) public {
vm.assume(from != to);
vm.assume(from != address(0) && to != address(0));
vm.assume(amount > 0 && amount <= shareClass.balanceOf(from));
uint256 fromBalanceBefore = shareClass.balanceOf(from);
uint256 toBalanceBefore = shareClass.balanceOf(to);
uint256 totalSupplyBefore = shareClass.totalSupply();
vm.prank(from);
shareClass.transfer(to, amount);
// Invariants
assertEq(shareClass.balanceOf(from), fromBalanceBefore - amount);
assertEq(shareClass.balanceOf(to), toBalanceBefore + amount);
assertEq(shareClass.totalSupply(), totalSupplyBefore);
}
function testFuzzOrderBookInvariants(
uint256 price,
uint256 amount
) public {
vm.assume(price > 0 && price <= 1000e6); // Reasonable price range
vm.assume(amount > 0 && amount <= 1000000); // Reasonable amount
uint256 orderId = orderBook.placeBuyOrder(asset, amount, price);
// Invariants
assertTrue(orderBook.isValidOrder(orderId));
assertEq(orderBook.getOrderPrice(orderId), price);
assertEq(orderBook.getOrderAmount(orderId), amount);
}
}
Gas Optimization Testing
Gas Usage Analysis
Gas Benchmarking
// test/gas/GasBenchmark.t.sol
contract GasBenchmarkTest is Test {
function testAssetCreationGas() public {
uint256 gasStart = gasleft();
address asset = assetFactory.createShareClass(
"Gas Test Corp",
"GAS",
1000000,
defaultConfig
);
uint256 gasUsed = gasStart - gasleft();
// Assert reasonable gas usage
assertLt(gasUsed, 500000); // Less than 500k gas
emit log_named_uint("Asset creation gas", gasUsed);
}
function testBatchOperationsGas() public {
// Single operations
uint256 gasStart = gasleft();
for (uint i = 0; i < 10; i++) {
shareClass.transfer(makeAddr(string(abi.encode(i))), 100);
}
uint256 singleOpsGas = gasStart - gasleft();
// Batch operation
gasStart = gasleft();
address[] memory recipients = new address[](10);
uint256[] memory amounts = new uint256[](10);
for (uint i = 0; i < 10; i++) {
recipients[i] = makeAddr(string(abi.encode(i + 10)));
amounts[i] = 100;
}
shareClass.batchTransfer(recipients, amounts);
uint256 batchOpsGas = gasStart - gasleft();
// Batch should be more efficient
assertLt(batchOpsGas, singleOpsGas);
emit log_named_uint("Single operations gas", singleOpsGas);
emit log_named_uint("Batch operations gas", batchOpsGas);
emit log_named_uint("Gas savings", singleOpsGas - batchOpsGas);
}
}
Hardhat Integration Tests
For JavaScript/TypeScript testing alongside Solidity tests.
Setup
// hardhat.config.ts
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-foundry";
const config: HardhatUserConfig = {
solidity: "0.8.20",
networks: {
hardhat: {
forking: {
url: process.env.BASE_SEPOLIA_URL || "",
},
},
},
};
export default config;
JavaScript Integration Tests
// test/integration/AssetManagement.test.ts
import { expect } from "chai";
import { ethers } from "hardhat";
import { CMXClient, Network } from "@capsign/sdk";
describe("Asset Management Integration", function () {
let client: CMXClient;
let deployer: any;
let user1: any;
let user2: any;
beforeEach(async function () {
[deployer, user1, user2] = await ethers.getSigners();
client = new CMXClient({
network: Network.HARDHAT,
signer: deployer,
});
});
it("Should create and transfer assets", async function () {
// Create asset
const asset = await client.assets.createShareClass({
name: "Test Corp",
symbol: "TEST",
totalShares: 1000000,
votingRights: true,
dividendRights: true,
});
expect(asset.address).to.match(/^0x[a-fA-F0-9]{40}$/);
// Transfer assets
await client.assets.transfer({
assetAddress: asset.address,
to: user1.address,
amount: ethers.utils.parseEther("1000"),
});
const balance = await client.assets.getBalance(
asset.address,
user1.address
);
expect(balance).to.equal(ethers.utils.parseEther("1000"));
});
it("Should handle compliance checks", async function () {
// Create asset with compliance requirements
const asset = await client.assets.createShareClass({
name: "Compliance Corp",
symbol: "COMP",
totalShares: 1000000,
complianceRequired: true,
});
// Attempt transfer without KYC (should fail)
await expect(
client.assets.transfer({
assetAddress: asset.address,
to: user1.address,
amount: ethers.utils.parseEther("1000"),
})
).to.be.revertedWith("Compliance check failed");
// Add KYC attestation
await client.compliance.createKYCAttestation({
subject: user1.address,
verificationLevel: "BASIC",
});
// Now transfer should succeed
await client.assets.transfer({
assetAddress: asset.address,
to: user1.address,
amount: ethers.utils.parseEther("1000"),
});
const balance = await client.assets.getBalance(
asset.address,
user1.address
);
expect(balance).to.equal(ethers.utils.parseEther("1000"));
});
});
Test Data Management
Test Fixtures
// test/utils/Fixtures.sol
library Fixtures {
struct TestAsset {
address asset;
string name;
string symbol;
uint256 totalSupply;
}
function createBasicShareClass(
AssetFactory factory,
address creator
) internal returns (TestAsset memory) {
vm.startPrank(creator);
address asset = factory.createShareClass(
"Test Corporation",
"TEST",
1000000,
ShareClassConfig({
votingRights: true,
dividendRights: true,
transferRestrictions: false,
complianceRequired: false
})
);
vm.stopPrank();
return TestAsset({
asset: asset,
name: "Test Corporation",
symbol: "TEST",
totalSupply: 1000000
});
}
function createKYCdInvestor(
AttestationRegistry registry,
address kycProvider,
address investor
) internal returns (bytes32) {
vm.startPrank(kycProvider);
bytes32 attestationId = registry.attest(
kycSchemaId,
investor,
abi.encode(investor, 2, block.timestamp + 365 days)
);
vm.stopPrank();
return attestationId;
}
}
Continuous Integration
GitHub Actions Workflow
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
- name: Run unit tests
run: forge test --match-path "test/unit/*"
- name: Generate gas report
run: forge test --gas-report
integration-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
- name: Run integration tests
run: forge test --match-path "test/integration/*"
security-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
- name: Run security tests
run: forge test --match-path "test/security/*"
- name: Run fuzzing tests
run: forge test --match-path "test/security/*" --fuzz-runs 10000
Best Practices
Test Organization
Unit tests: Focus on individual function behavior
Integration tests: Test cross-contract interactions
End-to-end tests: Test complete user workflows
Security tests: Test for vulnerabilities and edge cases
Test Data
Use deterministic test data for consistent results
Create reusable fixtures for common test scenarios
Use fuzzing for edge case discovery
Mock external dependencies for isolated testing
Performance
Optimize test execution time with parallel running
Use caching for compiled contracts
Minimize redundant setup in test suites
Profile gas usage for optimization opportunities
This comprehensive testing guide ensures robust, secure, and efficient development of CMX Protocol integrations.
Last updated
Was this helpful?