Skip to main content
This page covers the end-to-end deployment and configuration of the Commodity Token License contracts. The deployment sequence matters - contracts reference each other and must be deployed in the correct order.

Deployment Sequence

1

Deploy the Identity Registry

Deploy IdentityRegistryUpgradeable as a UUPS proxy. Initialize with your Gnosis Safe multisig as gnosisSafe (receives DEFAULT_ADMIN_ROLE) and a named legal entity as legalOperator (receives LEGAL_OPERATOR_ROLE, required by BaFin for upgrade authorization).After initialization, grant roles:
  • KYC_PROVIDER_ROLE to your licensed KYC service via addKYCProvider(address, name)
  • REGISTER_OPERATOR_ROLE to your register operator via addRegisterOperator(address, name)
  • COMPLIANCE_ROLE to your compliance officer(s) via addComplianceOfficer(address)
2

Deploy the Token Implementation

Deploy CommodityTokenUpgradeable as a standalone contract (not behind a proxy). This serves as the shared implementation that all token proxies will point to. Do not call initialize on the implementation - the constructor calls _disableInitializers().
3

Deploy the Factory

Deploy CommodityTokenFactoryUpgradeable as a UUPS proxy. Initialize with:
  • _identityRegistry - address from Step 1
  • _commodityTokenImplementation - address from Step 2
  • _admin - TrussetDAO Gnosis Safe address
  • _platformWallet - Trusset treasury address for fee collection
4

Create Tokens

Call createCommodityToken or createSimpleCommodityToken on the factory. Each call deploys a new ERC1967Proxy and initializes the token with its own mainIssuer, gramsPerToken, maxSupply, and compliance settings.The mainIssuer address receives full operational control. The upgradeAuthority is set to the factory’s admin (TrussetDAO).
5

Deploy Compliance Module (optional)

Deploy BasicComplianceModule with the token contract address from Step 4, defaultMaxHolding (set 0 for unlimited), and defaultMinHolding.Register the module on the identity registry via addComplianceModule(moduleAddress).
BasicComplianceModule takes tokenContract as an immutable constructor parameter. Deploy it after the token exists, or use deterministic deployment (CREATE2) to predict the token address in advance.
6

Deploy Sale Infrastructure (optional)

Deploy CommodityTokenSaleFactory with the platform wallet. Create sale contracts via createSaleContract(tokenAddress, issuerAddress).For mint-on-purchase sales, the sale contract must be registered as an authorized minter on the token: call configureIssuer(saleContractAddress, true, false, 0) on the token as the main issuer.

Token Configuration

After deployment, the main issuer (or delegates) should configure the token for their use case.

Fees

// Set 1% mint fee and 0.5% burn fee
token.setFees(100, 50);
Fees are in basis points. Maximum is 1000 (10%) per operation. The 75/25 split between issuer and platform is fixed at the protocol level.

KYC and Transfer Restrictions

// Enable KYC enforcement
token.setKYCConfig(true, identityRegistryAddress);

// Or use whitelist mode instead
token.setTransferRestrictionMode(1); // WHITELIST
token.setWhitelistBatch(addresses, statuses);
KYC enforcement and whitelist/blacklist restrictions are independent. You can use both simultaneously - transfers must pass both checks.

Redemption

token.setRedemptionConfig(
    true,           // unitsEnabled - require whole-unit redemption
    1000,           // unitWeightGrams - 1kg bars
    100e18,         // minTokensForAsset - minimum 100 tokens for physical
    true,           // cashRedemptionEnabled
    usdcAddress,    // USDC token address
    oracleAddress,  // IPriceOracle implementation
    3600            // maxOracleAge in seconds
);
For cash redemption to work, the token contract must hold sufficient USDC. Transfer USDC to the token contract address before enabling cash redemption.

Sub-Issuers and Delegates

// Add a delegate (can do most issuer operations)
token.setDelegate(delegateAddress, true);

// Add a sub-issuer with mint authority and a 1M token limit
token.configureIssuer(subIssuerAddress, true, false, 1_000_000e18);
Delegates have broad permissions but cannot transfer the main issuer role. Sub-issuers have narrow, configurable permissions with mint caps.

Price Oracle

The suite expects a price oracle implementing IPriceOracle:
interface IPriceOracle {
    function getPrice() external view returns (
        uint256 price,    // USD price scaled by 10^decimals
        uint8 decimals,   // decimal precision of the price
        uint256 timestamp // last update time
    );
}
You must deploy your own oracle implementation or adapt an existing one (e.g., Chainlink). The oracle is used by both the token’s cash redemption and the sale contract’s dynamic pricing. Staleness is enforced - if block.timestamp - timestamp > maxOracleAge, the transaction reverts with OraclePriceStale.

Sale Configuration

Creating a Sale Round

saleContract.createSale(
    saleId,             // bytes32 unique identifier
    true,               // mintOnPurchase
    0,                  // fixedPriceUsd (0 = use oracle)
    oracleAddress,      // priceOracle
    usdcAddress,        // paymentToken (address(0) for ETH)
    10e18,              // minPurchase - 10 tokens minimum
    10_000e18,          // maxPurchase per buyer
    1_000_000e18,       // totalAllocation
    200,                // feeBps - 2% sale fee
    block.timestamp,    // startTime
    block.timestamp + 30 days, // endTime
    "ipfs://...",       // metadataUri
    0                   // mintRequestType - INSTANT
);
For mintOnPurchase sales, ensure the sale contract has minter permissions on the token before the sale starts. For pre-funded sales, call fundSale(saleId, amount) after creating the sale round.

Payment Handling

ETH sales: buyers send ETH with their purchase call. The contract forwards payment to the issuer and refunds any excess. ERC-20 sales: buyers must approve the sale contract for the total payment amount (including fees) before calling purchase. The contract pulls payment via safeTransferFrom.

Identity Registry Configuration

Verifying Investors

The KYC provider verifies investors before they can receive or transfer tokens:
identityRegistry.verifyIdentity(
    userAddress,
    kycHash,           // SHA-256 of off-chain KYC data
    InvestorType.RETAIL,
    uint64(block.timestamp + 365 days), // softExpiry
    uint64(block.timestamp + 730 days)  // hardExpiry
);
For onboarding at scale, use batchVerifyIdentities (up to 500 per call). The function returns successCount and failedIndices for partial-failure handling.

Adding Claims

identityRegistry.addClaim(
    userAddress,
    ClaimType.ACCREDITATION,
    keccak256(abi.encodePacked(accreditationData)),
    uint64(block.timestamp + 365 days) // expiry, 0 for permanent
);

Compliance Modules

After deploying BasicComplianceModule:
// Register on identity registry
identityRegistry.addComplianceModule(complianceModuleAddress);

// Configure limits (called by the token contract, restricted by onlyToken modifier)
complianceModule.setDefaultLimits(1_000_000e18, 100e18); // max 1M, min 100
complianceModule.setLockup(investorAddress, block.timestamp + 180 days);
Custom compliance modules can be built by implementing IComplianceModule.canTransfer. Register them on the identity registry alongside or instead of BasicComplianceModule.

Reserve Management

The issuer maintains reserve attestation data on-chain:
token.updateReserveAttestation(
    totalGrams,         // must satisfy reserve ratio
    vaultLocationHash,  // keccak256 of vault identifier
    "https://..."       // URI to attestation document
);
The contract enforces that the new totalGrams still satisfies totalSupply * gramsPerToken <= totalGrams * 1e18. You cannot decrease reserves below what the current supply requires. isReserveCompliant() is a public view function that returns whether the reserve ratio holds - useful for monitoring and off-chain verification.