Skip to main content
This page covers how to deploy, configure, and integrate with the Stock Token contract suite. It assumes you have the license source code and a deployment environment targeting an EVM-compatible chain.

Deployment Sequence

The contracts must be deployed in order due to their interdependencies. Each contract uses a UUPS proxy pattern, so you deploy a proxy pointing to an implementation contract, then call initialize() on the proxy.
1
Deploy IdentityRegistryUpgradeable
2
Deploy the implementation, then a UUPS proxy. Initialize with a Gnosis Safe (multi-sig) as DEFAULT_ADMIN_ROLE and a legal operator address for upgrade authorization.
3
identityRegistry.initialize(gnosisSafe, legalOperator);
4
After initialization, add your KYC provider, register operator, and compliance officer via the Safe:
5
identityRegistry.addKYCProvider(kycProviderAddress, "Provider Name");
identityRegistry.addRegisterOperator(operatorAddress, "Operator Name");
identityRegistry.addComplianceOfficer(complianceOfficerAddress);
6
Deploy StockToken
7
Deploy the implementation and proxy. Initialize with the identity registry address, issuer, and legal operator.
8
stockToken.initialize(
    "Acme Corp Class A",           // name
    "ACME-A",                      // symbol
    "DE000A0D9PT0",                // ISIN
    "https://example.com/meta.json", // metadata URI
    address(identityRegistry),     // identity registry
    issuerAddress,                 // receives ISSUER_ROLE + DEFAULT_ADMIN_ROLE
    legalOperatorAddress           // receives LEGAL_OPERATOR_ROLE
);
9
Deploy BasicComplianceModule (optional)
10
If you need holding limits or lockup periods, deploy the compliance module with the token address and default limits.
11
BasicComplianceModule compliance = new BasicComplianceModule(
    address(stockToken),
    0,    // default max holding (0 = unlimited)
    0     // default min holding
);
12
Then link it to the token:
13
stockToken.setComplianceModule(address(compliance));
14
Configure Sub-issuers
15
Grant SUB_ISSUER_ROLE to addresses that will mint tokens, and optionally set caps.
16
stockToken.addSubIssuer(subIssuerAddress);
stockToken.setSubIssuerCap(subIssuerAddress, 1_000_000 * 1e18); // cap at 1M tokens
17
Configure Controllers
18
Grant CONTROLLER_ROLE to addresses responsible for regulatory enforcement.
19
stockToken.addController(controllerAddress);
20
Authorize Contracts (if applicable)
21
If you are deploying an orderbook, AMM pool, or lending contract that will hold tokens in custody, whitelist those contracts so they bypass identity checks.
22
stockToken.addAuthorizedContract(address(orderbook));
stockToken.addAuthorizedContract(address(lendingPool));

Verifying Identities

Before any tokens can be issued or transferred, both sender and receiver must have verified identities in the registry (unless they are authorized contracts).
identityRegistry.verifyIdentity(
    userAddress,
    keccak256(abi.encodePacked(kycDocumentHash)), // SHA256 of KYC data
    IIdentityRegistry.InvestorType.PROFESSIONAL,
    uint64(block.timestamp + 365 days),            // soft expiry
    uint64(block.timestamp + 730 days)             // hard expiry
);
For onboarding at scale, use batchVerifyIdentities() with up to 500 addresses per call. The function returns a count of successes and an array of failed indices for retry.

Issuing Tokens

With identities verified and sub-issuers configured, issue tokens:
// Single issuance
stockToken.issue(recipientAddress, 10_000 * 1e18, "INITIAL_OFFERING");

// Batch issuance (up to 200 recipients)
address[] memory recipients = new address[](3);
uint256[] memory amounts = new uint256[](3);
// ... populate arrays ...
uint256 processed = stockToken.batchIssue(recipients, amounts, "BATCH_OFFERING");
The reason parameter is bytes32 - use it for categorizing issuances in your audit trail. You can encode strings with bytes32("INITIAL_OFFERING") or use custom numeric codes.

Pre-checking Transfers

Before submitting a transfer on-chain, call canTransfer to check whether it would succeed. This avoids wasted gas on transactions that will revert.
(ISecurityToken.TransferRestrictionCode code, string memory reason) =
    stockToken.canTransfer(from, to, amount);

if (code != ISecurityToken.TransferRestrictionCode.SUCCESS) {
    // Handle rejection - `reason` contains human-readable explanation
    // `code` contains machine-readable enum for programmatic handling
}
This is a view function and costs no gas when called off-chain. Integrate it into your frontend to show users whether a transfer will succeed before they sign.

Configuring Compliance Rules

The BasicComplianceModule enforces holding limits and lockup periods. Configuration calls must come from the token contract, so you need to expose these through your issuer workflow.

Holding limits

Set global defaults and per-user overrides:
// Global: max 100K tokens per holder, no minimum
compliance.setDefaultLimits(100_000 * 1e18, 0);

// Override for a specific institutional holder: max 5M, min 10K
compliance.setUserLimits(institutionAddress, 5_000_000 * 1e18, 10_000 * 1e18);

Lockup periods

Lock tokens for a specific address until a timestamp:
// Lock until January 1, 2027
compliance.setLockup(founderAddress, 1798761600);
The lockup blocks all outgoing transfers from that address until the timestamp passes. To clear a lockup early, set the timestamp to 0.

Executing a Stock Split

Forward splits multiply all holder balances by a ratio. You must provide the complete list of current holders.
// 2:1 split
address[] memory holders = getHolderList(); // your holder registry
stockToken.stockSplit(2, 1, holders);
After a split, frozen token amounts are adjusted proportionally. The cumulative split ratio (queryable via splitRatio()) updates and simplifies by GCD.
Dependent contracts (orderbooks, lending pools, AMM pools) must be notified after a split. Listen for the StockSplit event and update any cached price or balance data accordingly.

Listening to Events

Index these events for your backend and reporting systems:
// Token lifecycle
stockToken.on("Issued", (to, amount, reason, subIssuer) => { /* ... */ });
stockToken.on("Redeemed", (from, amount, reason, redeemer) => { /* ... */ });
stockToken.on("ForceTransfer", (from, to, amount, reason, controller) => { /* ... */ });

// Regulatory actions
stockToken.on("TokensFrozen", (account, amount) => { /* ... */ });
stockToken.on("TokensUnfrozen", (account, amount) => { /* ... */ });

// Corporate actions
stockToken.on("StockSplit", (numerator, denominator, newTotalSupply) => { /* ... */ });
stockToken.on("BatchIssueCompleted", (processed, total, reason) => { /* ... */ });

// Identity events
identityRegistry.on("IdentityVerified", (user, kycHash, investorType, provider) => { /* ... */ });
identityRegistry.on("KYCStatusChanged", (user, oldStatus, newStatus) => { /* ... */ });
identityRegistry.on("AddressFrozen", (user, officer) => { /* ... */ });

API Integration

If you are using Trusset’s API layer, the token and identity registry are managed through the Tokenization and Customers API groups respectively. The API handles deployment, configuration, and lifecycle management without direct contract interaction. For direct contract integration (self-hosted or custom deployment), use standard ethers.js or viem patterns with the contract ABIs included in the license package.