Skip to main content
The Stock Token suite consists of three deployable contracts and two interfaces. This page covers architecture, state layout, and the complete function reference for each contract.

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

The initializer grants 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

Mints 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.
Issues tokens to up to 200 recipients in a single transaction. Processing stops early if gas drops below 80,000 per item. Emits BatchIssueCompleted(processedCount, total, reason) so callers can detect partial completion.

Redemption

Burns 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.
Burns tokens from a specific account. The sub-issuer’s outstanding counter decrements, limiting how much any single sub-issuer can redeem. Only unfrozen (transferable) tokens can be redeemed.

Transfer Compliance

The standard ERC-3643 transfer and transferFrom functions are overridden to call _validateTransfer before execution. This internal function calls canTransfer and reverts if the result is not SUCCESS.
Pre-checks whether a transfer would succeed. Returns a machine-readable TransferRestrictionCode and a human-readable reason string. The check order:
  1. Contract paused? -> PAUSED
  2. Sufficient balance? -> INSUFFICIENT_BALANCE
  3. Sufficient transferable balance (after frozen)? -> SENDER_TOKENS_FROZEN
  4. Sender verified? (skipped for authorized contracts) -> SENDER_NOT_ELIGIBLE
  5. Receiver verified? (skipped for authorized contracts) -> RECEIVER_NOT_ELIGIBLE
  6. Compliance module allows? -> COMPLIANCE_REJECTED

Token Freezing

Freezing locks a specified amount on an account. The 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

Moves tokens between addresses regardless of sender approval, for regulatory seizure or recovery. The recipient must still be identity-verified or an authorized contract. If the sender has frozen tokens, the frozen amount is reduced proportionally (by the lesser of amount and frozen).

Stock Split

Executes a forward stock split. Only forward splits are supported (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().
Reverse splits are not supported because integer division would destroy fractional shares. Use redemption and re-issuance for reverse splits. If any holders are omitted from the array, their balances will not be adjusted - the issuer is legally required to maintain a complete holder register under eWpG.

Configuration

View Functions

Events


IdentityRegistryUpgradeable

MiCA/eWpG-compliant KYC/AML registry shared across all token contracts in an instance.

Initialization

The 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.
Batch verification is available via 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.
Claims can be revoked individually via revokeClaim(). The getClaims() view returns only active claims.

Transfer Compliance Check

Returns a 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

Freezing an address at the registry level blocks all transfers for that identity across all tokens using this registry. This is distinct from token-level freezing (which locks a specific amount on one token).

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 every canTransfer call:
  1. Sender lockup. If the sender’s lockup has not expired, the transfer is rejected with SENDER_LOCKED.
  2. 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.
  3. Recipient maximum holding. If the recipient’s balance after the transfer exceeds the maximum, the transfer is rejected with RECIPIENT_EXCEEDS_MAX.
Per-user overrides take precedence over defaults. Set per-user values to 0 to fall back to the global default.

Configuration (called by token contract)

View Functions