Contract Relationships
StockToken holds a reference to IdentityRegistryUpgradeable and an optional BasicComplianceModule. On every transfer, the token contract calls isVerified() on the registry for both parties (unless one is an authorized contract), then calls canTransfer() on the compliance module if one is set.
The identity registry can itself hold references to multiple compliance modules via addComplianceModule(). When the registry’s own canTransfer() is called (by external integrators checking compliance at the registry level), it iterates all registered modules. The token contract’s direct compliance module reference is separate - it allows per-token compliance configuration independent of the registry-level modules.
StockToken
UUPS-upgradeable ERC-3643 with transfer restrictions, corporate actions, and sub-issuer architecture.Initialization
DEFAULT_ADMIN_ROLE and ISSUER_ROLE to the issuer address. LEGAL_OPERATOR_ROLE goes to a separate address representing the licensed legal entity. The split ratio starts at 1:1.
Roles
State Variables
Issuance
amount tokens to to. The recipient must be identity-verified or an authorized contract. The sub-issuer’s net outstanding counter increments by amount; if a cap is set and would be exceeded, the call reverts with SubIssuerCapExceeded.
The reason parameter is a bytes32 code for audit trails - define your own encoding scheme or use standardized codes.
BatchIssueCompleted(processedCount, total, reason) so callers can detect partial completion.
Redemption
amount from the caller’s balance. Restricted to ISSUER_ROLE and SUB_ISSUER_ROLE - regular holders cannot self-redeem (eWpG register control requirement). If the caller is a sub-issuer, their outstanding counter decrements; the call reverts with SubIssuerRedemptionExceeded if amount exceeds their net outstanding.
Transfer Compliance
The standard ERC-3643transfer and transferFrom functions are overridden to call _validateTransfer before execution. This internal function calls canTransfer and reverts if the result is not SUCCESS.
TransferRestrictionCode and a human-readable reason string. The check order:
- Contract paused? ->
PAUSED - Sufficient balance? ->
INSUFFICIENT_BALANCE - Sufficient transferable balance (after frozen)? ->
SENDER_TOKENS_FROZEN - Sender verified? (skipped for authorized contracts) ->
SENDER_NOT_ELIGIBLE - Receiver verified? (skipped for authorized contracts) ->
RECEIVER_NOT_ELIGIBLE - Compliance module allows? ->
COMPLIANCE_REJECTED
Token Freezing
transferableBalance of that account decreases accordingly. Frozen amounts cannot exceed the account’s total balance. Unfreezing requires that the amount does not exceed the currently frozen amount.
Force Transfer
amount and frozen).
Stock Split
numerator must exceed denominator). For each holder in the array, the contract calculates (currentBalance * numerator) / denominator, mints the difference, and adjusts frozen token amounts proportionally (floor division).
The cumulative split ratio is updated and reduced by GCD after each split. Query it via splitRatio().
Configuration
View Functions
Events
IdentityRegistryUpgradeable
MiCA/eWpG-compliant KYC/AML registry shared across all token contracts in an instance.Initialization
gnosisSafe address receives DEFAULT_ADMIN_ROLE and manages all role assignments. The legalOperator receives LEGAL_OPERATOR_ROLE for upgrade authorization.
Roles
Identity Lifecycle
Identities move through a defined lifecycle: verification, optional updates, soft expiry (KYC refresh needed), hard expiry (transfers blocked), and revocation.batchVerifyIdentities() for up to 500 identities per call, with gas-aware early termination and per-entry validation. Failed entries are returned as an index array.
Claims
Claims are typed attestations attached to an identity. They support the full range of MiCA reporting requirements.revokeClaim(). The getClaims() view returns only active claims.
Transfer Compliance Check
TransferCheckResult with allowed, reason (machine-readable TransferRejectionReason), and the KYC status of both parties. The check runs identity validation for both parties, then iterates all registered compliance modules. The registry fails closed on compliance module errors.
Address Freezing
BasicComplianceModule
Stateless compliance module that enforces holding limits and lockup periods. Deployed per token contract, controlled by the token contract (no separate admin keys).Constructor
Rules
The module checks three conditions on everycanTransfer call:
- Sender lockup. If the sender’s lockup has not expired, the transfer is rejected with
SENDER_LOCKED. - Sender minimum holding. After the transfer, if the sender retains a non-zero balance below the minimum, the transfer is rejected with
BELOW_MIN_HOLDING. Emptying an account entirely (balance going to zero) is always allowed. - Recipient maximum holding. If the recipient’s balance after the transfer exceeds the maximum, the transfer is rejected with
RECIPIENT_EXCEEDS_MAX.
