Transfer Rules
Overview
The TransferRules contract manages token type determination and transfer restrictions for the RestrictedLockupToken. It implements a two-layer approach to regulatory compliance through configurable rules managed by the Transfer Admin.
Architecture
The TransferRules contract provides two main functionalities:
- Token Type Rules - Determine token type based on investor identity
- Transfer Rules - Control transfer restrictions based on token type and recipient identity
Usage
Token Type Rules
Token Type Rules map investor identity (region + accreditation) to appropriate token types during minting.
Data Structure
struct TokenTypeRule {
uint256 tokenType; // Token type to assign (1=RegS, 2=RegD, etc.)
bool requiresAmlKyc; // Whether AML/KYC is required for this type
bool isActive; // Whether this rule is currently active
}
Configuration
// Set a single rule
transferRules.setTokenTypeRule(
840, // region (US)
2, // accreditation (Accredited)
2, // tokenType (RegD)
true, // requiresAmlKyc
true // isActive
);
// Get a configured rule
TokenTypeRule memory rule = transferRules.getTokenTypeRule(840, 2);
// Batch set multiple rules
transferRules.batchSetTokenTypeRules(
[840, 0], // regions
[2, 0], // accreditations
[2, 1], // tokenTypes
[true, true], // requiresAmlKyc flags
[true, true] // isActive flags
);
// Remove a rule
transferRules.removeTokenTypeRule(840, 2);
Automatic Type Determination
// Called by RestrictedLockupToken during minting
uint256 tokenType = transferRules.determineTokenType(
walletAddress,
identityRegistry
);
Example Rules for US Exempt Offerings
| Region | Accreditation | Token Type | AML/KYC Required | Description |
|---|---|---|---|---|
| 840 (US) | 2 (Accredited) | 2 (RegD) | Yes | US Accredited → RegD |
| 0 (Any) | 0 (Any) | 1 (RegS) | Yes | Default → RegS |
| 840 (US) | 1 (Retail) | 3 (RegCF) | Yes | US Retail → RegCF |
Transfer Rules Configuration
Transfer Rules define restrictions for transferring tokens based on token type, mint timestamp, and recipient identity.
Transfer Rule Data Structure
struct TransferRule {
uint256 lockDurationSeconds; // Holding period from mint timestamp
bool requiresAmlKyc; // Whether recipient must have AML/KYC
bool isActive; // Whether this rule is currently active
}
Transfer Rule Configuration
// Set a transfer rule
transferRules.setTransferRule(
2, // tokenType (RegD)
840, // recipientRegion (US)
0, // recipientAccreditation (Any)
TransferRule({
lockDurationSeconds: 180 days,
requiresAmlKyc: true,
isActive: true
})
);
// Get a transfer rule
TransferRule memory rule = transferRules.transferRuleFor(2, 840, 0);
// Batch set multiple rules
transferRules.batchSetTransferRules(
[2, 1], // tokenTypes
[840, 0], // recipientRegions
[0, 0], // recipientAccreditations
[rule1, rule2] // TransferRule structs
);
// Remove a rule
transferRules.removeTransferRule(2, 840, 0);
Transfer Validation Usage
// Called by RestrictedLockupToken during transfers
uint8 restrictionCode = transferRules.detectTransferRestriction(
tokenAddress,
senderAddress,
recipientAddress,
transferAmount
);
// Get unlock timestamp for a holding
uint256 unlockTime = transferRules.getUnlockTimestamp(
tokenType,
mintTimestamp,
recipientAddress,
identityRegistry
);
Example Transfer Rules for US Exempt Offerings
| Token Type | Recipient Region | Recipient Accreditation | Lock Duration | AML/KYC | Description |
|---|---|---|---|---|---|
| 2 (RegD) | 840 (US) | 0 (Any) | 180 days | Yes | RegD → US: 6 months |
| 1 (RegS) | 0 (Any) | 0 (Any) | 365 days | Yes | RegS → Any: 12 months |
| 4 (Institutional) | 0 (Any) | 4 (Institutional) | 0 days | Yes | Institutional → Institutional: No hold |
Process Flows
Token Minting Flow
Transfer Validation Flow
Validation Logic
Token Type Determination
When determineTokenType() is called:
- Get Identity: Retrieve investor's region and accreditation from IdentityRegistry
- Find Rule: Look up TokenTypeRule for (region, accreditation) combination
- Return Type:
- If rule found and active: return
rule.tokenType - If no rule found: return
TOKEN_TYPE_GENERIC (0)
- If rule found and active: return
Transfer Validation Logic
When checkTransferAllowed() is called:
- Get Recipient Identity: Retrieve recipient's region, accreditation, and AML/KYC status
- Find Transfer Rule: Look up TransferRule for (tokenType, recipientRegion, recipientAccreditation)
- Check Conditions:
- Rule exists and is active
- Holding period satisfied:
mintTimestamp + lockDurationSeconds <= block.timestamp - AML/KYC requirement met (if required)
- Return Result:
SUCCESS (0): All conditions metNO_RULE_FOR_RECIPIENT (13): No matching rule foundHOLDING_PERIOD_NOT_MET (12): Lock duration not elapsedRECIPIENT_NOT_QUALIFIED (14): AML/KYC requirement not met
Administrative Functions
Access Control
- Transfer Admin: Can configure all rules (token type and transfer rules) via external
AccessControl - Contract Admin: Can upgrade the TransferRules contract via external
AccessControl
Key Functions
// Token Type Rules
function setTokenTypeRule(uint256 region, uint256 accreditation, uint256 tokenType, bool requiresAmlKyc) external onlyTransferAdmin;
function removeTokenTypeRule(uint256 region, uint256 accreditation) external onlyTransferAdmin;
function batchSetTokenTypeRules(uint256[] regions, uint256[] accreditations, uint256[] tokenTypes, bool[] requiresAmlKycFlags) external onlyTransferAdmin;
// Transfer Rules
function setTransferRule(uint256 tokenType, uint256 recipientRegion, uint256 recipientAccreditation, TransferRule memory rule) external onlyTransferAdmin;
function removeTransferRule(uint256 tokenType, uint256 recipientRegion, uint256 recipientAccreditation) external onlyTransferAdmin;
function batchSetTransferRules(uint256[] tokenTypes, uint256[] recipientRegions, uint256[] recipientAccreditations, TransferRule[] rules) external onlyTransferAdmin;
// View Functions
function getTokenTypeRule(uint256 region, uint256 accreditation) external view returns (TokenTypeRule memory);
function transferRuleFor(uint256 tokenType, uint256 recipientRegion, uint256 recipientAccreditation) external view returns (TransferRule memory);
Error Handling
Common Restriction Codes
- SUCCESS (0): Transfer allowed
- HOLDING_PERIOD_NOT_MET (12): Required holding period not elapsed
- NO_RULE_FOR_RECIPIENT (13): No transfer rule defined for combination
- RECIPIENT_NOT_QUALIFIED (14): AML/KYC requirement not met
Troubleshooting
- No rule found: Verify rules are configured for the specific combination
- Holding period: Check
mintTimestamp + lockDurationSecondsvs current time - AML/KYC issues: Confirm recipient's AML/KYC status in IdentityRegistry
- Rule inactive: Ensure rules have
isActive = true
Integration
With RestrictedLockupToken
- Token calls
determineTokenType()during minting - Token calls
checkTransferAllowed()during transfers - Token respects restriction codes and blocks invalid transfers
With IdentityRegistry
- TransferRules queries recipient identity for validation
- Supports region, accreditation, and AML/KYC status checks
- Works with configurable AML/KYC validity periods
Practical Configuration Process
Step 1: Identity Management
// Register investor identity
uint256[] memory regions = new uint256[](1);
regions[0] = 840; // United States
IdentityInfo memory identity = IdentityInfo({
regions: regions,
accreditationType: 2, // Accredited
lastAmlKycChangeTimestamp: 0, // Contract will use block.timestamp
lastAccreditationChangeTimestamp: 0, // Contract will use block.timestamp
amlKycPassed: true
});
identityRegistry.setIdentity(investorAddress, identity);
Step 2: Rule Configuration
// Set token type rules for automatic assignment
transferRules.setTokenTypeRule(840, 2, 2, true, true); // US Accredited → RegD, Active
// Set transfer restrictions for RegD tokens
transferRules.setTransferRule(2, 0, 0, TransferRule({
lockDurationSeconds: 180 days,
requiresAmlKyc: true,
isActive: true
}));
Step 3: Token Operations
// Mint tokens (type automatically determined via rules)
token.mint(investorAddress, 1000 * 10**18); // 1000 tokens
// Or mint specific type
token.mintTokenType(investorAddress, 1000 * 10**18, 2); // 1000 RegD tokens
Advanced Transfer Scenarios
Multi-Jurisdiction Compliance
// Investor with dual citizenship (US + UK)
uint256[] memory dualRegions = new uint256[](2);
dualRegions[0] = 840; // US
dualRegions[1] = 826; // UK
// Configure rules for both jurisdictions
transferRules.setTokenTypeRule(840, 2, 2, true, true); // US Accredited → RegD, Active
transferRules.setTokenTypeRule(826, 3, 1, true, true); // UK Qualified → RegS, Active
Time-Based Restrictions
// Configure different holding periods for different token types
transferRules.setTransferRule(1, 0, 0, TransferRule({
lockDurationSeconds: 365 days, // RegS: 1 year hold
requiresAmlKyc: true,
isActive: true
}));
transferRules.setTransferRule(2, 840, 0, TransferRule({
lockDurationSeconds: 180 days, // RegD to US: 6 months
requiresAmlKyc: true,
isActive: true
}));
transferRules.setTransferRule(4, 0, 4, TransferRule({
lockDurationSeconds: 0, // Institutional to Institutional: No hold
requiresAmlKyc: true,
isActive: true
}));
API Reference
Constructor
constructor(address trustedForwarder_, address accessControl_)
Initializes the TransferRules contract with ERC-2771 meta-transaction support and access control integration.
Parameters:
trustedForwarder_(address): Address of the trusted forwarder for meta-transactionsaccessControl_(address): Address of the AccessControl contract for role management
Requirements:
accessControl_cannot be the zero address
Emits:
- None
Errors:
TransferRules_InvalidAccessControl(): When accessControl_ is the zero address
Constants
Restriction Codes
SUCCESS() → uint8
Returns the success code (0) indicating no transfer restrictions.
GREATER_THAN_RECIPIENT_MAX_BALANCE() → uint8
Returns restriction code 1 for exceeding recipient maximum balance.
SENDER_TOKENS_TIME_LOCKED() → uint8
Returns restriction code 2 for sender tokens being time-locked.
DO_NOT_SEND_TO_TOKEN_CONTRACT() → uint8
Returns restriction code 3 for attempting to send tokens to the token contract itself.
DO_NOT_SEND_TO_EMPTY_ADDRESS() → uint8
Returns restriction code 4 for attempting to send tokens to the zero address.
SENDER_ADDRESS_FROZEN() → uint8
Returns restriction code 5 for sender address being frozen.
ALL_TRANSFERS_PAUSED() → uint8
Returns restriction code 6 for all transfers being paused.
RECIPIENT_ADDRESS_FROZEN() → uint8
Returns restriction code 7 for recipient address being frozen.
LOWER_THAN_RECIPIENT_MIN_BALANCE() → uint8
Returns restriction code 8 for transfer resulting in balance below recipient minimum.
INSUFFICIENT_BALANCE_OF_SENDER() → uint8
Returns restriction code 9 for insufficient sender balance.
SENDER_NOT_AMLKYCPASSED() → uint8
Returns restriction code 10 for sender not having passed AML/KYC.
RECIPIENT_NOT_AMLKYCPASSED() → uint8
Returns restriction code 11 for recipient not having passed AML/KYC.
HOLDING_PERIOD_NOT_MET() → uint8
Returns restriction code 12 for holding period not being met.