Restricted Lockup Token
Overview
The Restricted Lockup Token contract is the core token contract that implements the ERC-20 token standard with ERC-1404 transfer restrictions.
To bypass contract size limits, some of the calls are delegated to the RestrictedLockupTokenExtension.sol and RestrictedLockupTokenManagementExtension.sol contracts.
Storage.sol is used to share the storage layout between the extensions and the core contract.
Usage
Lockups
The contract implements token lockup with scheduled release and distribution:
- Enforced scheduled release of tokens (e.g. investment lockups)
- Smart contract enforced lockup schedules are used to control the circulating supply and can be an important part of tokenomics.
- Some lockups are cancelable - such as employee vestings. When canceled, unlocked tokens remain in the recipients account address and locked tokens are returned to an address specified by the administrator that has an appropriate transfer group to receive the tokens.
Note that tokens in lockups cannot be burned by admins to avoid significant complexity. In order to burn those tokens, first cancelTimelock so they are removed from the timelock, and then burn them.
superBalanceOf
There are 3 classes of token types: simple tokens, unlocked tokens, and locked tokens. Locked and unlocked tokens are tokens supplied to the user via either fundReleaseSchedule or mintReleaseSchedule, but still evaluated within the timelock.
Unlocked tokens become removed from the timelock once they are transferred, and then are converted into simple tokens. A user's full balance of tokens consists of the sum of their simple, unlocked, and locked tokens. Cancelling a timelock also converts the unlocked tokens into simple tokens that become burnable.
Convenience Balance Methods
unlockedBalanceOf: tokens that are ONLY supplied viafundReleaseScheduleormintReleaseSchedulethat have been unlocked but not yet transferred out (ie not yet "removed" from the timelock). Includes unlocked tokens across all timelocks.superBalanceOf: simple tokens belonging to the wallet. Does not include locked or unlocked tokens still within timelocks.unlockedTotalBalanceOf: the sum ofunlockedBalanceOfandsuperBalanceOf, ie the total amount of tokens available to the wallet to transfer. Includes unlocked tokens across all timelocks.lockedBalanceOf: tokens that are ONLY supplied viafundReleaseScheduleormintReleaseSchedulethat are still locked. Includes unlocked tokens across all timelocks.balanceOf: the sum ofunlockedBalanceOf,lockedBalanceOf, andsuperBalanceOf. The total balance belonging to the wallet. Not all of this balance is tradeable though, as this includes locked tokens too.lockedBalanceOfTimelock: locked balance for a wallet within a specific timelock.unlockedBalanceOfTimelock: unlocked balance for a wallet within a specific timelock.
fundReleaseSchedule vs mintReleaseSchedule
Lockups, also referred to as token release schedules, are distinguished between funded and minted types. Funded release schedules are done using already-minted tokens that are owned by any admin. These are akin to a type of token transfer and thus must adhere to transfer restrictions.
Release schedules can also be minted directly to recipients by the Reserve Admin. In this case, as it is akin to token minting, it does not have to adhere to transfer restrictions, and is purely under the discretion of the Reserve Admin.
Enhanced Token Tracking
- Multiple Token Types: Single contract supports different regulatory classifications
- Timestamp Preservation: Each token holding maintains its original mint timestamp
- FIFO Transfer Logic: Oldest minted holdings are transferred first, preserving regulatory compliance
- Type-Specific Restrictions: Different rules for different token types and recipients
Optimized Holdings Storage with Bitmask Approach
The contract relies on bitmask-based storage optimization to efficiently track token holdings across different types and timestamps. This approach provides significant gas savings for users with multiple holdings while maintaining fast query performance:
- Constant-time existence checks: Using bit operations to verify holding existence
- Efficient iteration: Direct bitmap scanning without array enumeration
- Optimal storage: Compact representation of holding indexes using bit positions
- Batch operations: Multiple holdings can be processed in single storage operations
Storage Structure
// Global registry of all mint timestamps for each token type
mapping(uint256 => uint256[]) globalMintTimestamps;
// Actual token balances by type and timestamp
mapping(address => mapping(uint256 => mapping(uint256 => uint256))) balancesByTypeAndTime;
// Bitmask indicating which global timestamps a wallet has holdings for
mapping(address => mapping(uint256 => uint256[])) walletIndexesByType;
Bitmask Operations with Popcount and BitScan
The system leverages two key bit manipulation libraries:
Popcount (Population Count)
- Purpose: Counts the number of set bits (1s) in a bitmask
- Usage: Determines total number of holdings a wallet has for a token type
- Efficiency: Single CPU instruction on modern processors
- Example:
bitmask.popcount256B()returns count of holdings
function holdingCountOf(address who_, uint256 tokenType_) external view returns (uint256) {
uint256 count = 0;
for (uint256 i = 0; i < walletIndexesByType[who_][tokenType_].length; i++) {
count += walletIndexesByType[who_][tokenType_][i].popcount256B();
}
return count;
}
BitScan (Bit Scanning)
- Purpose: Finds the position of the first set bit in a bitmask
- Usage: Locates specific holdings during iteration without array traversal
- Types:
bitScanForward256()finds lowest set bit position - Efficiency: Enables direct access to holding positions
function _searchWordForHolding(...) internal view returns (bool, uint256, uint256) {
while (tempWord != 0) {
uint256 globalIndex = wordIndex_ * 256 + tempWord.bitScanForward256();
// Process holding at this index
tempWord &= tempWord - 1; // Clear the lowest set bit
}
}
Gas Optimization Benefits
- Batch Bitmap Updates: Multiple holdings can be set/cleared in single operations
- Direct Index Access: No need to iterate through arrays to find holdings
- Efficient Counting: Popcount provides instant holding counts
- Minimal Storage Overhead: Bitmasks are much more compact than holding arrays
Gas Cost and Limits
The bitmask layout removes the need to read arrays. But the quantity of work is still a function of
the count of different holdings in a wallet. A holding is one (tokenType, daysAfterDeployment)
global index with a balance that is more than 0.
Each operation that reads the balance of a wallet has a gas cost for each holding. transfer,
transferFrom, burn, softBurn and forceTransferBetween use the holdings in FIFO order. The
RestrictedSwap settlement check reads the holdings, but it does not change them.
The scan is driven by the amount, and not by the balance of the wallet. The loops in
_processTransfers stop when the requested amount is satisfied, and not when the bitmap is
exhausted. The cost is therefore a function of the count of holdings that the call actually
consumes, and not of the count that the wallet owns.
The measured cost is ≈4,787 gas for each holding. The test testBurnGasPerHoldingIsBounded in
test/foundry-tests/GasUsageHoldingsTest.t.sol burns 365 holdings for 1,747,597 gas, and asserts
the limit. The block gas limit is 30M. The practical limit for one call is therefore approximately
6,000 different holdings in one wallet.
A wallet above this limit is not stuck. A holding that the code fully drains has its bitmap bit cleared, so the work divides cleanly across calls. Three options are available:
- Divide the operation. More than one smaller
transfer,claimPrincipal(amount)orforceTransferBetween()call completes what one call cannot. Each call permanently retires the holdings that it fully drains, so the next call skips them and the progress carries over. The gas cost of each call is then a function of the amount, and not of the count of holdings in the wallet. - Address one holding.
burnHolding(from, globalHoldingIdx, amount)(reserve admin) takes a global holding index directly and makes no scan, so its cost does not increase with the count of holdings in the wallet. Refer to Recovering a Wallet With Very Many Holdings. - Limit the future increase.
setRecordMintTimestamp(false)(transfer admin), described below.
Two properties control the increase in the count of holdings:
- Only a mint makes a new global index. A transfer uses the index of the sender. The balance of
the recipient goes to the same
(tokenType, mintDay)pair. The set of indexes is therefore the set of different pairs from all earlier mints. - A wallet keeps one index for each holding that it received. The bitmap word array also only becomes longer. When the code clears a holding, it clears the bit, but it does not make the array shorter. A word that has gone to zero therefore still costs a cold SLOAD on each later pass.
setRecordMintTimestamp(false) (transfer admin) is the primary control. It holds the day part of the
index key at the current day. Each subsequent mint of a token type then uses one index. This limits
the future count of indexes to 256, one for each token type. The function limits only the future
increase. It does not remove the holdings that already exist. Therefore, set it early, or set it when
the issuer no longer needs one index for each day to enforce the holding period.
The bitmask layout is sized for a design envelope of approximately 1,000 holdings for each wallet
(BitManipulationLib.calculateSlotsPerWord). GasUsageHoldingsTest.t.sol benchmarks 1 to 1,000
holdings, with a 10,000-holding comparison case.
Two-Layer Rule System
The contract integrates a two-layer approach implemented in the TransferRules contract:
- Token Type Rules: Map investor identity → token type assignment
- Transfer Rules: Define transfer restrictions based on token type + recipient identity
- Default Token Type Rule: Fallback token type when no region/accreditation rule matches
For detailed information about transfer rules and configuration, see Transfer Rules.
Multi-Type Token Functionality
The contract includes built-in support for multiple token types to address different regulatory requirements. This allows a single token contract to handle various investor classifications such as RegS (Regulation S), RegD (Regulation D), RegCF (Regulation CF), and institutional investors.
Multi-Type Minting
The contract provides flexible minting capabilities that support both automatic token type determination and explicit type specification.
Automatic Token Type Minting
When using the standard mint() function, the token type is automatically determined based on the recipient's identity and current rules.
token.mint(recipient, amount);
Explicit Token Type Minting
For precise control, use mintTokenType() to specify the exact token type:
// Explicitly mint RegD tokens (type 2)
token.mintTokenType(recipient, amount, 2);
Multi-Type Functions
Minting
// Automatic type determination based on investor identity
token.mint(investor, 1000 * 10**18);
// Manual type specification
token.mintTokenType(investor, 500 * 10**18, 2); // RegD tokens
Querying
// Number of separate holdings
uint256 holdingCount = token.holdingCountOf(investor);
// Get specific holding details
(uint256 balance, uint256 mintTimestamp, uint256 tokenType) = token.holdingOf(investor, index);
Transfer Validation
// Check if transfer is allowed for specific token type
IIdentityRegistry.IdentityInfo memory recipientIdentity =
token.identityRegistry().identity(recipient);
uint256 restrictionCode = token.detectTransferRestrictionFor(
tokenType,
mintTimestamp,
recipientIdentity,
token.isAmlKycPassed(recipient)
);
Holder Management
Active Holder/Wallet management is conducted on-chain and autonomously, with multiple admin configurations that allow flexibility of transfer rule configuration. This greatly simplifies the off-chain accounting and effort required from Transfer (and other) Admins, removing the need to track Wallet holdings across different Holders.
Wallets are not programmatically prohibited from assuming multiple Admin roles. We advise against this in practice; however, this must be enforced off-chain.
Holders are allowed to keep multiple Wallet addresses, which can be spread across multiple Transfer Groups (in which case, they would be added to each group's holder count) as well as within Transfer Groups. These Wallets are consolidated under a common holderId.
- Ex.: Holder A can have 4 wallets spread across two Transfer Groups, X and Y. The Holder can have Wallets 1 & 2 in Group X, and Wallets 3 & 4 in Group Y. They will still count overall as one single Holder, but it will also be considered as a unique holder in Group X and one unique holder in Group Y.
To manage these Holders and their Wallets:
- A hook has been overridden (
_update) formint/transfer/burn/freezefunctions, automatically checking that recipient Wallet addresses of tokens are cataloged and assignedholderIds as needed. - If a "new" Wallet address receives a token, a new
holderIdis created for that Wallet address. - Admins can also separately create Holders from Wallet addresses and append Wallet addresses to existing Holder accounts
Maximum Number of Holders Allowed
A single Holder may have unlimited Wallet addresses. This cannot be altered. The Issuer can only configure the maximum number of Holders allowed (via Transfer Admin) by calling setHolderMax. By default, this limit is set to 2**255-1.
setHolderMax(amount)
Remove holder and wallet from holder
To efficiently remove unused holders and their associated wallets, transfer or wallet admins can call the removeHolder function, which iterates through all linked wallets and removes them. However, for holders with unusually large number of wallets, to avoid reaching block gas limit, it is possible first using batchRemoveWalletFromHolder and then calling removeHolder.
If a user does not have ownership of a specific wallet, it can still be removed from the holder's list individually using the removeWalletFromHolder function.
Maximum Total Supply, Minting and Burning
The variable maxTotalSupply is set when the contract is created and limits the total number of tokens that can be minted. It represents authorized shares and can be updated per company legal documents by setMaxTotalSupply.
Reserve Admins can mint tokens to and burn tokens from any address. This is primarily to comply with law enforcement, regulations and stock issuance scenarios - but this centralized power could be abused. Transfer Admins, authorized by Contract Admins, can also update the transfer rules at any moment in time as many times as they want.
Token Supply
There are also values that can be read from the smart contract for total token supply, circulating token supply, and unissued token supply. These correspond to the analogous Authorized shares, Outstanding shares, and Unissued shares, respectively, that exist in the context of stocks.
These methods are maxTotalSupply(), circulatingTokenSupply(), and unissuedTokenSupply().
Issue shares to investors
The reserve admin distributes the unissuedTokenSupply() to investors using either mint() or mintReleaseSchedule(), which increases the circulatingTokenSupply(). This method of token issuance should be used by issuers.
From a company shares perspective there are "authorized" and "outstanding" (issued) shares. For security tokens the authorized and unissued shares should not be held in a blockchain account. If they are unissued they should be distributed using the mint() or mintReleaseSchedule() functions. If shares are repurchased or reclaimed they should be retired by burning them. This is because all shares distributed to blockchain addresses receive dividend distributions.
Issued is a synonym for outstanding.
It should always hold true that circulatingTokenSupply == outstanding == issued, as these terms represent the same concept. Both circulatingTokenSupply() and totalSupply() reflect this value on-chain, ensuring consistency across these metrics.
Any "treasury" should not be issued on chain in a blockchain address. authorized == maxTokenSupply (also synonyms).
Meta Transactions
A custom ERC2771 forwarder contract has been created to allow for unique, but non-sequential nonce management. The forwarder is based on OpenZeppelin's ERC2771Forwarder.sol contract.
Example flow
-
Meta transaction is signed off-chain by a Transfer Admin using EIP-712 format.
-
Meta transaction is delivered to a relayer off-chain.
-
Permissionless relayer calls
executeorexecuteBatchon theERC2771Forwardercontract with the signed meta transaction. -
Trusted forwarder verifies the EIP-712 message and signature and executes the meta transaction in target contract on behalf of the Transfer Admin signer.
Initial Security Token Deployment
The deployment process involves several contracts that must be deployed in a specific order due to their dependencies. The system uses a fallback extension pattern where the main token contract delegates functionality to a separate extension contract.
Deployment Order Requirements
Phase 1: Core Infrastructure (No Dependencies)
- IdentityRegistry - Identity management and compliance verification
- TransferRules - Transfer restriction rule engine
- SnapshotPeriods - Historical balance tracking for dividends/voting
Phase 2: Token Extension Pattern
- RestrictedLockupTokenExtension - Advanced token operations (holdings, timelocks, multi-type support)
- RestrictedLockupToken - Main token contract with fallback to extension
Phase 3: Dependent Services
- InterestPayment - Interest accrual and dividend distribution
- RestrictedSwap - Token trading with compliance
- PurchaseContract - Automated token purchases with USDC
Integration Dependencies
| Contract | Required Dependencies | Purpose |
|---|---|---|
| IdentityRegistry | None | AML/KYC and regional compliance |
| TransferRules | IdentityRegistry (via constructor) | Token type assignment and transfer validation |
| SnapshotPeriods | None | Historical balance tracking |
| RestrictedLockupTokenExtension | None | Advanced token operations library |
| RestrictedLockupToken | TransferRules, IdentityRegistry, Extension | Main token with compliance integration |
| InterestPayment | RestrictedLockupToken, SnapshotPeriods | Interest and dividend distribution |
| RestrictedSwap | RestrictedLockupToken, SnapshotPeriods | Compliant token trading |
| PurchaseContract | RestrictedLockupToken, IdentityRegistry | Automated USDC → Token purchases |
Deployment Configuration Notes
IdentityRegistry Configuration:
amlKycValidityDuration: Set to0for permanent validity or duration in seconds (e.g.,31536000for 1 year)trustedForwarder: Address of ERC2771 forwarder for meta-transactions
RestrictedLockupToken Configuration:
extension: Address of deployed RestrictedLockupTokenExtensiontransferRules: Address of deployed TransferRules contractidentityRegistry: Address of deployed IdentityRegistry contractsnapshotPeriods: Address of deployed SnapshotPeriods contract
Multi-Type Token System: After deployment, configure token type rules for automatic regulatory compliance:
// US Accredited → RegD tokens
transferRules.setTokenTypeRule(840, 2, 2, true, true);
// 6-month holding period for RegD sent to US accredited recipients.
// Region/accreditation are matched exactly - there is no wildcard, so add one
// rule per recipient (region, accreditation) pair you intend to allow.
transferRules.setTransferRule(2, 840, 2, TransferRule({
lockDurationSeconds: 180 days,
requiresAmlKyc: true,
isActive: true
}));
Post-Deployment Configuration
After smart contract deployment, the system requires comprehensive configuration to enable secure, compliant token operations.
Phase 1: Administrative Role Assignment
Required Role Assignments:
- Transfer Admin (8): Configure compliance rules and payment operations
- Wallets Admin (4): Manage investor identities and compliance status
- Soft Burn Admin (16): Grant to InterestPayment for principal repayments
- Mint Admin (32): Grant to PurchaseContract for automated token purchases
Phase 2: Identity Registry & Compliance Setup
Key Configuration Examples:
// Configure RegD 6-month holding period
transferRules.setTransferRule(2, 840, 2, TransferRule({
lockDurationSeconds: 180 days,
requiresAmlKyc: true,
isActive: true
}));
// Configure RegS 1-year offshore holding period.
// One rule per offshore (region, accreditation) pair - no wildcard exists.
transferRules.setTransferRule(1, 826, 3, TransferRule({
lockDurationSeconds: 365 days,
requiresAmlKyc: true,
isActive: true
}));
Phase 3: Investor Identity Registration
Identity Registration Example:
// Register US accredited investor
uint256[] memory regions = new uint256[](1);
regions[0] = 840; // United States
IdentityInfo memory investorProfile = IdentityInfo({
regions: regions,
accreditationType: 2, // Accredited Investor
amlKycPassed: true,
lastAmlKycChangeTimestamp: block.timestamp,
lastAccreditationChangeTimestamp: block.timestamp
});
identityRegistry.setIdentity(investorAddress, investorProfile);
Phase 4: System Integration & Testing
Configuration Checklist
Core System Setup
- Transfer Admin Role: Granted to operational administrator
- Wallets Admin Role: Granted to compliance team
- Soft Burn Admin: Granted to InterestPayment contract
- Mint Admin: Granted to PurchaseContract (if used)
Compliance Configuration
- Token Type Rules: Configured for all investor categories
- Holding Periods: Set per regulatory requirements
- AML/KYC Validity: Duration configured in IdentityRegistry
- Holder Limits: Global and regional limits set (if applicable)
Investor Management
- Identity Profiles: Registered for all initial investors
- AML/KYC Status: Granted with appropriate validity periods
- Accreditation Levels: Assigned per investor sophistication
- Regional Classifications: Multi-jurisdiction support configured
Integration Testing
- Token Minting: Automatic type assignment verified
- Transfer Validation: Holding period restrictions tested
- Payment Systems: Interest and dividend functionality validated
- Purchase Contracts: Automated USDC-to-token conversion tested
Multi-Admin Security Architecture
The contract allows for a multi-admin architecture designed to minimize security risks while enabling efficient operations:
Administrative Separation Strategy
- Reserve Admin: Securely stored offline, used only for major supply operations
- Transfer Admin: Operational wallet for day-to-day rule management and payments
- Wallets Admin: Customer-facing operations for investor onboarding
- Contract Admin: Multi-sig protected for system configuration and emergencies
Progressive Security Implementation
Phase 1: Basic Operations
- Contract Admin grants Transfer Admin and Wallets Admin roles
- Wallets Admin registers investor identities in IdentityRegistry
- Transfer Admin configures token type and transfer rules
Phase 2: Operational Security
- Reserve Admin transitions to cold storage/multi-sig setup
- Transfer Admin establishes day-to-day operational procedures
- Wallets Admin implements investor onboarding workflows
Phase 3: Advanced Operations
- Interest Payment systems activated for loan/dividend operations
- Purchase Contracts deployed for automated token sales
- Compliance monitoring systems integrated with IdentityRegistry
Multi-Signature Recommendations
| Role | Recommended Security | Rationale |
|---|---|---|
| Contract Admin | 3-of-5 Multi-sig | Root access, system upgrades |
| Reserve Admin | 2-of-3 Multi-sig + Cold Storage | Token supply authority |
| Transfer Admin | 2-of-3 Multi-sig | Operational rules management |
| Wallets Admin | Single key or 2-of-3 Multi-sig | Customer operations, moderate risk |
Lockup Periods & Holding Restrictions
The system implements sophisticated lockup and holding period mechanisms through two complementary approaches: regulatory holding periods and individual timelocks.
Regulatory Holding Periods (Token Type Based)
Holding periods are automatically enforced based on token types and regulatory requirements:
// Configure holding periods per token type
transferRules.setTransferRule(
2, // tokenType: RegD
840, // recipientRegion: US
2, // recipientAccreditation: Accredited
TransferRule({
lockDurationSeconds: 180 days, // 6-month holding period
requiresAmlKyc: true,
isActive: true
})
);
Regulatory Holding Period Examples
| Token Type | Regulation | Holding Period | Recipient Requirements |
|---|---|---|---|
| RegS (1) | Offshore Placement | 365 days | Any jurisdiction, AML/KYC required |
| RegD (2) | US Private Placement | 180 days → Accredited / 365 days → Retail | US investors, different periods by accreditation |
| RegCF (3) | Crowdfunding | 0 days → Accredited / Restricted → Retail | Can only transfer to accredited investors |
| Institutional (4) | Qualified Investors | 90 days | Institutional-grade investors |
| Generic (0) | Default | Configurable | Based on configured rules |
Automatic Enforcement
- Mint Timestamp Tracking: Each holding records when tokens were minted
- Transfer Validation: System calculates if sufficient time has passed per token type
- FIFO Logic: Oldest holdings are transferred first to optimize holding period compliance
Individual Timelocks (Vesting Schedules)
Granular vesting schedules for specific token allocations using release schedules:
// Create vesting schedule for employee tokens
ReleaseScheduleParams memory schedule = ReleaseScheduleParams({
to: employeeAddress,
amount: 120000 * 10**18,
commencementTimestamp: block.timestamp + 365 days, // 1-year cliff
scheduleId: bytes32("employee_vesting_001"),
tokenType: 2, // RegD tokens
holdingIdx: 0 // Use first available holding
});
// Fund the vesting schedule
token.fundReleaseSchedule(schedule, cancellerAddress);
Timelock Features
- Cliff Periods: All-or-nothing release at specific timestamp
- Linear Vesting: Gradual token release over time
- Cancellation Rights: Designated canceller can reclaim unvested tokens
- Transfer Rights: Vested portions can be transferred to other recipients
- Token Type Preservation: Timelocked tokens maintain their regulatory classification
Advanced Lockup Scenarios
Multi-Type Holdings with Different Periods
// Investor holds multiple token types with different holding periods
// Check balances by token type (iterate through holdings to get type-specific balances)
uint256 regSBalance = 0; // 1-year lockup
uint256 regDBalance = 0; // 6-month lockup
uint256 regCFBalance = 0; // Recipient restrictions
uint256 holdingCount = token.holdingCountOf(investor);
for (uint256 i = 0; i < holdingCount; i++) {
Holding memory holding = token.holdingOf(investor, i);
if (holding.tokenType == 1) regSBalance += holding.balance;
else if (holding.tokenType == 2) regDBalance += holding.balance;
else if (holding.tokenType == 3) regCFBalance += holding.balance;
}
// Transfer will use oldest eligible holdings first (FIFO)
token.transfer(recipient, amount); // System automatically selects compliant holdings
Timelock with Token Type Updates
// Reserve Admin can update token types on timelocked tokens
token.updateTimelockTokenType(
walletAddress,
timelockIndex,
newTokenType // Change regulatory classification
);
Complex Vesting with Multiple Cancellers
// Multiple parties can have cancellation rights
address[] memory cancellers = new address[](2);
cancellers[0] = companyAddress; // Company can cancel for cause
cancellers[1] = employeeAddress; // Employee can forfeit early
// Each canceller can designate different reclaim addresses
Lockup Period Validation
Real-Time Compliance Checking
// Check if specific holding can be transferred
uint256 restrictionCode = transferRules.detectTransferRestriction(
tokenAddress,
senderAddress,
recipientAddress,
amount
);
bool canTransfer = (restrictionCode == 0);
// Get minimum waiting time for transfer (note: this function may not exist in actual contract)
// Use detectTransferRestrictionFor to check specific token type restrictions
uint256 restrictionCode = token.detectTransferRestrictionFor(
tokenType,
mintTimestamp,
recipientIdentity,
isAmlKycPassed
);
Batch Validation for Complex Portfolios
// Check multiple holdings for transfer eligibility
for (uint256 i = 0; i < holdingCount; i++) {
Holding memory holding = token.holdingOf(wallet, i);
// Check if this holding can be transferred
uint256 restrictionCode = token.detectTransferRestrictionFor(
holding.tokenType,
holding.mintTimestamp,
recipientIdentity,
isAmlKycPassed
);
if (restrictionCode == 0) {
// This holding is eligible for transfer
}
}
Integration with Interest Payments & Dividends
Lockup periods interact with payment systems:
- Dividend Eligibility: All holdings (locked and unlocked) receive dividends
- Interest Accrual: Timelocked tokens accrue interest from vesting commencement
- Principal Repayment: Uses
softBurn()mechanism respecting holding periods - Snapshot Timing: Dividend snapshots include both regular and timelocked holdings
Timelock Cancellations and Transfers
Cancel Timelock
In the case of cancellations, the transfer restrictions must be enabled between the initial target recipient and the reclaimTo address designated by the canceler.
Timelocks can be configured to be cancelled by a specific canceler address. This canceler can designate a separate reclaim address to receive the locked token portion of the remaining timelock, pending allowed group transfers as described above. The remaining unlocked portion will be transferred directly to the initial target recipient of the original vesting.
Transfer Timelock
For unlocked tokens within a timelock, the initial target recipient can choose to transfer unlocked tokens directly to another recipient. This is a convenience atop the typical transfer method.
In the case of a timelock transfer, the initial target recipient of the timelock must be in a group able to transfer tokens to the new recipient of the tokens. The initial target recipient must also be in a group able to transfer tokens to the new recipient.
Example Transfer Restrictions
The transfer restriction system uses identity-based compliance and token type rules to automatically enforce regulatory requirements. Here are practical examples of common scenarios:
Example 1: US Accredited Investor Token Trading (RegD)
Setup Identity and Rules:
// 1. Register US accredited investor identity
uint256[] memory regions = new uint256[](1);
regions[0] = 840; // United States
IdentityInfo memory accreditedInvestor = IdentityInfo({
regions: regions,
accreditationType: 2, // Accredited Investor
amlKycPassed: true,
lastAmlKycChangeTimestamp: block.timestamp,
lastAccreditationChangeTimestamp: block.timestamp
});
identityRegistry.setIdentity(investorAddress, accreditedInvestor);
// 2. Configure automatic RegD token assignment for US accredited
transferRules.setTokenTypeRule(840, 2, 2, true, true); // US, Accredited → RegD, AML/KYC required
// 3. Set 6-month holding period for RegD tokens to other accredited investors
transferRules.setTransferRule(2, 840, 2, TransferRule({
lockDurationSeconds: 180 days,
requiresAmlKyc: true,
isActive: true
}));
Token Operations:
// Mint tokens (automatically assigned RegD type based on investor identity)
token.mint(investorAddress, 50000 * 10**18);
// Transfer between accredited investors (subject to 6-month holding period)
token.transfer(recipientAddress, 10000 * 10**18); // Validates holding period automatically
Transfer Success Conditions:
- Sender has RegD tokens that are at least 6 months old (minted ≥180 days ago)
- Recipient is a verified accredited US investor with valid AML/KYC status
- Recipient's total holder count doesn't exceed global limits
- Neither party is frozen by administrators
Example 2: Preventing RegS Flowback (Offshore to US Restrictions)
Setup Multi-Jurisdiction Rules:
// 1. Configure RegS tokens for offshore investors
transferRules.setTokenTypeRule(826, 3, 1, true, true); // UK, Qualified → RegS, AML/KYC required, Active
// 2. RegS 1-year holding period for offshore (UK Qualified) recipients.
// Repeat this call for every offshore (region, accreditation) pair you support -
// region/accreditation are matched exactly and there is no wildcard.
transferRules.setTransferRule(1, 826, 3, TransferRule({
lockDurationSeconds: 365 days, // 1 year offshore holding
requiresAmlKyc: true,
isActive: true
}));
// 3. RegS to US accredited: extended flowback restriction
transferRules.setTransferRule(1, 840, 2, TransferRule({
lockDurationSeconds: 365 days + 40 days, // Extra 40-day seasoning
requiresAmlKyc: true,
isActive: true
}));
// 4. RegS to US retail: completely restricted
transferRules.setTransferRule(1, 840, 1, TransferRule({
lockDurationSeconds: 0,
requiresAmlKyc: false,
isActive: false // Disabled = no transfers allowed
}));
Results:
- RegS holders can trade among offshore investors after 1 year
- RegS to US accredited investors requires 13+ months (365 + 40 days)
- RegS to US retail investors is completely prohibited
- All transfers require valid AML/KYC status
Example 3: RegCF Crowdfunding Restrictions
Setup Retail Investor Limitations:
// 1. US retail investors get RegCF tokens
transferRules.setTokenTypeRule(840, 1, 3, true, true); // US, Retail → RegCF, Active
// 2. RegCF to accredited: no holding period (liquidity event)
transferRules.setTransferRule(3, 840, 2, TransferRule({
lockDurationSeconds: 0, // Immediate liquidity to accredited
requiresAmlKyc: true,
isActive: true
}));
// 3. RegCF to retail: restricted (secondary market limitations)
transferRules.setTransferRule(3, 840, 1, TransferRule({
lockDurationSeconds: 0,
requiresAmlKyc: false,
isActive: false // No retail-to-retail transfers
}));
Compliance Outcomes:
- Retail investors receive RegCF tokens automatically
- Can immediately sell to accredited investors (provides liquidity)
- Cannot trade among other retail investors (prevents unregulated secondary market)
Example 4: Exchange Omnibus Account Management
Identity Setup for Exchange:
// 1. Register exchange as institutional entity
IdentityInfo memory exchangeIdentity = IdentityInfo({
regions: [840], // US-based exchange
accreditationType: 4, // Institutional
amlKycPassed: true,
lastAmlKycChangeTimestamp: block.timestamp,
lastAccreditationChangeTimestamp: block.timestamp
});
identityRegistry.setIdentity(exchangeAddress, exchangeIdentity);
// 2. Configure institutional token type for exchanges
transferRules.setTokenTypeRule(840, 4, 4, true, true); // US, Institutional → Institutional tokens, Active
// 3. Set reduced holding periods for institutional trading
// (must match the recipient's exact region/accreditation - no wildcard)
transferRules.setTransferRule(4, 840, 4, TransferRule({
lockDurationSeconds: 30 days, // Reduced institutional holding period
requiresAmlKyc: true,
isActive: true
}));
Exchange Operations:
- Exchange receives institutional-grade tokens with shorter holding periods
- Customer withdrawals subject to recipient's identity verification
- Bulk holder management for custody operations
- Regulatory reporting simplified through automated compliance
Example 5: Multi-Type Portfolio Management
Complex Investor Scenario:
// Investor with multiple token types acquired over time
address investor = 0x123...;
// Check balances by token type (note: balanceOfByType function may not exist in actual contract)
// Use holdingCountOf and holdingOf to iterate through holdings by type
uint256 holdingCount = token.holdingCountOf(investor);
for (uint256 i = 0; i < holdingCount; i++) {
// Get holding details to check token types
}
// View individual holdings with timestamps (remove duplicate holdingCount)
for (uint256 i = 0; i < holdingCount; i++) {
Holding memory holding = token.holdingOf(investor, i);
// Check if this holding can be transferred
uint256 restrictionCode = token.detectTransferRestrictionFor(
holding.tokenType,
holding.mintTimestamp,
recipientIdentity,
isAmlKycPassed
);
bool canTransfer = (restrictionCode == 0);
}
Automatic FIFO Transfer Logic:
- System automatically selects oldest eligible holdings first
- Optimizes for regulatory compliance (shortest remaining holding periods)
- Transparent holding-level visibility for investors
Example 6: Employee Vesting with Regulatory Compliance
Setup Vesting Schedule:
// Employee receives RegD tokens with vesting
ReleaseScheduleParams memory vestingSchedule = ReleaseScheduleParams({
to: employeeAddress,
amount: 24000 * 10**18,
commencementTimestamp: block.timestamp + 365 days, // 1-year cliff
scheduleId: bytes32("employee_2024_001"),
tokenType: 2, // RegD tokens
holdingIdx: 0
});
token.fundReleaseSchedule(vestingSchedule, companyAddress);
Compliance Integration:
- Vested tokens subject to RegD 6-month holding period from vest date
- Company can cancel unvested portions for terminated employees
- Employee transfers subject to recipient verification
- Interest/dividend eligibility from initial grant date
Transfers Can Be Paused To Comply With Regulatory Action
If there is a regulatory issue with the token, all transfers may be paused by the Transfer Admin calling pause(). During normal functioning of the contract pause() should never need to be called.
The pause() mechanism has been implemented into the RestrictedLockupToken and RestrictedSwap, and InterestPayment contracts.
Recovery From A Blockchain Fork
Issuers should have a plan for what to do during a blockchain fork. Often security tokens represent a scarce off chain asset and a fork in the blockchain may present ambiguity about who can claim an off chain asset. For example, if 1 token represents 1 ounce of gold, a fork introduces 2 competing claims for 1 ounce of gold.
In the advent of a blockchain fork, the issuer should do something like the following:
- have a clear and previously defined way of signaling which branch of the blockchain is valid
- signal which branch is the system of record at the time of the fork
- call
pause()on the invalid fork (Transfer Admin) - use
burn()andmint()to fix errors that have been agreed to by both parties involved or ruled by a court in the issuers jurisdiction (Reserve Admin)
Law Enforcement Recovery of Stolen Assets
In the case of stolen assets with sufficient legal reason to be returned to their owner, the issuer can call freeze() (Wallets Admin, Transfer Admin), burn(), and mint() (Reserve Admin) to transfer the assets to the appropriate account.
Although this is not in the spirit of a cryptocurrency, it is available as a response to requirements that some regulators impose on blockchain security token projects.
Asset Recovery In The Case of Lost Keys
In the case of lost keys with sufficient legal reason to be returned to their owner, the issuer can call freeze(), burn(), and mint() to transfer the assets to the appropriate account. This opens the issuer up to potential cases of fraud. Handle with care.
Once again, although this is not in the spirit of a cryptocurrency, it is available as a response to requirements that some regulators impose on blockchain security token projects.
Recovering a Wallet With Very Many Holdings
A wallet accumulates one holding for each distinct (tokenType, mintDay) global index it has ever received tokens at, and anyone already holding a given index can pass it on — including in dust amounts. A wallet can therefore end up with more holdings than a single forceTransferBetween() call can process within the block gas limit. Recovery of such a wallet is still bounded by the number of transactions it takes, never by a gas wall, because burnHolding() addresses a holding directly and performs no scan.
freeze()(Wallets Admin, Transfer Admin) on the source address stops further holdings from arriving. It does not remove holdings that have already arrived.- Enumerate the wallet's global holding indexes off-chain.
getWalletIndexesLength(who)gives the number of bitmap words; for each wordi, readgetWalletIndexes(who, i)and treat each set bitbas global indexi * 256 + b.getTokenTypeAndMintTimestampFromGlobalIndex(globalIndex)describes each one, andholdingCountOf(who)gives the total count. All of these areviewand cost nothing. Note thatholdingOf(who, index)identifies a holding by its position among the wallet's holdings, not by global index, and returns only its balance, mint timestamp and token type — neither its argument nor its result can be passed toburnHolding(). burnHolding(from, globalHoldingIdx, amount)(Reserve Admin) removes one global index at a time, at a cost that does not depend on how many holdings the wallet has. Use it to clear unwanted indexes.mint()/mintTokenType()re-issue any value that should survive the burn.forceTransferBetween(from, to, amount)(Reserve Admin) moves the remainder. If the wallet still holds more than one call can cover, size each call from the enumeration in step 2 and repeat — each call permanently retires the holdings it fully drains, so progress carries over between calls.
When the holdings are legitimate and must be preserved rather than burned, use step 5 on its own, in stages. Drain unlocked timelocks in the first call: the timelock leg of forceTransferBetween() restarts from timelock index 0 on every call and skips exhausted timelocks without pruning them, so it does not get cheaper as the transfer is split. A first call sized to the unlocked timelock total consumes them before it reaches the holdings.
See Gas Cost and Limits above for the measured per-holding cost and the single-call ceiling.
API Reference
Constructor
constructor(ConstructorParams params)
Initializes the RestrictedLockupToken contract with comprehensive configuration parameters.
Parameters:
params(ConstructorParams): Struct containing all initialization parameters
ConstructorParams Structure:
struct ConstructorParams {
address transferRules; // Transfer rules contract address
address accessControl; // Access control contract address
address snapshotPeriods; // Snapshot periods contract address (optional)
address trustedForwarder; // Trusted forwarder for meta-transactions
address identityRegistry; // Identity registry contract address
address restrictedLockupTokenManagementExtension; // Management extension contract
address restrictedLockupTokenExtension; // Main extension contract
string symbol; // Token symbol
string name; // Token name
uint8 decimals; // Token decimals
uint256 maxTotalSupply; // Maximum total supply
uint256 minTimelockAmount; // Minimum amount for timelocks
uint256 maxReleaseDelay; // Maximum release delay
bool recordMintTimestamp; // Whether to record mint timestamps
}
Requirements:
- All address parameters (except snapshotPeriods) cannot be zero address
minTimelockAmountmust be greater than 0maxTotalSupplymust not exceed maximum safe supply- Extension contracts must have matching
slotsPerWordconfiguration
Emits:
- None
Errors:
RestrictedLockupToken_InvalidName(): When name is emptyRestrictedLockupToken_InvalidSymbol(): When symbol is emptyRestrictedLockupToken_InvalidTransferRules(): When transferRules is zero addressRestrictedLockupToken_InvalidAccessControl(): When accessControl is zero addressRestrictedLockupToken_InvalidTrustedForwarder(): When trustedForwarder is zero addressRestrictedLockupToken_InvalidIdentityRegistry(): When identityRegistry is zero addressRestrictedLockupToken_InvalidRestrictedLockupTokenManagementExtension(): When management extension is zero addressRestrictedLockupToken_InvalidRestrictedLockupTokenExtension(): When extension is zero addressRestrictedLockupToken_InvalidMinTimelockAmount(): When minTimelockAmount is 0RestrictedLockupToken_MaxTotalSupplyTooLarge(): When maxTotalSupply exceeds safe limitRestrictedLockupToken_ExtensionSlotsPerWordMismatch(): When extension has different slotsPerWord
ERC20 Functions
name() → string
Returns the name of the token.
symbol() → string
Returns the symbol of the token.
decimals() → uint8
Returns the number of decimals used by the token.
totalSupply() → uint256
Returns the total supply of tokens currently in circulation.
balanceOf(address account) → uint256
Returns the token balance of a specific account.
Parameters:
account(address): The address to query the balance of
allowance(address owner, address spender) → uint256
Returns the remaining number of tokens that spender is allowed to spend on behalf of owner.
Parameters:
owner(address): The address that owns the tokensspender(address): The address that is allowed to spend the tokens
approve(address spender, uint256 amount) → bool
Sets the allowance for a spender to spend tokens on behalf of the caller.
Parameters:
spender(address): The address to approve for spendingamount(uint256): The amount to approve
transfer(address to, uint256 amount) → bool
Transfers tokens from the caller to another address, enforcing transfer restrictions.
Parameters:
to(address): The recipient addressamount(uint256): The amount to transfer
Requirements:
- Transfer restrictions must be satisfied
- Sufficient balance (including unlocked timelock tokens)
transferFrom(address from, address to, uint256 amount) → bool
Transfers tokens from one address to another using allowance, enforcing transfer restrictions.
Parameters:
from(address): The sender addressto(address): The recipient addressamount(uint256): The amount to transfer
Requirements:
- Sufficient allowance
- Transfer restrictions must be satisfied
- Sufficient balance (including unlocked timelock tokens)
Token Supply Functions
maxTotalSupply() → uint256
Returns the maximum total supply that can be created (authorized shares).
circulatingTokenSupply() → uint256
Returns the current circulating supply (issued/outstanding shares).
unissuedTokenSupply() → uint256
Returns the remaining supply that can still be minted (unissued shares).
Balance Query Functions
superBalanceOf(address who) → uint256
Returns the balance of simple ERC20 tokens without any timelocks.
Parameters:
who(address): Address to query
unlockedTotalBalanceOf(address who) → uint256
Returns the total unlocked balance (simple tokens + unlocked timelock tokens).
Parameters:
who(address): Address to query
lockedBalanceOf(address who) → uint256
Returns the total locked balance across all timelocks.
Parameters:
who(address): Address to query
unlockedBalanceOf(address who) → uint256
Returns the total unlocked balance remaining across all timelocks.
Parameters:
who(address): Address to query
totalBalanceOf(address account) → uint256
Returns the total balance across all token types for an address.
Parameters:
account(address): The address to query
Timelock Functions
timelockCountOf(address who) → uint256
Returns the total count of timelocks for a specific address.
Parameters:
who(address): The address to get the timelock count for
timelockOf(address who, uint256 index) → Timelock
Returns the timelock struct details for a specific timelock.
Parameters:
who(address): Address to checkindex(uint256): The index of the timelock
Returns:
Timelock: Struct containing timelock details
lockedBalanceOfTimelock(address who, uint256 timelockIndex) → uint256
Returns the locked balance for a specific timelock.
Parameters:
who(address): The address to checktimelockIndex(uint256): Specific timelock index
unlockedBalanceOfTimelock(address who, uint256 timelockIndex) → uint256
Returns the unlocked balance for a specific timelock.
Parameters:
who(address): The address to checktimelockIndex(uint256): Specific timelock index
balanceOfTimelock(address who, uint256 timelockIndex) → uint256
Returns the total remaining balance (locked + unlocked) of a specific timelock.
Parameters:
who(address): The address to checktimelockIndex(uint256): Specific timelock index
totalUnlockedToDateOfTimelock(address who, uint256 timelockIndex) → uint256
Returns the total unlocked amount for a specific timelock to the current timestamp.
Parameters:
who(address): The address to checktimelockIndex(uint256): The timelock index
transferTimelock(address to, uint256 amount, uint256 timelockId) → bool
Transfers unlocked tokens from a specific timelock.
Parameters:
to(address): The recipient addressamount(uint256): The amount to transfertimelockId(uint256): The specific timelock to transfer from
Requirements:
- Sufficient unlocked balance in the specified timelock
- Transfer restrictions must be satisfied
cancelTimelock(address target, uint256 timelockIndex, uint256 scheduleId, uint256 commencementTimestamp, uint256 totalAmount, address reclaimTokenTo) → bool
Cancels a cancelable timelock and returns locked tokens to the reclaim address.
Parameters:
target(address): The timelock recipient addresstimelockIndex(uint256): The timelock indexscheduleId(uint256): Expected schedule ID (for validation)commencementTimestamp(uint256): Expected commencement timestamp (for validation)totalAmount(uint256): Expected total amount (for validation)reclaimTokenTo(address): Address to reclaim locked tokens to
Requirements:
- Timelock must be cancelable by caller
- All validation parameters must match
- Timelock must have remaining value
- The token must not be paused
reclaimTokenTomust not be the zero address, the token contract itself, or a frozen address- Holding-level transfer restrictions (token type rule, holding period, recipient AML/KYC where the
rule requires it) must be satisfied for
reclaimTokenTo
Deliberately not enforced (sender side):
cancelTimelock applies only the recipient side of the basic restriction layer. The sender-side
checks are intentionally skipped so that a clawback cannot be stalled by the state of the wallet
being clawed back from:
SENDER TOKENS LOCKED— expected; the canceled amount is locked by definitionSENDER NOT AMLKYCPASSED— a clawback must keep working after the holder loses eligibilitySENDER ADDRESS FROZEN— required by thefreeze()→cancelTimelock()→burn()→mint()recovery flow, since timelocked tokens are not directly burnable (see the note near the top of this document)
Minting Functions
mint(address to, uint256 value)
Mints tokens to an address with automatic token type determination.
Parameters:
to(address): The address to mint tokens tovalue(uint256): The amount to mint
Requirements:
- Caller must have RESERVE_ADMIN_ROLE or MINT_ADMIN_ROLE
- Total supply + amount must not exceed maxTotalSupply
- Recipient address cannot be zero
mintTokenType(address to, uint256 amount, uint256 tokenType)
Mints tokens of a specific type to an address.
Parameters:
to(address): The address to mint toamount(uint256): The amount to minttokenType(uint256): The specific token type (0-255)
Requirements:
- Caller must have RESERVE_ADMIN_ROLE or MINT_ADMIN_ROLE
- Token type must be within valid range (0-255)
- Token type must be allowed for the recipient
- Total supply + amount must not exceed maxTotalSupply
Release Schedule Functions
Release schedules (also called timelocks or vesting schedules) allow tokens to be locked and released over time according to predefined schedules. There are two main types: funded (using existing tokens) and minted (creating new tokens).
mintReleaseSchedule(address to, uint256 amount, uint256 commencementTimestamp, uint256 scheduleId, address[] cancelableBy) → bool
Mints tokens directly to a recipient on a release schedule with automatic token type determination based on the recipient's identity.
Parameters:
to(address): Recipient address that will have tokens unlocked on a release scheduleamount(uint256): Amount of tokens in base units (smallest unit without decimal point)commencementTimestamp(uint256): When the release schedule starts (unix timestamp)scheduleId(uint256): The ID of the release schedule to use (created viacreateReleaseSchedule)cancelableBy(address[]): Array of addresses that can cancel this timelock
Returns:
bool: True if successful
Requirements:
- Caller must have RESERVE_ADMIN_ROLE or MINT_ADMIN_ROLE
- Schedule ID must exist and be valid
- Total supply + amount must not exceed maxTotalSupply
- Does not require transfer restrictions (minting privilege)
Usage Note:
This function automatically determines the token type based on the recipient's identity using transferRules.determineTokenType(). Use mintReleaseScheduleTokenType() for explicit token type control.
mintReleaseScheduleTokenType(address to, uint256 amount, uint256 commencementTimestamp, uint256 scheduleId, uint256 tokenType, address[] cancelableBy) → bool
Mints tokens directly to a recipient on a release schedule with explicit token type specification.
Parameters:
to(address): Recipient address that will have tokens unlocked on a release scheduleamount(uint256): Amount of tokens in base unitscommencementTimestamp(uint256): When the release schedule starts (unix timestamp)scheduleId(uint256): The ID of the release schedule to usetokenType(uint256): The specific token type to mint (0-255)cancelableBy(address[]): Array of addresses that can cancel this timelock
Returns:
bool: True if successful
Requirements:
- Caller must have RESERVE_ADMIN_ROLE or MINT_ADMIN_ROLE
- Token type must be within valid range (0-255)
- Schedule ID must exist and be valid
- Total supply + amount must not exceed maxTotalSupply
- Does not require transfer restrictions (minting privilege)
fundReleaseSchedule(address to, uint256 amount, uint256 commencementTimestamp, uint256 scheduleId, address[] cancelableBy) → bool
Creates a release schedule using the caller's existing tokens with automatic holding and token type detection.
Parameters:
to(address): Recipient address that will receive tokens on a scheduleamount(uint256): Amount of tokens to lock in the schedulecommencementTimestamp(uint256): When the release schedule startsscheduleId(uint256): The ID of the release schedule to usecancelableBy(address[]): Array of addresses that can cancel this timelock
Returns:
bool: True if successful
Requirements:
- Caller must have any admin role (CONTRACT_ADMIN, RESERVE_ADMIN, TRANSFER_ADMIN, or WALLETS_ADMIN)
- Caller must have sufficient balance
- Transfer restrictions must be satisfied between caller and recipient
- Schedule ID must exist and be valid
- Automatically finds the first suitable holding using
findHoldingFor()
Behavior:
This function automatically:
- Searches for a suitable holding from the funder that can be transferred to the recipient
- Uses the found holding's token type for the timelock
- Validates transfer restrictions before creating the schedule
fundReleaseScheduleWithHolding(FundReleaseScheduleParams params, address[] cancelableBy) → bool
Creates a release schedule using a specific holding from the caller's tokens, providing explicit control over which holding is used.
Parameters:
params(FundReleaseScheduleParams): Struct containing schedule parameterscancelableBy(address[]): Array of addresses that can cancel this timelock
FundReleaseScheduleParams Structure:
struct FundReleaseScheduleParams {
address to; // Recipient address
uint256 amount; // Amount of tokens to lock
uint256 commencementTimestamp; // When schedule starts
bytes32 scheduleId; // Release schedule ID
uint256 tokenType; // Specific token type to use
uint256 holdingIdx; // Global holding index to fund from
}
Returns:
bool: True if successful
Requirements:
- Caller must have any admin role
- Specified holding must exist and have sufficient balance
- Holding must be of the specified token type
- Transfer restrictions must be satisfied
- Schedule ID must exist and be valid
batchMintReleaseSchedule(address[] to, uint256[] amounts, uint256[] commencementTimestamps, uint256[] scheduleIds, address[] cancelableBy) → bool
Batch version of mintReleaseSchedule for creating multiple release schedules in a single transaction with automatic token type determination.
Parameters:
to(address[]): Array of recipient addressesamounts(uint256[]): Array of token amountscommencementTimestamps(uint256[]): Array of start timestampsscheduleIds(uint256[]): Array of schedule IDscancelableBy(address[]): Array of addresses that can cancel ALL timelocks (applied to all schedules)
Returns:
bool: True if all schedules created successfully
Requirements:
- Caller must have RESERVE_ADMIN_ROLE or MINT_ADMIN_ROLE
- All arrays must have the same length
- Each schedule must meet individual
mintReleaseSchedulerequirements - Total minted amount must not exceed maxTotalSupply
batchMintReleaseScheduleTokenType(address[] to, uint256[] amounts, uint256[] commencementTimestamps, uint256[] scheduleIds, uint256[] tokenTypes, address[] cancelableBy) → bool
Batch version of mintReleaseScheduleTokenType for creating multiple release schedules with explicit token type control.
Parameters:
to(address[]): Array of recipient addressesamounts(uint256[]): Array of token amountscommencementTimestamps(uint256[]): Array of start timestampsscheduleIds(uint256[]): Array of schedule IDstokenTypes(uint256[]): Array of token types for each schedulecancelableBy(address[]): Array of addresses that can cancel ALL timelocks
Returns:
bool: True if all schedules created successfully
Requirements:
- Caller must have RESERVE_ADMIN_ROLE or MINT_ADMIN_ROLE
- All arrays must have the same length
- Each token type must be valid (0-255)
- Each schedule must meet individual
mintReleaseScheduleTokenTyperequirements
batchFundReleaseSchedule(address[] to, uint256[] amounts, uint256[] commencementTimestamps, uint256[] scheduleIds, address[] cancelableBy) → bool
Batch version of fundReleaseSchedule for creating multiple funded release schedules with automatic holding detection.
Parameters:
to(address[]): Array of recipient addressesamounts(uint256[]): Array of token amountscommencementTimestamps(uint256[]): Array of start timestampsscheduleIds(uint256[]): Array of schedule IDscancelableBy(address[]): Array of addresses that can cancel ALL timelocks
Returns:
bool: True if all schedules created successfully
Requirements:
- Caller must have any admin role
- All arrays must have the same length
- Caller must have sufficient balance for total amount
- Transfer restrictions must be satisfied for each recipient
- Each schedule automatically finds suitable holdings using
findHoldingFor()
batchFundReleaseScheduleWithHolding(FundReleaseScheduleParams[] params, address[] cancelableBy) → bool
Batch version of fundReleaseScheduleWithHolding for creating multiple release schedules with explicit holding control.
Parameters:
params(FundReleaseScheduleParams[]): Array of schedule parameter structscancelableBy(address[]): Array of addresses that can cancel ALL timelocks
Returns:
bool: True if all schedules created successfully
Requirements:
- Caller must have any admin role
- Each params element must specify a valid holding with sufficient balance
- Transfer restrictions must be satisfied for each recipient
- Each schedule ID must exist and be valid
Transfer Restriction Functions
detectTransferRestriction(address from, address to, uint256 value) → uint256
Checks if a transfer is allowed and returns a restriction code.
Parameters:
from(address): The sender addressto(address): The recipient addressvalue(uint256): The transfer amount
Returns:
uint256: Restriction code (0 = allowed, non-zero = restricted)
detectTransferRestrictionFor(uint256 tokenType, uint256 mintTimestamp, IdentityInfo recipientIdentity, bool isAmlKycPassed) → uint256
Checks transfer restrictions for a specific token type and recipient.
Parameters:
tokenType(uint256): The token type being transferredmintTimestamp(uint256): When the tokens were mintedrecipientIdentity(IdentityInfo): The recipient's identity informationisAmlKycPassed(bool): Whether the recipient has passed AML/KYC
Returns:
uint256: Restriction code (0 = allowed, non-zero = restricted)
messageForTransferRestriction(uint256 restrictionCode) → string
Returns a human-readable message for a restriction code.
Parameters:
restrictionCode(uint256): The restriction code to get message for
Returns:
string: Human-readable description of the restriction
Holding Management Functions
findHoldingFor(address sender, address recipient, uint256 amount) → (uint256 globalIndex, uint256 holdingAmount, uint256 tokenType)
Searches for a valid holding that can be transferred from sender to recipient.
This function efficiently scans through the sender's holdings using bitmap operations to find the first holding that:
- Has sufficient balance (≥ requested amount)
- Can be transferred to the recipient (passes transfer restrictions based on token type, mint timestamp, and recipient identity)
Parameters:
sender(address): The address holding the tokensrecipient(address): The intended recipient addressamount(uint256): The minimum amount required
Returns:
globalIndex(uint256): The global holding index (returnstype(uint256).maxif no valid holding found)holdingAmount(uint256): The total balance in the found holding (returns0if not found)tokenType(uint256): The token type of the found holding (returns0if not found)
transferHolding(address to, uint256 amount, uint256 globalHoldingIdx) → bool
Transfers tokens from a specific holding of a particular token type.
Parameters:
to(address): The recipient addressamount(uint256): The amount to transferglobalHoldingIdx(uint256): The global holding index
Requirements:
- Sufficient balance in the specified holding
- Transfer restrictions must be satisfied
burnHolding(address from, uint256 globalHoldingIdx, uint256 amount)
Burns tokens from one specific global holding index of a wallet, reducing total supply. The holding is addressed directly rather than located by scanning, so the cost does not grow with the number of holdings the wallet has. This is the bounded-cost counterpart to burn() and softBurn(), which both walk the wallet's holdings in FIFO order.
Parameters:
from(address): The address to burn tokens fromglobalHoldingIdx(uint256): The global holding index to burn from. This is not the wallet's position among its own holdings — the index taken byholdingOf(who, index)is such a position and cannot be used here. Obtain a global index fromgetWalletIndexes(who, i), where each set bitbof wordiis global indexi * 256 + b.amount(uint256): The amount to burn from that holding
Requirements:
- Caller must have RESERVE_ADMIN_ROLE
frommust not be the zero addressglobalHoldingIdxmust be less thanglobalHoldingCount(), otherwise reverts withRestrictedLockupToken_HoldingIndexOutOfBoundamountmust not exceed the balance held at that index, otherwise reverts withRestrictedLockupToken_NoItemWithEnoughBalance
Emits: TokenTypeBurned(from, amount, tokenType) and Transfer(from, address(0), amount)
When the burn takes the holding's balance to zero, its bitmap bit is cleared, so the holding stops costing gas on later scans.
Utility Functions
batchTransfer(address[] recipients, uint256[] amounts) → bool
Transfers tokens to multiple recipients in a single transaction.
Parameters:
recipients(address[]): Array of recipient addressesamounts(uint256[]): Array of amounts to transfer
Requirements:
- Arrays must have the same length
- Each individual transfer must satisfy restrictions
safeApprove(address spender, uint256 value)
Safely sets allowance, only allowing setting to zero or from zero.
Parameters:
spender(address): The address to approvevalue(uint256): The amount to approve
Requirements:
- Can only be called when setting initial allowance or resetting to zero
forceTransferBetween(address from, address to, uint256 amount)
Transfers tokens between addresses ignoring transfer restrictions.
Parameters:
from(address): The sender addressto(address): The recipient addressamount(uint256): The amount to transfer
Requirements:
- Caller must have RESERVE_ADMIN_ROLE
- Recipient must have a holder record
Administrative Functions
setMaxTotalSupply(uint256 maxTotalSupply)
Sets a new maximum total supply for the token.
Parameters:
maxTotalSupply(uint256): The new maximum total supply
Requirements:
- Caller must have RESERVE_ADMIN_ROLE
- New max supply must exceed current total supply
- Must not exceed maximum safe supply
upgradeTransferRules(ITransferRules newTransferRules)
Upgrades the transfer rules contract.
Parameters:
newTransferRules(ITransferRules): The new transfer rules contract
Requirements:
- Caller must have CONTRACT_ADMIN_ROLE
- New contract must implement ITransferRules interface
upgradeIdentityRegistry(IIdentityRegistry newIdentityRegistry)
Upgrades the identity registry contract.
Parameters:
newIdentityRegistry(IIdentityRegistry): The new identity registry contract
Requirements:
- Caller must have CONTRACT_ADMIN_ROLE
- New contract must implement IIdentityRegistry interface
setRecordMintTimestamp(bool enabled)
Enables or disables recording of mint timestamps.
Parameters:
enabled(bool): Whether to record mint timestamps
Requirements:
- Caller must have TRANSFER_ADMIN_ROLE
View Functions
scheduleCount() → uint256
Returns the total number of release schedules created.
getFrozenStatus(address addr) → bool
Returns whether an address is frozen.
Parameters:
addr(address): The address to check
isValidTransferRules(address transferRulesAddr) → bool
Checks if an address is a valid transfer rules contract.
Parameters:
transferRulesAddr(address): The address to validate
isValidIdentityRegistry(address identityRegistryAddr) → bool
Checks if an address is a valid identity registry contract.
Parameters:
identityRegistryAddr(address): The address to validate
snapshotPeriodsAddress() → address
Returns the address of the snapshot periods contract.
isAmlKycPassed(address wallet) → bool
Returns whether a wallet has passed AML/KYC verification.
Parameters:
wallet(address): The wallet address to check
determineTokenType(address wallet) → uint256
Determines the appropriate token type for a wallet based on identity.
Parameters:
wallet(address): The wallet address
existingTokenTypesCount() → uint256
Returns the count of existing token types that have been used.
existingTokenTypes(uint256 index) → uint256
Returns the token type at a specific index in the existing types array.
Parameters:
index(uint256): The index to query
tokenTypeExists(uint256 tokenType) → bool
Checks if a token type has been used in the system.
Parameters:
tokenType(uint256): The token type to check
getWalletIndexesLength(address who) → uint256
Returns the length of wallet indexes array for an address.
Parameters:
who(address): The address to query
getWalletIndexes(address who, uint256 index) → uint256
Returns the wallet index at a specific position.
Parameters:
who(address): The address to queryindex(uint256): The index position
globalHoldingCount() → uint256
Returns the total count of global holdings.
getTokenTypeAndMintTimestampFromGlobalIndex(uint256 globalIndex) → (uint256 tokenType, uint256 mintTimestamp)
Returns the token type and mint timestamp for a specific global holding index.
Parameters:
globalIndex(uint256): The global holding index to query
Returns:
tokenType(uint256): The token type of the holdingmintTimestamp(uint256): The mint timestamp of the holding
Vesting Calculation Functions
calculateUnlocked(uint256 commencedTimestamp, uint256 currentTimestamp, uint256 amount, uint256 scheduleId) → uint256
Calculates unlocked tokens for a schedule at a specific time.
Parameters:
commencedTimestamp(uint256): When the schedule startedcurrentTimestamp(uint256): The time to calculate foramount(uint256): The total amountscheduleId(uint256): The schedule ID
calculateUnlocked(uint256 commencedTimestamp, uint256 currentTimestamp, uint256 amount, ReleaseSchedule releaseSchedule) → uint256
Calculates unlocked tokens using a ReleaseSchedule struct.
Parameters:
commencedTimestamp(uint256): When the schedule startedcurrentTimestamp(uint256): The time to calculate foramount(uint256): The total amountreleaseSchedule(ReleaseSchedule): The release schedule struct
ERC-165 Interface Support
supportsInterface(bytes4 interfaceId) → bool
Checks if the contract supports a specific interface.
Parameters:
interfaceId(bytes4): The interface identifier
Returns:
bool: True if the interface is supported
Events
Token Events
Transfer(address indexed from, address indexed to, uint256 value)
Emitted when tokens are transferred between addresses.
Approval(address indexed owner, address indexed spender, uint256 value)
Emitted when an approval is set.
TokenTypeMinted(address indexed to, uint256 amount, uint256 indexed tokenType, uint256 mintTimestamp)
Emitted when tokens of a specific type are minted.
Parameters:
to(address, indexed): The recipient addressamount(uint256): The amount mintedtokenType(uint256, indexed): The token typemintTimestamp(uint256): The mint timestamp
TokenTypeTransferred(address indexed from, address indexed to, uint256 amount, uint256 indexed tokenType)
Emitted when tokens of a specific type are transferred.
Parameters:
from(address, indexed): The sender addressto(address, indexed): The recipient addressamount(uint256): The amount transferredtokenType(uint256, indexed): The token type
TokenTypeBurned(address indexed from, uint256 amount, uint256 indexed tokenType)
Emitted when tokens of a specific type are burned.
Parameters:
from(address, indexed): The address tokens were burned fromamount(uint256): The amount burnedtokenType(uint256, indexed): The token type
Administrative Events
SetMaxTotalSupply(address indexed admin, uint256 maxTotalSupply)
Emitted when the maximum total supply is updated.
Parameters:
admin(address, indexed): The admin who made the changemaxTotalSupply(uint256): The new maximum total supply
Upgrade(address indexed admin, address indexed oldContract, address indexed newContract)
Emitted when a contract is upgraded.
Parameters:
admin(address, indexed): The admin who performed the upgradeoldContract(address, indexed): The old contract addressnewContract(address, indexed): The new contract address
ForceTransferBetween(address indexed admin, address indexed from, address indexed to, uint256 amount)
Emitted when a force transfer is executed.
Parameters:
admin(address, indexed): The admin who executed the transferfrom(address, indexed): The sender addressto(address, indexed): The recipient addressamount(uint256): The amount transferred
Timelock Events
TimelockCanceled(address indexed admin, address indexed target, uint256 timelockIndex, address indexed reclaimTokenTo, uint256 canceledAmount, uint256 paidAmount)
Emitted when a timelock is canceled.
Parameters:
admin(address, indexed): The admin who canceled the timelocktarget(address, indexed): The timelock recipienttimelockIndex(uint256): The timelock indexreclaimTokenTo(address, indexed): Where canceled tokens were sentcanceledAmount(uint256): The amount of canceled (locked) tokenspaidAmount(uint256): The amount of paid (unlocked) tokens
Holding Management Events
HoldingTokenTypeUpdated(address indexed admin, address indexed wallet, uint256 oldTokenType, uint256 newTokenType, uint256 mintTimestamp, uint256 amount)
Emitted when a holding's token type is updated.
Parameters:
admin(address, indexed): The admin who made the updatewallet(address, indexed): The wallet whose holding was updatedoldTokenType(uint256): The previous token typenewTokenType(uint256): The new token typemintTimestamp(uint256): The mint timestamp of the holdingamount(uint256): The amount updated
TimelockTokenTypeUpdated(address indexed admin, address indexed wallet, uint256 timelockIndex, uint256 oldTokenType, uint256 newTokenType, uint256 amount)
Emitted when a timelock's token type is updated.
Parameters:
admin(address, indexed): The admin who made the updatewallet(address, indexed): The wallet whose timelock was updatedtimelockIndex(uint256): The timelock indexoldTokenType(uint256): The previous token typenewTokenType(uint256): The new token typeamount(uint256): The timelock amount
Custom Errors
ERC20 Errors
ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed)
Thrown when there is insufficient allowance for a transfer.
ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed)
Thrown when there is insufficient balance for a transfer.
ERC20InvalidApprover(address approver)
Thrown when an invalid address tries to approve.
ERC20InvalidReceiver(address receiver)
Thrown when tokens are sent to an invalid receiver.
ERC20InvalidSender(address sender)
Thrown when an invalid address tries to send tokens.
ERC20InvalidSpender(address spender)
Thrown when an invalid spender is specified.
Access Control Errors
EasyAccessControl_DoesNotHaveReserveAdminRole(address addr)
Thrown when an address lacks the reserve admin role.
EasyAccessControl_DoesNotHaveTransferAdminRole(address addr)
Thrown when an address lacks the transfer admin role.
EasyAccessControl_DoesNotHaveContractAdminRole(address addr)
Thrown when an address lacks the contract admin role.
EasyAccessControl_DoesNotHaveSoftBurnAdminRole(address addr)
Thrown when an address lacks the soft burn admin role.
EasyAccessControl_DoesNotHaveAdminRole(address addr)
Thrown when an address lacks any required admin role.
EasyAccessControl_InvalidZeroAddress()
Thrown when a zero address is provided where not allowed.
Token Operation Errors
RestrictedLockupToken_InvalidAmount()
Thrown when an invalid amount is specified.
RestrictedLockupToken_InvalidZeroAmount()
Thrown when zero amount is provided where not allowed.
RestrictedLockupToken_InvalidZeroAddress()
Thrown when zero address is provided where not allowed.
RestrictedLockupToken_CannotExceedMaxTotalSupply()
Thrown when minting would exceed the maximum total supply.
RestrictedLockupToken_InsufficientTotalBalanceOf()
Thrown when there is insufficient total balance for an operation.
RestrictedLockupToken_InsufficientBurnableBalance()
Thrown when there is insufficient burnable balance.
RestrictedLockupToken_SafeApprove()
Thrown when safeApprove is called with invalid parameters.
Token Type Errors
RestrictedLockupToken_InvalidTokenType()
Thrown when an invalid token type is specified.
RestrictedLockupToken_TokenTypeNotAllowedForRecipient()
Thrown when a token type is not allowed for a specific recipient.
Timelock Errors
RestrictedLockupToken_InvalidTimelock()
Thrown when an invalid timelock is referenced.
RestrictedLockupToken_TimelockCannotBeCanceled()
Thrown when attempting to cancel a non-cancelable timelock.
RestrictedLockupToken_TimelockHasNoValueRemaining()
Thrown when a timelock has no remaining value.
RestrictedLockupToken_AmountExceedsUnlockedBalance()
Thrown when trying to transfer more than the unlocked balance.
RestrictedLockupToken_ScheduleIdDoesNotMatch()
Thrown when the schedule ID doesn't match expected value.
RestrictedLockupToken_CommencementTimestampDoesNotMatch()
Thrown when the commencement timestamp doesn't match expected value.
RestrictedLockupToken_TotalAmountDoesNotMatch()
Thrown when the total amount doesn't match expected value.
RestrictedLockupToken_InvalidReclaimTo()
Thrown when an invalid reclaim address is specified.
Holding Errors
RestrictedLockupToken_HoldingIndexOutOfBound()
Thrown when a holding index is out of bounds.
RestrictedLockupToken_InvalidHolding()
Thrown when an invalid holding is referenced.
RestrictedLockupToken_NoItemWithEnoughBalance()
Thrown when no holding has sufficient balance.
RestrictedLockupToken_InvalidHoldingIndex()
Thrown when an invalid holding index is provided.
Configuration Errors
RestrictedLockupToken_InvalidName()
Thrown when an invalid token name is provided.
RestrictedLockupToken_InvalidSymbol()
Thrown when an invalid token symbol is provided.
RestrictedLockupToken_InvalidTransferRules()
Thrown when an invalid transfer rules address is provided.
RestrictedLockupToken_InvalidAccessControl()
Thrown when an invalid access control address is provided.
RestrictedLockupToken_InvalidTrustedForwarder()
Thrown when an invalid trusted forwarder address is provided.
RestrictedLockupToken_InvalidIdentityRegistry()
Thrown when an invalid identity registry address is provided.
RestrictedLockupToken_InvalidRestrictedLockupTokenManagementExtension()
Thrown when an invalid management extension address is provided.
RestrictedLockupToken_InvalidRestrictedLockupTokenExtension()
Thrown when an invalid extension address is provided.
RestrictedLockupToken_InvalidMinTimelockAmount()
Thrown when an invalid minimum timelock amount is provided.
RestrictedLockupToken_MaxTotalSupplyTooLarge()
Thrown when the maximum total supply is too large.
RestrictedLockupToken_NewMaxTotalSupplyMustExceedCurrentTotalSupply()
Thrown when the new max supply doesn't exceed current supply.
RestrictedLockupToken_ExtensionSlotsPerWordMismatch(uint256 expected, uint256 actual)
Thrown when extension has mismatched slotsPerWord configuration.
RestrictedLockupToken_AlreadySet()
Thrown when trying to set a value that's already set to the same value.
Transfer Restriction Errors
RestrictedLockupToken_SenderCannotBeRecipient(address addr)
Thrown when sender and recipient are the same address.
RestrictedLockupToken_RecipientAndAmountLengthsShouldMatch()
Thrown when recipient and amount arrays have different lengths.
RestrictedLockupToken_NewTransferRulesContractDoesNotImplementITransferRules()
Thrown when new transfer rules contract doesn't implement required interface.
RestrictedLockupToken_NewIdentityRegistryContractDoesNotImplementIIdentityRegistry()
Thrown when new identity registry contract doesn't implement required interface.
Holder Management Errors
RestrictedLockupToken_HolderAddressDoesNotExist()
Thrown when a holder address doesn't exist.