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 viafundReleaseSchedule
ormintReleaseSchedule
that 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 ofunlockedBalanceOf
andsuperBalanceOf
, ie the total amount of tokens available to the wallet to transfer. Includes unlocked tokens across all timelocks.lockedBalanceOf
: tokens that are ONLY supplied viafundReleaseSchedule
ormintReleaseSchedule
that 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
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, tokenType);
// Get specific holding details
(uint256 balance, uint256 mintTimestamp) = token.holdingOf(investor, tokenType, index);
Transfer Validation
// Check if transfer is allowed for specific token type
uint8 restrictionCode = token.detectTransferRestrictionFor(
tokenType,
mintTimestamp,
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/freeze
functions, automatically checking that recipient Wallet addresses of tokens are cataloged and assignedholderId
s as needed. - If a "new" Wallet address receives a token, a new
holderId
is 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 totalTokenSupply()
, 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
execute
orexecuteBatch
on theERC2771Forwarder
contract 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 to0
for permanent validity or duration in seconds (e.g.,31536000
for 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
transferRules.setTransferRule(2, 0, 0, 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
transferRules.setTransferRule(1, 0, 0, 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 general holding period (offshore placements)
transferRules.setTransferRule(1, 0, 0, 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
transferRules.setTransferRule(4, 0, 0, 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.
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
minTimelockAmount
must be greater than 0maxTotalSupply
must not exceed maximum safe supply- Extension contracts must have matching
slotsPerWord
configuration
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
totalTokenSupply() → 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
- Transfer restrictions must be satisfied for reclaim address
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
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
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
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.
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_TokenTypeNotAllowed()
Thrown when a token type is not allowed for a recipient.
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.