Modular Compliance
Pluggable smart contract architecture allowing regulatory rules (country bans, holder limits) to be updated without redeploying the token contract.
Modular Compliance is the ERC-3643 architectural pattern that separates regulatory rule enforcement from token logic, enabling compliance rules to be updated, added, or removed without redeploying the security token contract or migrating user balances. Rules like country restrictions, maximum holder limits, and transfer volume caps are implemented as independent "compliance modules" that can be plugged into the main compliance contract.
The article emphasizes this design: "Hardcoding compliance rules leads to technical debt. Regulations change. Use the ModularCompliance contract to plug in specific logic modules." This modularity addresses the fundamental challenge that securities regulations: vary by jurisdiction, change over time, and may require rapid updates (sanctions, regulatory actions).
Architecture
The Modular Compliance system consists of:
Compliance Contract: Orchestrates compliance checks by calling registered modules:
1contract ModularCompliance {2 address[] public modules;34 function canTransfer(5 address from,6 address to,7 uint256 amount8 ) external view returns (bool) {9 // All modules must approve the transfer10 for (uint i = 0; i < modules.length; i++) {11 if (!IComplianceModule(modules[i]).moduleCheck(from, to, amount)) {12 return false;13 }14 }15 return true;16 }17}
Compliance Modules: Individual contracts implementing specific rules:
1interface IComplianceModule {2 function moduleCheck(3 address from,4 address to,5 uint256 amount6 ) external view returns (bool);78 function moduleMintAction(address to, uint256 amount) external;9 function moduleBurnAction(address from, uint256 amount) external;10 function moduleTransferAction(address from, address to, uint256 amount) external;11}
Token Contract: Calls the Compliance contract before executing transfers:
1function transfer(address to, uint256 amount) public override returns (bool) {2 require(identityRegistry.isVerified(to), "Not verified");3 require(compliance.canTransfer(msg.sender, to, amount), "Not compliant");4 _transfer(msg.sender, to, amount);5 compliance.transferred(msg.sender, to, amount); // Update module state6 return true;7}
Common Compliance Modules
The article lists essential modules:
CountryAllowModule: Geographic restrictions based on investor country codes:
1contract CountryAllowModule is IComplianceModule {2 mapping(uint16 => bool) public allowedCountries;3 IIdentityRegistry public identityRegistry;45 function moduleCheck(address from, address to, uint256)6 external view returns (bool)7 {8 uint16 fromCountry = identityRegistry.investorCountry(from);9 uint16 toCountry = identityRegistry.investorCountry(to);1011 return allowedCountries[fromCountry] && allowedCountries[toCountry];12 }1314 function setCountryAllowed(uint16 country, bool allowed) external onlyOwner {15 allowedCountries[country] = allowed;16 }17}
MaxHoldersModule: Enforces cap on total token holders:
1contract MaxHoldersModule is IComplianceModule {2 uint256 public maxHolders;3 uint256 public currentHolders;4 mapping(address => bool) public isHolder;56 function moduleCheck(address from, address to, uint256)7 external view returns (bool)8 {9 // If recipient already holds tokens, no new holder created10 if (isHolder[to]) return true;1112 // If sender will have zero balance, holder count stays same13 if (getBalance(from) == amount && !isHolder[to]) {14 return true; // Net zero change15 }1617 // Adding new holder - check against cap18 return currentHolders < maxHolders;19 }2021 function moduleTransferAction(address from, address to, uint256 amount) external {22 // Update holder tracking after transfer23 if (!isHolder[to] && getBalance(to) > 0) {24 isHolder[to] = true;25 currentHolders++;26 }27 if (isHolder[from] && getBalance(from) == 0) {28 isHolder[from] = false;29 currentHolders--;30 }31 }32}
This is critical for exemptions like the US "2,000-investor rule" for private companies.
VolumeRestrictionModule: Prevents large transfers ("dumping"):
1contract VolumeRestrictionModule is IComplianceModule {2 uint256 public maxPercentagePerPeriod; // e.g., 10% = 1000 (basis points)3 uint256 public periodDuration; // e.g., 24 hours45 mapping(address => uint256) public periodStart;6 mapping(address => uint256) public periodVolume;78 function moduleCheck(address from, address, uint256 amount)9 external view returns (bool)10 {11 uint256 balance = getBalance(from);12 uint256 maxTransfer = balance * maxPercentagePerPeriod / 10000;1314 uint256 volumeInPeriod = periodVolume[from];15 if (block.timestamp >= periodStart[from] + periodDuration) {16 volumeInPeriod = 0; // New period17 }1819 return (volumeInPeriod + amount) <= maxTransfer;20 }21}
LockupModule: Enforces holding periods:
1contract LockupModule is IComplianceModule {2 mapping(address => uint256) public lockupEnd;34 function moduleCheck(address from, address, uint256)5 external view returns (bool)6 {7 return block.timestamp >= lockupEnd[from];8 }910 function setLockup(address holder, uint256 duration) external onlyOwner {11 lockupEnd[holder] = block.timestamp + duration;12 }13}
Module Management
Adding and removing modules requires careful access control:
1contract ModularCompliance {2 address public owner;3 address[] public modules;45 function addModule(address module) external onlyOwner {6 // Verify module implements interface7 require(8 IERC165(module).supportsInterface(type(IComplianceModule).interfaceId),9 "Invalid module"10 );11 modules.push(module);12 emit ModuleAdded(module);13 }1415 function removeModule(address module) external onlyOwner {16 for (uint i = 0; i < modules.length; i++) {17 if (modules[i] == module) {18 modules[i] = modules[modules.length - 1];19 modules.pop();20 emit ModuleRemoved(module);21 return;22 }23 }24 revert("Module not found");25 }26}
The article notes: "Because these are modular, you can upgrade a rule by pointing the Token Contract to a new Compliance Module address without migrating user balances."
Security Considerations
Module Removal Attacks: If an attacker gains module management access, they could remove all compliance modules, effectively converting the security token into a permissionless token. Mitigation: multi-sig ownership, timelock on module changes, and minimum module requirements.
Malicious Module Addition: Adding a module that always returns true for moduleCheck but performs malicious state changes in action hooks. Mitigation: thorough module auditing before deployment, interface validation, and module whitelists.
Module Ordering: If module execution order matters (stateful modules), reordering could cause unexpected behavior. Mitigation: document order dependencies, use deterministic ordering, or make modules order-independent.
State Synchronization: Modules tracking state (holder counts, volume) must stay synchronized with actual token state. If moduleTransferAction fails or isn't called, module state diverges from reality. Mitigation: atomic state updates, fallback synchronization mechanisms.
Gas Griefing: Many modules increase gas costs per transfer. Attackers could: add gas-expensive modules to make transfers prohibitively expensive, or exploit modules with unbounded loops. Mitigation: gas limits on module calls, module complexity bounds.
Module Upgrade Risks: Upgrading a module (deploying new, removing old) creates a window where compliance rules change. Users could exploit this window to execute transfers that wouldn't be allowed before/after. Mitigation: atomic module swaps, timelock announcements, pause during upgrades.
Audit Checklist for Modular Compliance
- Module Management: Who can add/remove modules? What controls exist?
- Module Validation: How are modules verified before addition?
- State Consistency: Do modules maintain consistent state with token balances?
- Gas Limits: Can module execution exceed block gas limits?
- Action Hooks: Are moduleTransferAction/moduleMintAction/moduleBurnAction called correctly?
- Upgrade Safety: How are module upgrades handled? What's the attack window?
- Minimum Modules: Is there a minimum required module set that can't be removed?
- Module Interactions: Do modules have order dependencies or conflicts?
Module Design Best Practices
Stateless When Possible: Modules that only read state (country checks, claim verification) are safer than stateful modules (holder counting, volume tracking). Stateless modules can't get out of sync.
Fail-Safe Defaults: Modules should default to rejecting transfers if state is unclear or corrupted. A compliance module that fails open defeats its purpose.
Event Logging: All module decisions should emit events for compliance auditing. Include: addresses involved, amounts, module decision, and reason codes.
Pausability: Individual modules should be pausable in case of discovered vulnerabilities, without requiring full token pause.
Upgrade Paths: Design modules with upgrade mechanisms (proxy patterns or module replacement procedures) that maintain state continuity.
Regulatory Flexibility
Modular Compliance enables rapid response to regulatory changes:
New Sanctions: Add a new CountryBlockModule for newly sanctioned countries without touching existing modules or token contract.
Regulation Updates: If holder limits change (regulatory exemption threshold), update MaxHoldersModule parameter rather than redeploying.
Jurisdiction Expansion: When entering new markets, add jurisdiction-specific modules (EU MiFID requirements, Singapore MAS rules) alongside existing US compliance.
Emergency Response: If a compliance issue is discovered, immediately add a restrictive module while developing a proper fix.
Understanding Modular Compliance is essential for auditing ERC-3643 security tokens. The flexibility that makes it powerful also creates attack surface—module management, state synchronization, and upgrade procedures must be carefully secured. Auditors should verify that: minimum compliance requirements can't be circumvented, module state stays synchronized with token state, and module upgrades don't create exploitable windows.
Articles Using This Term
Learn more about Modular Compliance in these articles:
Related Terms
Security Token
Blockchain-based representation of regulated securities (equity, debt, real estate) requiring transfer restrictions and investor verification under securities law.
Identity Registry
ERC-3643 component acting as source of truth, mapping wallet addresses to verified on-chain identities and enabling compliance checks before transfers.
On-Chain Identity
Decentralized identity system (ONCHAINID) based on ERC-734/735 that decouples user identity from wallet addresses, enabling key rotation and portable credentials.
Permissioned DeFi
Regulated sub-layer on public blockchains where only verified participants can interact, combining institutional compliance with decentralized infrastructure.
Need expert guidance on Modular Compliance?
Our team at Zealynx has deep expertise in blockchain security and DeFi protocols. Whether you need an audit or consultation, we're here to help.
Get a Quote

