Files
Mohamad HammoudandGitHub 94182deb9c ✨ BT-1418 integrate foundry & hardhat + migrating solidity tests (#76)
* ✨ integrate foundry alongside hardhat for dual testing framework

- Add foundry.toml configuration
- Reorganize test structure: test/forge/ and test/hardhat/
- Add Foundry scripts to package.json (build:forge, test:forge, coverage:forge)
- Update hardhat.config.ts with evmVersion for compatibility
- Add GitHub workflows and coverage scripts from template
- Add Husky hooks (pre-commit, post-checkout, post-merge)
- Add .vscode/settings.json for better IDE support
- Disable coverage checks until tests are migrated (commented with TODO)

This migration is based on TokenySolutions/contracts-template and enables
gradual migration of tests from Hardhat (TypeScript) to Foundry (Solidity).

Related to: BT-1418

* ✅ trex factory tests

* ⚰️ remove trex suit

* feat(forge): reorganize Foundry test structure and sync GitHub workflows

- Move TREXFactorySetup to helpers folder
- Rename TREXSuite.t.sol to TREXFactory.t.sol
- Update fixture to inherit from Test for proper Foundry pattern
- Transfer ownership to deployer address to match Hardhat behavior
- Sync GitHub workflows with package.json scripts
- Update test.yml to use npm run build and npm run test
- Update push_checking.yml to use unified npm run lint
- Update forge_coverage.sh to use npm run coverage

* fix(ci): install Foundry and skip husky in push_checking workflow

- Add Foundry installation step before npm ci
- Set HUSKY=0 to skip husky setup in CI environment
- Fixes 'forge: not found' error during npm prepare script

* 🔧 keep hardhat settings

* ✅ create addressess directly

* ✅ removed test_StandardAddressesSet

* ✅ fix lint

* 💡 removed title natspecs

* ✅ migrated trex factort tests

* ✅ added test module multicall

* ✅ migrate agent role test

* ✅ migrate claim topics registry

* ✅ migrated trusted issuer registry

* ✅ migrate identity registry storage

* ✅ migrated identity registry tests

* ✅ migrated implementation authority test

* ✅ migrated token agen restrictions

* ✅ migrated token information test

* ✅ migrated token recover

* ✅ migrated token permit

* ✅ migrated token transfer

* ✅ migrated utilities upgrade

* ✅ migrated freeze check

* ✅ migrated compliance check in utilities test

* ✅ migrated eligibility check

* ✅ migrated transfer check in utilities

* ✅ migrated compliance test

* ✅ migrated gateway tests

* 🔥 removed already defined events

* 🗑️ imported Initializable error from openzeppelin

* ➕ added already defined events and errors

* ✅ added base test token

* ✅ used idFactory

* ♻️ refactored compliance test

* ⚰️ remoced useless vm.prank

* 🔧 deleted hardhat configurations

* 💚 fixed ci

* refactor: remove 100% coverage requirement from forge_coverage.sh

- Remove strict 100% coverage checks that were causing workflow failures
- Script now only generates and displays coverage report without failing
- Aligns with previous behavior where coverage was informational only

* ✅ resolve getVerifiedDetails

* ✅ applied _getEligibility and remove via-ir from foundry.toml

* ✅ cover tokena and token permit

* ✅ cover compliance

* ✅ cover identity registry

* ✅ increased trex gateway coverage

* ✅ increase coverage for trex factory

* ✅ increased AbstractModuleUpgradeable.sol coverage

* ⚰️ removed abstract module

* 🔧 updted generate coverage

* ✅ coverage 100% identity registry

* ✅ coverage 100% trex gate way

* ✅ coverage trex factory

* ✅ coverage 100% AbstractModuleUpgradeable by test module

* ✅ coverage IAFactory.sol

* ✅ coverage 100% for implementation authority

* ✅ coverage 1005 abstract proxy

* ✅ coverage 100% claim topic registry

* ✅ coverage 100% identity registry storage

* ✅ coverage 100% modular compliance proxy

* ✅ coverage trusted issuer registry proxy

* ✅ coverage 100% identity registry

* ✅ coverage 100% token proxy

* 🚨 resolve linter in token information

* ✅ coverage 100% roles

* 🚨 resolve linter

* 🔧 added no coverage for test, _testContracts and  utils

* ✅ coverage 100% for token.sol

* ✅ coverage 100% for modular compliance

* ✅ coverage 100% trusted issuer registry

* 🔧 restore package.json

* ✅ coverage 100% for utlity checker

* 🚨 fix linter
2025-12-31 16:58:16 +08:00

955 lines
39 KiB
Solidity

// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.30;
import { IIdentity } from "@onchain-id/solidity/contracts/interface/IIdentity.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import { IERC3643 } from "contracts/ERC-3643/IERC3643.sol";
import { ComplianceAdded } from "contracts/ERC-3643/IERC3643.sol";
import { IERC3643Compliance } from "contracts/ERC-3643/IERC3643Compliance.sol";
import { TokenBound, TokenUnbound } from "contracts/ERC-3643/IERC3643Compliance.sol";
import { IERC3643IdentityRegistry } from "contracts/ERC-3643/IERC3643IdentityRegistry.sol";
import { MockContract } from "contracts/_testContracts/MockContract.sol";
import { ModuleNotPnP } from "contracts/_testContracts/ModuleNotPnP.sol";
import { TestModule } from "contracts/_testContracts/TestModule.sol";
import { IModularCompliance } from "contracts/compliance/modular/IModularCompliance.sol";
import { ModuleAdded, ModuleInteraction, ModuleRemoved } from "contracts/compliance/modular/IModularCompliance.sol";
import { ModularCompliance } from "contracts/compliance/modular/ModularCompliance.sol";
import {
MaxModulesReached,
ModuleAlreadyBound,
ModuleNotBound,
OnlyOwnerOrTokenCanCall,
TokenNotBound
} from "contracts/compliance/modular/ModularCompliance.sol";
import { ModuleProxy } from "contracts/compliance/modular/modules/ModuleProxy.sol";
import { ArraySizeLimited, OwnableUnauthorizedAccount } from "contracts/errors/CommonErrors.sol";
import { InitializationFailed } from "contracts/errors/CommonErrors.sol";
import {
AddressNotATokenBoundToComplianceContract,
ComplianceNotSuitableForBindingToModule
} from "contracts/errors/ComplianceErrors.sol";
import { ZeroAddress } from "contracts/errors/InvalidArgumentErrors.sol";
import { ZeroValue } from "contracts/errors/InvalidArgumentErrors.sol";
import { ZeroAddress } from "contracts/errors/InvalidArgumentErrors.sol";
import { ITREXFactory } from "contracts/factory/ITREXFactory.sol";
import { ModularComplianceProxy } from "contracts/proxy/ModularComplianceProxy.sol";
import { ITREXImplementationAuthority } from "contracts/proxy/authority/ITREXImplementationAuthority.sol";
import { TREXImplementationAuthority } from "contracts/proxy/authority/TREXImplementationAuthority.sol";
import { IdentityRegistry } from "contracts/registry/implementation/IdentityRegistry.sol";
import { OwnableOnceNext2StepUpgradeable } from "contracts/roles/OwnableOnceNext2StepUpgradeable.sol";
import { OwnershipTransferStarted } from "contracts/roles/OwnableOnceNext2StepUpgradeable.sol";
import { Token } from "contracts/token/Token.sol";
import { InterfaceIdCalculator } from "contracts/utils/InterfaceIdCalculator.sol";
import { IdentityFactoryHelper } from "test/helpers/IdentityFactoryHelper.sol";
import { TREXFactorySetup } from "test/helpers/TREXFactorySetup.sol";
contract ComplianceTest is TREXFactorySetup {
ModularCompliance public compliance;
ModularCompliance public complianceBeta;
Token public token;
address public tokenAgent = makeAddr("tokenAgent");
/// @notice Helper to deploy TestModule with proxy
function _deployTestModuleWithProxy() internal returns (address moduleAddress) {
TestModule moduleImplementation = new TestModule();
bytes memory initData = abi.encodeWithSelector(TestModule.initialize.selector);
ModuleProxy moduleProxy = new ModuleProxy(address(moduleImplementation), initData);
moduleAddress = address(moduleProxy);
}
/// @notice Helper to deploy ModuleNotPnP with proxy
function _deployModuleNotPnPWithProxy() internal returns (address moduleAddress) {
ModuleNotPnP moduleImplementation = new ModuleNotPnP();
bytes memory initData = abi.encodeWithSelector(ModuleNotPnP.initialize.selector);
ModuleProxy moduleProxy = new ModuleProxy(address(moduleImplementation), initData);
moduleAddress = address(moduleProxy);
}
/// @notice Helper to deploy ModularCompliance with proxy
function _deployModularComplianceWithProxy(address implementationAuthority) internal returns (ModularCompliance) {
ModularComplianceProxy proxy = new ModularComplianceProxy(implementationAuthority);
return ModularCompliance(address(proxy));
}
function setUp() public override {
super.setUp();
// Deploy token suite
token = _deployToken("salt");
// Deploy two compliance contracts
compliance = _deployModularComplianceWithProxy(address(getTREXImplementationAuthority()));
complianceBeta = _deployModularComplianceWithProxy(address(getTREXImplementationAuthority()));
// Transfer ownership of compliance contracts to deployer (they're owned by test contract initially)
vm.startPrank(address(this));
compliance.transferOwnership(deployer);
complianceBeta.transferOwnership(deployer);
vm.stopPrank();
// Bind compliance to token
vm.prank(deployer);
token.setCompliance(address(compliance));
// Add tokenAgent as an agent to Token and IdentityRegistry (for burn operations)
IERC3643IdentityRegistry ir = token.identityRegistry();
vm.startPrank(deployer);
token.addAgent(tokenAgent);
IdentityRegistry(address(ir)).addAgent(tokenAgent);
vm.stopPrank();
// Register alice and bob identities and mint tokens (for tests that need them)
vm.startPrank(tokenAgent);
IdentityRegistry(address(ir)).registerIdentity(alice, aliceIdentity, 42);
IdentityRegistry(address(ir)).registerIdentity(bob, bobIdentity, 666);
token.mint(alice, 1000);
token.mint(bob, 500);
vm.stopPrank();
}
// ============================================
// .init Tests
// ============================================
/// @notice Should prevent calling init twice
function test_init_RevertWhen_CalledTwice() public {
vm.expectRevert(Initializable.InvalidInitialization.selector);
compliance.init();
}
// ============================================
// Constructor Tests
// ============================================
/// @notice Should revert when implementation authority is zero address
function test_constructor_RevertWhen_ImplementationAuthorityZeroAddress() public {
vm.expectRevert(ZeroAddress.selector);
new ModularComplianceProxy(address(0));
}
/// @notice Should revert when initialization fails (invalid implementation)
function test_constructor_RevertWhen_InitializationFails() public {
// Deploy a mock contract that doesn't have init() function
MockContract mockImpl = new MockContract();
// Deploy an IA and manually set an invalid MC implementation
TREXImplementationAuthority incompleteIA = new TREXImplementationAuthority(true, address(0), address(0));
// Create a version with invalid MC implementation (mock contract without init())
ITREXImplementationAuthority.Version memory version =
ITREXImplementationAuthority.Version({ major: 4, minor: 0, patch: 0 });
ITREXImplementationAuthority.TREXContracts memory contracts = ITREXImplementationAuthority.TREXContracts({
tokenImplementation: address(mockImpl), // Invalid - doesn't have proper init
ctrImplementation: address(mockImpl), // Invalid
irImplementation: address(mockImpl), // Invalid
irsImplementation: address(mockImpl), // Invalid
tirImplementation: address(mockImpl), // Invalid
mcImplementation: address(mockImpl) // Invalid - doesn't have init() function
});
// Add version to IA (need to be owner)
Ownable(address(incompleteIA)).transferOwnership(deployer);
vm.prank(deployer);
incompleteIA.addAndUseTREXVersion(version, contracts);
// Now try to deploy proxy - delegatecall to mockImpl.init() will fail
// because MockContract doesn't have init() function, causing InitializationFailed() revert
vm.expectRevert(InitializationFailed.selector);
new ModularComplianceProxy(address(incompleteIA));
}
// ============================================
// .bindToken Tests
// ============================================
/// @notice Should revert when calling as another account than the owner
function test_bindToken_RevertWhen_CalledByNonOwner() public {
vm.prank(another);
vm.expectRevert(OnlyOwnerOrTokenCanCall.selector);
compliance.bindToken(address(token));
}
/// @notice Should revert when compliance is already bound and caller is not token
function test_bindToken_RevertWhen_AlreadyBoundAndNotToken() public {
// Deploy new compliance and bind it to token
ModularCompliance newCompliance = _deployModularComplianceWithProxy(address(getTREXImplementationAuthority()));
newCompliance.transferOwnership(deployer);
vm.prank(deployer);
newCompliance.bindToken(address(token));
// Try to bind again as another account
vm.prank(another);
vm.expectRevert(OnlyOwnerOrTokenCanCall.selector);
newCompliance.bindToken(address(token));
}
/// @notice Should set the new compliance when calling as the token
function test_bindToken_Success_WhenCalledByToken() public {
// Deploy a fresh token suite (without compliance bound)
Token testToken = _deployToken("salt2");
// Deploy a compliance and bind it
ModularCompliance compliance = _deployModularComplianceWithProxy(address(getTREXImplementationAuthority()));
compliance.transferOwnership(deployer);
vm.prank(deployer);
compliance.bindToken(address(testToken));
// Deploy new compliance (not bound yet)
ModularCompliance newCompliance = _deployModularComplianceWithProxy(address(getTREXImplementationAuthority()));
newCompliance.transferOwnership(deployer);
// Verify new compliance is not bound yet
assertFalse(newCompliance.isTokenBound(address(testToken)));
{
// Token sets new compliance (this should bind it)
// Event order: TokenBound (new), then the second event ComplianceAdded (token)
vm.expectEmit(true, false, false, false, address(newCompliance));
emit TokenBound(address(testToken));
vm.expectEmit(true, false, false, false, address(testToken));
emit ComplianceAdded(address(newCompliance));
vm.prank(deployer);
testToken.setCompliance(address(newCompliance));
}
assertTrue(newCompliance.isTokenBound(address(testToken)));
}
/// @notice Should revert when token address is zero
function test_bindToken_RevertWhen_TokenAddressIsZero() public {
ModularCompliance newCompliance = _deployModularComplianceWithProxy(address(getTREXImplementationAuthority()));
newCompliance.transferOwnership(deployer);
vm.prank(deployer);
vm.expectRevert(ZeroAddress.selector);
newCompliance.bindToken(address(0));
}
// ============================================
// .unbindToken Tests
// ============================================
/// @notice Should revert when calling as another account
function test_unbindToken_RevertWhen_CalledByNonOwner() public {
vm.prank(another);
vm.expectRevert(OnlyOwnerOrTokenCanCall.selector);
compliance.unbindToken(address(token));
}
/// @notice Should revert when token is zero address
function test_unbindToken_RevertWhen_TokenIsZeroAddress() public {
ModularCompliance newCompliance = _deployModularComplianceWithProxy(address(getTREXImplementationAuthority()));
newCompliance.transferOwnership(deployer);
vm.prank(deployer);
vm.expectRevert(ZeroAddress.selector);
newCompliance.unbindToken(address(0));
}
/// @notice Should revert when token is not bound
function test_unbindToken_RevertWhen_TokenNotBound() public {
ModularCompliance newCompliance = _deployModularComplianceWithProxy(address(getTREXImplementationAuthority()));
newCompliance.transferOwnership(deployer);
vm.prank(deployer);
vm.expectRevert(TokenNotBound.selector);
newCompliance.unbindToken(address(token));
}
/// @notice Should bind the new compliance to the token when called as token
function test_unbindToken_Success_WhenCalledByToken() public {
// Ensure compliance is bound to token first (matching Hardhat test)
vm.prank(deployer);
token.setCompliance(address(compliance));
// Set new compliance (this triggers unbind on old compliance)
// Event order: TokenUnbound (old) -> TokenBound (new) -> ComplianceAdded (token)
vm.expectEmit(true, false, false, false, address(compliance));
emit TokenUnbound(address(token));
vm.expectEmit(true, false, false, false, address(complianceBeta));
emit TokenBound(address(token));
vm.expectEmit(true, false, false, false, address(token));
emit ComplianceAdded(address(complianceBeta));
vm.prank(deployer);
token.setCompliance(address(complianceBeta));
assertEq(complianceBeta.getTokenBound(), address(token));
assertFalse(compliance.isTokenBound(address(token)));
}
// ============================================
// .addModule Tests
// ============================================
/// @notice Should revert when not calling as the owner
function test_addModule_RevertWhen_NotOwner() public {
vm.prank(another);
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, another));
compliance.addModule(address(0));
}
/// @notice Should revert when module address is zero
function test_addModule_RevertWhen_ModuleAddressIsZero() public {
vm.prank(deployer);
vm.expectRevert(ZeroAddress.selector);
compliance.addModule(address(0));
}
/// @notice Should revert when module address is already bound
function test_addModule_RevertWhen_ModuleAlreadyBound() public {
address moduleAddress = _deployTestModuleWithProxy();
vm.prank(deployer);
compliance.addModule(moduleAddress);
vm.prank(deployer);
vm.expectRevert(ModuleAlreadyBound.selector);
compliance.addModule(moduleAddress);
}
/// @notice Should revert when module is not plug & play and compliance is not suitable
function test_addModule_RevertWhen_ModuleNotPnPAndNotSuitable() public {
vm.prank(deployer);
compliance.bindToken(address(token));
address moduleAddress = _deployModuleNotPnPWithProxy();
ModuleNotPnP module = ModuleNotPnP(moduleAddress);
vm.prank(deployer);
vm.expectRevert(abi.encodeWithSelector(ComplianceNotSuitableForBindingToModule.selector, moduleAddress));
compliance.addModule(moduleAddress);
}
/// @notice Should bind when module is not plug & play but compliance is suitable
function test_addModule_Success_WhenModuleNotPnPAndSuitable() public {
vm.prank(deployer);
compliance.bindToken(address(token));
// Burn tokens to make compliance suitable
vm.prank(tokenAgent);
token.burn(alice, 1000);
vm.prank(tokenAgent);
token.burn(bob, 500);
address moduleAddress = _deployModuleNotPnPWithProxy();
ModuleNotPnP module = ModuleNotPnP(moduleAddress);
// Set module as ready for this compliance
vm.prank(deployer);
module.setModuleReady(address(compliance), true);
vm.expectEmit(true, false, false, false, address(compliance));
emit ModuleAdded(moduleAddress);
vm.prank(deployer);
compliance.addModule(moduleAddress);
address[] memory modules = compliance.getModules();
assertEq(modules.length, 1);
assertEq(modules[0], moduleAddress);
}
/// @notice Should add the module when module is plug & play
function test_addModule_Success_WhenModuleIsPlugAndPlay() public {
address moduleAddress = _deployTestModuleWithProxy();
vm.expectEmit(true, false, false, false, address(compliance));
emit ModuleAdded(moduleAddress);
vm.prank(deployer);
compliance.addModule(moduleAddress);
address[] memory modules = compliance.getModules();
assertEq(modules.length, 1);
assertEq(modules[0], moduleAddress);
}
/// @notice Should revert when attempting to bind a 25th module
function test_addModule_RevertWhen_MaxModulesReached() public {
// Add 24 modules (max is 25, so 24 + 1 more = 25)
for (uint256 i = 0; i < 24; i++) {
address moduleAddress = _deployTestModuleWithProxy();
vm.prank(deployer);
compliance.addModule(moduleAddress);
}
// Add 25th module
address module25 = _deployTestModuleWithProxy();
vm.prank(deployer);
compliance.addModule(module25);
// Try to add 26th module (should revert)
address module26 = _deployTestModuleWithProxy();
vm.prank(deployer);
vm.expectRevert(abi.encodeWithSelector(MaxModulesReached.selector, 25));
compliance.addModule(module26);
}
// ============================================
// .removeModule Tests
// ============================================
/// @notice Should revert when not calling as owner
function test_removeModule_RevertWhen_NotOwner() public {
vm.prank(another);
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, another));
compliance.removeModule(address(0));
}
/// @notice Should revert when module address is zero
function test_removeModule_RevertWhen_ModuleAddressIsZero() public {
vm.prank(deployer);
vm.expectRevert(ZeroAddress.selector);
compliance.removeModule(address(0));
}
/// @notice Should revert when module address is not bound
function test_removeModule_RevertWhen_ModuleNotBound() public {
address moduleAddress = _deployTestModuleWithProxy();
vm.prank(deployer);
vm.expectRevert(ModuleNotBound.selector);
compliance.removeModule(moduleAddress);
}
/// @notice Should remove the module when module is bound
function test_removeModule_Success_WhenModuleBound() public {
address moduleA = _deployTestModuleWithProxy();
address moduleB = _deployTestModuleWithProxy();
vm.prank(deployer);
compliance.addModule(moduleA);
vm.prank(deployer);
compliance.addModule(moduleB);
vm.expectEmit(true, false, false, false, address(compliance));
emit ModuleRemoved(moduleB);
vm.prank(deployer);
compliance.removeModule(moduleB);
assertFalse(compliance.isModuleBound(moduleB));
}
// ============================================
// .transferred Tests
// ============================================
/// @notice Should revert when not calling as a bound token
function test_transferred_RevertWhen_NotBoundToken() public {
vm.prank(another);
vm.expectRevert(AddressNotATokenBoundToComplianceContract.selector);
compliance.transferred(address(0), address(0), 0);
}
/// @notice Should revert when from address is null
function test_transferred_RevertWhen_FromAddressIsZero() public {
Token testToken = _setupComplianceBoundToWallet();
vm.prank(address(testToken));
vm.expectRevert(ZeroAddress.selector);
compliance.transferred(address(0), bob, 10);
}
/// @notice Should revert when to address is null
function test_transferred_RevertWhen_ToAddressIsZero() public {
Token testToken = _setupComplianceBoundToWallet();
vm.prank(address(testToken));
vm.expectRevert(ZeroAddress.selector);
compliance.transferred(alice, address(0), 10);
}
/// @notice Should revert when amount is zero
function test_transferred_RevertWhen_AmountIsZero() public {
Token testToken = _setupComplianceBoundToWallet();
vm.prank(address(testToken));
vm.expectRevert(ZeroValue.selector);
compliance.transferred(alice, bob, 0);
}
/// @notice Should update the modules when amount is greater than zero
function test_transferred_Success_WhenAmountGreaterThanZero() public {
Token testToken = _setupComplianceBoundToWallet();
vm.prank(address(testToken));
compliance.transferred(alice, bob, 10);
}
// ============================================
// .created Tests
// ============================================
/// @notice Should revert when not calling as a bound token
function test_created_RevertWhen_NotBoundToken() public {
vm.prank(another);
vm.expectRevert(AddressNotATokenBoundToComplianceContract.selector);
compliance.created(address(0), 0);
}
/// @notice Should revert when to address is null
function test_created_RevertWhen_ToAddressIsZero() public {
Token testToken = _setupComplianceBoundToWallet();
vm.prank(address(testToken));
vm.expectRevert(ZeroAddress.selector);
compliance.created(address(0), 10);
}
/// @notice Should revert when amount is zero
function test_created_RevertWhen_AmountIsZero() public {
Token testToken = _setupComplianceBoundToWallet();
vm.prank(address(testToken));
vm.expectRevert(ZeroValue.selector);
compliance.created(bob, 0);
}
/// @notice Should update the modules when amount is greater than zero
function test_created_Success_WhenAmountGreaterThanZero() public {
Token testToken = _setupComplianceBoundToWallet();
vm.prank(address(testToken));
compliance.created(bob, 10);
}
/// @notice Should call moduleMintAction on all bound modules
function test_created_Success_CallsModuleMintAction() public {
Token testToken = _setupComplianceBoundToWallet();
address moduleAddress = _deployTestModuleWithProxy();
vm.prank(deployer);
compliance.addModule(moduleAddress);
vm.prank(address(testToken));
compliance.created(bob, 100);
}
// ============================================
// .destroyed Tests
// ============================================
/// @notice Should revert when not calling as a bound token
function test_destroyed_RevertWhen_NotBoundToken() public {
vm.prank(another);
vm.expectRevert(AddressNotATokenBoundToComplianceContract.selector);
compliance.destroyed(address(0), 0);
}
/// @notice Should revert when from address is null
function test_destroyed_RevertWhen_FromAddressIsZero() public {
Token testToken = _setupComplianceBoundToWallet();
vm.prank(address(testToken));
vm.expectRevert(ZeroAddress.selector);
compliance.destroyed(address(0), 10);
}
/// @notice Should revert when amount is zero
function test_destroyed_RevertWhen_AmountIsZero() public {
Token testToken = _setupComplianceBoundToWallet();
vm.prank(address(testToken));
vm.expectRevert(ZeroValue.selector);
compliance.destroyed(alice, 0);
}
/// @notice Should update the modules when amount is greater than zero
function test_destroyed_Success_WhenAmountGreaterThanZero() public {
Token testToken = _setupComplianceBoundToWallet();
vm.prank(address(testToken));
compliance.destroyed(alice, 10);
}
/// @notice Should call moduleBurnAction on all bound modules
function test_destroyed_Success_CallsModuleBurnAction() public {
Token testToken = _setupComplianceBoundToWallet();
address moduleAddress = _deployTestModuleWithProxy();
vm.prank(deployer);
compliance.addModule(moduleAddress);
vm.prank(address(testToken));
compliance.destroyed(alice, 100);
}
// ============================================
// .callModuleFunction Tests
// ============================================
/// @notice Should revert when sender is not the owner
function test_callModuleFunction_RevertWhen_NotOwner() public {
vm.prank(another);
bytes memory randomData = new bytes(32);
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, another));
compliance.callModuleFunction(randomData, address(0));
}
/// @notice Should revert when module is not bound
function test_callModuleFunction_RevertWhen_ModuleNotBound() public {
vm.prank(deployer);
bytes memory randomData = new bytes(32);
vm.expectRevert(ModuleNotBound.selector);
compliance.callModuleFunction(randomData, address(0));
}
/// @notice Should call module function and emit ModuleInteraction event
function test_callModuleFunction_Success_EmitsModuleInteraction() public {
address moduleAddress = _deployTestModuleWithProxy();
vm.prank(deployer);
compliance.addModule(moduleAddress);
bytes memory callData = abi.encodeWithSignature("blockModule(bool)", true);
bytes4 expectedSelector = bytes4(callData);
vm.expectEmit(true, true, false, false, address(compliance));
emit ModuleInteraction(moduleAddress, expectedSelector);
vm.prank(deployer);
compliance.callModuleFunction(callData, moduleAddress);
}
/// @notice Should revert when module function call fails
function test_callModuleFunction_RevertWhen_ModuleCallFails() public {
// Use ModuleNotPnP which doesn't have a fallback function
// So calls to non-existent functions will revert
address moduleAddress = _deployModuleNotPnPWithProxy();
vm.prank(deployer);
compliance.bindToken(address(token));
// Make compliance suitable for ModuleNotPnP by burning tokens
vm.startPrank(tokenAgent);
token.burn(alice, 1000);
token.burn(bob, 500);
vm.stopPrank();
// Set module as ready for this compliance and add it
vm.startPrank(deployer);
ModuleNotPnP(moduleAddress).setModuleReady(address(compliance), true);
compliance.addModule(moduleAddress);
vm.stopPrank();
// Call a non-existent function to trigger revert (no fallback function)
bytes memory callData = abi.encodeWithSignature("nonExistentFunction()");
vm.prank(deployer);
vm.expectRevert();
compliance.callModuleFunction(callData, moduleAddress);
}
/// @notice Should handle callData with length less than 4 bytes
function test_callModuleFunction_Success_WithShortCallData() public {
// Use TestModule which has a fallback function
// This allows the module call to succeed even with invalid callData
address moduleAddress = _deployTestModuleWithProxy();
vm.prank(deployer);
compliance.addModule(moduleAddress);
// Create callData with less than 4 bytes (covers _selector's return bytes4(0) path)
bytes memory shortCallData = new bytes(3);
shortCallData[0] = 0x12;
shortCallData[1] = 0x34;
shortCallData[2] = 0x56;
// The fallback function accepts any callData, so the module call succeeds
// This allows _selector to be called with short callData, testing the explicit return
vm.expectEmit(true, true, false, false, address(compliance));
emit ModuleInteraction(moduleAddress, bytes4(0));
vm.prank(deployer);
compliance.callModuleFunction(shortCallData, moduleAddress);
}
// ============================================
// .addAndSetModule Tests
// ============================================
/// @notice Should revert when not calling as the owner
function test_addAndSetModule_RevertWhen_NotOwner() public {
address moduleAddress = _deployTestModuleWithProxy();
bytes[] memory interactions = new bytes[](0);
vm.prank(another);
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, another));
compliance.addAndSetModule(moduleAddress, interactions);
}
/// @notice Should revert when interactions array exceeds 5 elements
function test_addAndSetModule_RevertWhen_InteractionsArrayExceeds5() public {
address moduleAddress = _deployTestModuleWithProxy();
bytes[] memory interactions = new bytes[](6);
for (uint256 i = 0; i < 6; i++) {
interactions[i] = abi.encodeWithSignature("someFunction()");
}
vm.prank(deployer);
vm.expectRevert(abi.encodeWithSelector(ArraySizeLimited.selector, 5));
compliance.addAndSetModule(moduleAddress, interactions);
}
/// @notice Should add module and perform interactions
function test_addAndSetModule_Success_WithInteractions() public {
address moduleAddress = _deployTestModuleWithProxy();
bytes[] memory interactions = new bytes[](2);
interactions[0] = abi.encodeWithSignature("blockModule(bool)", true);
interactions[1] = abi.encodeWithSignature("blockModule(bool)", false);
vm.expectEmit(true, false, false, false, address(compliance));
emit ModuleAdded(moduleAddress);
vm.expectEmit(true, true, false, false, address(compliance));
emit ModuleInteraction(moduleAddress, bytes4(interactions[0]));
vm.prank(deployer);
compliance.addAndSetModule(moduleAddress, interactions);
assertTrue(compliance.isModuleBound(moduleAddress));
}
/// @notice Should work with empty interactions array
function test_addAndSetModule_Success_WithEmptyInteractions() public {
address moduleAddress = _deployTestModuleWithProxy();
bytes[] memory interactions = new bytes[](0);
vm.expectEmit(true, false, false, false, address(compliance));
emit ModuleAdded(moduleAddress);
vm.prank(deployer);
compliance.addAndSetModule(moduleAddress, interactions);
assertTrue(compliance.isModuleBound(moduleAddress));
}
// ============================================
// .supportsInterface Tests
// ============================================
/// @notice Should return false for unsupported interfaces
function test_supportsInterface_ReturnsFalse_ForUnsupportedInterface() public {
bytes4 unsupportedInterfaceId = 0x12345678;
assertFalse(compliance.supportsInterface(unsupportedInterfaceId));
}
/// @notice Should correctly identify the IModularCompliance interface ID
function test_supportsInterface_ReturnsTrue_ForIModularCompliance() public {
InterfaceIdCalculator calculator = new InterfaceIdCalculator();
bytes4 interfaceId = calculator.getIModularComplianceInterfaceId();
assertTrue(compliance.supportsInterface(interfaceId));
}
/// @notice Should correctly identify the IERC3643Compliance interface ID
function test_supportsInterface_ReturnsTrue_ForIERC3643Compliance() public {
InterfaceIdCalculator calculator = new InterfaceIdCalculator();
bytes4 interfaceId = calculator.getIERC3643ComplianceInterfaceId();
assertTrue(compliance.supportsInterface(interfaceId));
}
/// @notice Should correctly identify the IERC173 interface ID
function test_supportsInterface_ReturnsTrue_ForIERC173() public {
InterfaceIdCalculator calculator = new InterfaceIdCalculator();
bytes4 interfaceId = calculator.getIERC173InterfaceId();
assertTrue(compliance.supportsInterface(interfaceId));
}
/// @notice Should correctly identify the IERC165 interface ID
function test_supportsInterface_ReturnsTrue_ForIERC165() public {
InterfaceIdCalculator calculator = new InterfaceIdCalculator();
bytes4 interfaceId = calculator.getIERC165InterfaceId();
assertTrue(compliance.supportsInterface(interfaceId));
}
/// @notice Should correctly identify the IModule interface ID
function test_getIModuleInterfaceId_ReturnsCorrectId() public {
InterfaceIdCalculator calculator = new InterfaceIdCalculator();
bytes4 interfaceId = calculator.getIModuleInterfaceId();
// Deploy a test module to verify the interface ID
address moduleAddress = _deployTestModuleWithProxy();
TestModule testModule = TestModule(moduleAddress);
assertTrue(testModule.supportsInterface(interfaceId));
}
// ============================================
// Edge Cases Tests
// ============================================
/// @notice Should return false if any module check fails
function test_canTransfer_ReturnsFalse_WhenBlockedModule() public {
address moduleAddress = _deployTestModuleWithProxy();
vm.prank(deployer);
compliance.addModule(moduleAddress);
// Block the module to make moduleCheck return false
bytes memory callData = abi.encodeWithSignature("blockModule(bool)", true);
vm.prank(deployer);
compliance.callModuleFunction(callData, moduleAddress);
bool result = compliance.canTransfer(alice, bob, 100);
assertFalse(result);
}
/// @notice Should return true when all modules pass
function test_canTransfer_ReturnsTrue_WhenAllModulesPass() public {
address moduleAddress = _deployTestModuleWithProxy();
vm.prank(deployer);
compliance.addModule(moduleAddress);
// Ensure module is not blocked (default state allows transfers)
bool result = compliance.canTransfer(alice, bob, 100);
assertTrue(result);
}
/// @notice Should still remove the module from array when removing middle module
function test_removeModule_Success_RemovesMiddleModule() public {
address moduleA = _deployTestModuleWithProxy();
address moduleB = _deployTestModuleWithProxy();
address moduleC = _deployTestModuleWithProxy();
vm.prank(deployer);
compliance.addModule(moduleA);
vm.prank(deployer);
compliance.addModule(moduleB);
vm.prank(deployer);
compliance.addModule(moduleC);
vm.expectEmit(true, false, false, false, address(compliance));
emit ModuleRemoved(moduleB);
vm.prank(deployer);
compliance.removeModule(moduleB);
address[] memory modules = compliance.getModules();
assertEq(modules.length, 2);
// Check that moduleA and moduleC are present, moduleB is not
bool foundA = false;
bool foundC = false;
for (uint256 i = 0; i < modules.length; i++) {
if (modules[i] == moduleA) foundA = true;
if (modules[i] == moduleC) foundC = true;
assertTrue(modules[i] != moduleB);
}
assertTrue(foundA);
assertTrue(foundC);
}
// ============================================
// OwnableOnceNext2StepUpgradeable Tests
// ============================================
/// @notice Should set owner to caller when first deploy
function test_OwnableOnceNext2StepUpgradeable_SetsOwner_WhenFirstDeploy() public {
ModularCompliance newCompliance = _deployModularComplianceWithProxy(address(getTREXImplementationAuthority()));
// Owner is initially the deployer (test contract)
assertEq(newCompliance.owner(), address(this));
}
/// @notice Should set next owner to caller when set first owner
function test_OwnableOnceNext2StepUpgradeable_SetsNextOwner_WhenSetFirstOwner() public {
ModularCompliance newCompliance = _deployModularComplianceWithProxy(address(getTREXImplementationAuthority()));
newCompliance.transferOwnership(alice);
assertEq(newCompliance.owner(), alice);
}
/// @notice Should set owner to next owner in 2 steps
function test_OwnableOnceNext2StepUpgradeable_SetsOwnerIn2Steps() public {
ModularCompliance newCompliance = _deployModularComplianceWithProxy(address(getTREXImplementationAuthority()));
newCompliance.transferOwnership(alice);
vm.expectEmit(true, true, false, false, address(newCompliance));
emit OwnershipTransferStarted(alice, bob);
vm.prank(alice);
newCompliance.transferOwnership(bob);
vm.expectEmit(true, true, false, false, address(newCompliance));
emit Ownable.OwnershipTransferred(alice, bob);
vm.prank(bob);
newCompliance.acceptOwnership();
assertEq(newCompliance.owner(), bob);
}
/// @notice Should revert when acceptOwnership is called by non-pending owner
function test_OwnableOnceNext2StepUpgradeable_acceptOwnership_RevertWhen_NotPendingOwner() public {
ModularCompliance newCompliance = _deployModularComplianceWithProxy(address(getTREXImplementationAuthority()));
newCompliance.transferOwnership(alice);
vm.expectEmit(true, true, false, false, address(newCompliance));
emit OwnershipTransferStarted(alice, bob);
vm.prank(alice);
newCompliance.transferOwnership(bob);
// Try to accept ownership from a different address (not the pending owner)
address charlie = makeAddr("charlie");
vm.prank(charlie);
vm.expectRevert(abi.encodeWithSelector(OwnableUnauthorizedAccount.selector, charlie));
newCompliance.acceptOwnership();
}
// ============================================
// Helper Functions
// ============================================
/// @notice Helper to deploy a token suite
/// @param salt The salt for CREATE2 deployment
/// @return testToken The deployed token
function _deployToken(string memory salt) internal returns (Token testToken) {
ITREXFactory.TokenDetails memory tokenDetails = ITREXFactory.TokenDetails({
owner: deployer,
name: "TREX DINO",
symbol: "TREXD",
decimals: 0,
irs: address(0),
ONCHAINID: address(0),
irAgents: new address[](0),
tokenAgents: new address[](0),
complianceModules: new address[](0),
complianceSettings: new bytes[](0)
});
ITREXFactory.ClaimDetails memory claimDetails = ITREXFactory.ClaimDetails({
claimTopics: new uint256[](0), issuers: new address[](0), issuerClaims: new uint256[][](0)
});
vm.prank(deployer);
trexFactory.deployTREXSuite(salt, tokenDetails, claimDetails);
testToken = Token(trexFactory.getToken(salt));
}
/// @notice Helper to setup compliance bound to token
/// @return testToken The deployed token bound to the compliance
function _setupComplianceBoundToWallet() internal returns (Token testToken) {
// Deploy new compliance for this test
compliance = _deployModularComplianceWithProxy(address(getTREXImplementationAuthority()));
compliance.transferOwnership(deployer);
// Deploy and add modules
address moduleA = _deployTestModuleWithProxy();
address moduleB = _deployTestModuleWithProxy();
vm.startPrank(deployer);
compliance.addModule(moduleA);
compliance.addModule(moduleB);
vm.stopPrank();
// Deploy a token and bind compliance to it
testToken = _deployToken("compliance-salt");
// Bind compliance to token
vm.prank(deployer);
testToken.setCompliance(address(compliance));
}
}