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.
Deploy IdentityRegistryUpgradeable
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.
identityRegistry.initialize(gnosisSafe, legalOperator);
After initialization, add your KYC provider, register operator, and compliance officer via the Safe:
identityRegistry.addKYCProvider(kycProviderAddress, "Provider Name");
identityRegistry.addRegisterOperator(operatorAddress, "Operator Name");
identityRegistry.addComplianceOfficer(complianceOfficerAddress);
Deploy the implementation and proxy. Initialize with the identity registry address, issuer, and legal operator.
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
);
Deploy BasicComplianceModule (optional)
If you need holding limits or lockup periods, deploy the compliance module with the token address and default limits.
BasicComplianceModule compliance = new BasicComplianceModule(
address(stockToken),
0, // default max holding (0 = unlimited)
0 // default min holding
);
Then link it to the token:
stockToken.setComplianceModule(address(compliance));
Grant SUB_ISSUER_ROLE to addresses that will mint tokens, and optionally set caps.
stockToken.addSubIssuer(subIssuerAddress);
stockToken.setSubIssuerCap(subIssuerAddress, 1_000_000 * 1e18); // cap at 1M tokens
Grant CONTROLLER_ROLE to addresses responsible for regulatory enforcement.
stockToken.addController(controllerAddress);
Authorize Contracts (if applicable)
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.
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.