Skip to main content
This page documents every contract in the Commodity Token License. Each section covers the contract’s purpose, key state variables, functions, and events. For deployment ordering and configuration, see Integration.

Factory

CommodityTokenFactoryUpgradeable

Deploys commodity tokens as UUPS proxies sharing a common implementation contract. The factory is itself upgradeable (UUPS) and controlled by the TrussetDAO admin address (typically a Gnosis Safe). State
VariableTypeDescription
adminaddressTrussetDAO multisig controlling the factory
commodityTokenImplementationaddressCurrent default implementation for new deployments
identityRegistryaddressDefault KYC registry passed to new tokens
platformWalletaddressPlatform fee wallet passed to new tokens
approvedImplementationsmapping(address => bool)Implementations approved for token upgrades
allTokensaddress[]All deployed token proxy addresses
isFactoryTokenmapping(address => bool)Quick lookup for factory-deployed tokens
Key Functions Events: CommodityTokenCreated, ImplementationUpdated, ImplementationApproved, AdminUpdated, PlatformWalletUpdated, IdentityRegistryUpdated

Token

CommodityTokenUpgradeable

The core ERC-20 token contract. Each instance represents a single commodity-backed token with its own reserve state, issuer hierarchy, fee configuration, and compliance settings. Deployed as a UUPS proxy via the factory. Inherits: Initializable, UUPSUpgradeable, EIP712Upgradeable, ReentrancyGuardUpgradeable

Access Control

The token uses a three-tier access model:
  • Main Issuer - full control over all token operations, fee configuration, compliance settings, and issuer role transfer. Set at initialization, transferable via two-step process (transferMainIssuer then acceptMainIssuer).
  • Delegates - can perform most issuer operations except transferring the main issuer role or setting the upgrade authority. Managed via setDelegate / setDelegatesBatch.
  • Sub-Issuers - configured via configureIssuer with granular isMinter / isBurner flags and per-address mintLimit caps.

Reserve Enforcement

Every token is backed by gramsPerToken grams of the underlying commodity (scaled to 18 decimals). The contract enforces this invariant:
totalSupply * gramsPerToken <= totalGramsReserve * 1e18
This check runs on every mint. totalGramsReserve increases when mint requests specify gramsAdded, and decreases on physical redemption. Reserve metadata (vaultLocationHash, reserveAttestationURI) is updatable by the issuer for off-chain audit linkage.

Minting

Minting uses a request system. The caller creates a MintRequest specifying the recipient, amount, grams added to reserve, and one of four request types:
TypeBehavior
INSTANTAuto-approved and executed in the same transaction
MANUAL_APPROVALRequires explicit approveMintRequest call before execution
TIMELOCKExecutable only after timelockSeconds have elapsed
CONDITIONALRequires an EIP-712 signature from the main issuer or delegate
Sub-issuers creating mint requests are checked against their mintLimit. The gramsAdded parameter must cover (amount * gramsPerToken) / 1e18 or the transaction reverts with InsufficientGramsAdded. mintBatch allows minting to multiple recipients in a single transaction with proportional grams distribution. The last recipient in the batch receives any rounding remainder.

Redemption

Redemption also uses the request system. A holder calls createBurnRequest which locks the specified tokens (they remain in _balances but are tracked in lockedBalances and excluded from transferable balance). Physical redemption (burnTokens = true): tokens are burned, totalSupply and totalGramsReserve decrease. If unitsEnabled is true, the redeemed grams must be divisible by unitWeightGrams (e.g., 1000 for 1kg bars). A minTokensForAsset threshold can enforce minimum redemption sizes. Cash redemption (burnTokens = false): tokens are burned, the contract pays the holder in USDC at the current oracle price. The oracle must implement IPriceOracle.getPrice() returning (price, decimals, timestamp). The contract normalizes from 18-decimal token amounts to USDC decimals:
usdcAmount = (tokenAmount * price) / 10^(18 + priceDecimals - usdcDecimals)
Oracle staleness is enforced via maxOracleAge (default 3600 seconds). The USDC must be pre-funded in the token contract.

Fees

Mint and burn fees are configured independently in basis points (max 1000 = 10%). Fees are split 75% to the mainIssuer and 25% to the platformWallet (Trusset treasury). The platform wallet is updatable only by the upgradeAuthority (TrussetDAO), not by the issuer.

Transfer Compliance

Transfers are checked against two independent layers:
  1. Identity Registry (if kycRequired is true and identityRegistry is set): calls IIdentityRegistry.canTransfer() which validates identity status, expiry, freeze state, and runs all attached compliance modules. The full TransferCheckResult includes sender/receiver KYC status and machine-readable rejection reasons.
  2. Whitelist/Blacklist (issuer-controlled via transferRestrictionMode):
    • NONE - no restrictions
    • WHITELIST - only whitelisted recipients can receive tokens
    • BLACKLIST - blacklisted addresses cannot send or receive
Both layers support batch operations (setWhitelistBatch, setBlacklistBatch).

Upgrade Governance

Token upgrades follow a propose-accept pattern:
1

DAO proposes

The upgradeAuthority (TrussetDAO) calls proposeUpgrade(newImplementation). This sets pendingImplementation and resets upgradeAccepted to false.
2

Issuer decides

The mainIssuer calls acceptUpgrade() or rejectUpgrade(). Rejection clears the pending implementation.
3

Upgrade executes

After acceptance, the issuer calls upgradeToAndCall (inherited from UUPS). The _authorizeUpgrade function verifies both proposal and acceptance before allowing the proxy to point to the new implementation.
Neither the DAO nor the issuer can upgrade unilaterally.

Administrative Functions

pause / unpause halt all transfers, mints, and burns. freezeAccount / unfreezeAccount target individual addresses. setKYCConfig toggles KYC enforcement and updates the registry address. All administrative functions are callable by the main issuer or delegates. Events: Transfer, Approval, Minted, Redeemed, ReserveUpdated, Paused, Unpaused, AccountFrozen, AccountUnfrozen, MintRequestCreated, MintRequestApproved, MintRequestExecuted, BurnRequestCreated, BurnRequestApproved, BurnRequestExecuted, RequestCancelled, FeesCollected, CashRedemption, UpgradeProposed, UpgradeAccepted, UpgradeRejected, FeesUpdated, MainIssuerTransferred, DelegateUpdated, WhitelistUpdated, BlacklistUpdated, TransferRestrictionModeUpdated

Identity Registry

IdentityRegistryUpgradeable

MiCA/eWpG-compliant identity management for regulated token transfers. Manages investor verification, claims, and transfer compliance with role-based access designed for German regulatory requirements. Roles
RolePurposeTypical Holder
DEFAULT_ADMIN_ROLEManages all other roles, compliance modules, pauseGnosis Safe multisig
LEGAL_OPERATOR_ROLEAuthorizes contract upgradesNamed legal entity (BaFin requirement)
KYC_PROVIDER_ROLEVerifies/updates identities, manages claimsLicensed KYC service
REGISTER_OPERATOR_ROLERevokes identitiesSecurity register operator
COMPLIANCE_ROLEFreezes/unfreezes addressesCompliance officer
Identity Lifecycle Each identity stores a kycHash (SHA-256 of off-chain KYC data), an InvestorType (MiFID II classification: RETAIL, PROFESSIONAL, or ELIGIBLE_COUNTERPARTY), the verifying provider, and two expiry timestamps:
  • softExpiry - KYC needs refresh but transfers still allowed
  • hardExpiry - KYC is invalid, transfers blocked
KYC status transitions: ACTIVE -> SOFT_EXPIRED -> HARD_EXPIRED. Revocation sets status to REVOKED permanently. Claims Claims are typed attestations attached to an identity. Supported types: KYC, AML, ACCREDITATION, RESIDENCY, CITIZENSHIP, TAX_RESIDENCY, SANCTIONS_CHECK, PEP_CHECK. Each claim stores a zero-knowledge dataHash, the issuing provider, and an optional expiry. Transfer Compliance canTransfer validates both sender and receiver against identity status, expiry, freeze state, and all registered compliance modules. It returns a TransferCheckResult with:
  • allowed - boolean
  • reason - machine-readable TransferRejectionReason enum
  • senderStatus / receiverStatus - current KYCStatus for both parties
Compliance modules are called in sequence. If any module rejects or throws, the transfer is blocked (fail-closed). Batch Operations batchVerifyIdentities processes up to 500 identities per call with gas-aware iteration - the loop breaks if remaining gas drops below 50,000 per item. Returns successCount and failedIndices for partial-success handling. batchRevokeIdentities supports up to 1,000 revocations per call.

Compliance Module

BasicComplianceModule

A stateless compliance module enforcing holding limits and lockup periods. Deployed per token contract - the token contract is the sole authorized caller. Rules
  • Maximum holding: prevents a recipient’s post-transfer balance from exceeding the cap. Per-user overrides fall back to defaultMaxHolding.
  • Minimum holding: prevents a sender from retaining a balance below the floor (unless they empty the account entirely). Per-user overrides fall back to defaultMinHolding.
  • Lockup: blocks all outbound transfers from an address until the lockup timestamp expires.
The module implements IComplianceModule.canTransfer and returns machine-readable ComplianceRejectionReason values: SENDER_LOCKED, BELOW_MIN_HOLDING, RECIPIENT_EXCEEDS_MAX, or NONE. getUserLimits returns the effective max, effective min, and lock status for any address.

Sale

CommodityTokenSale

Primary market sale contract supporting fixed-price and oracle-priced commodity token sales. Each sale contract is bound to a single commodity token and can host multiple sale rounds identified by bytes32 sale IDs. Sale Configuration Each sale round specifies:
FieldDescription
mintOnPurchaseIf true, calls createMintRequest on the token. If false, transfers pre-funded tokens.
fixedPriceUsdFixed USD price per token (18 decimals). Set 0 to use oracle.
priceOracleIPriceOracle address for dynamic pricing
paymentTokenERC-20 address for payment, or address(0) for native ETH
minPurchase / maxPurchasePer-purchase minimum and per-buyer maximum (in tokens)
totalAllocationTotal tokens available in this round
feeBpsSale fee in basis points (max 500 = 5%), split 75/25 issuer/platform
startTime / endTimeSale window (0 = no constraint)
mintRequestTypeRequest type passed to createMintRequest (0-3)
Purchase Flow Buyers call purchase(saleId, tokenAmount, maxPayment) where maxPayment provides slippage protection. The contract calculates the payment amount from the configured price source, adds the fee, and verifies it does not exceed maxPayment. For ETH payments, excess msg.value is refunded. For mintOnPurchase sales, the sale contract must be configured as an authorized minter on the commodity token (via configureIssuer). The gramsNeeded for the mint request is computed from the token’s gramsPerToken. Pre-funded sales require the issuer to call fundSale to transfer tokens into the sale contract before purchases begin. Unsold tokens can be recovered via withdrawUnsoldTokens (deactivates the sale).

Sale Factory

CommodityTokenSaleFactory

Deploys CommodityTokenSale instances. Admin-gated - only the factory admin can create sale contracts. Tracks deployments per issuer and per commodity token, supporting multiple sale rounds per token. Key Functions
  • createSaleContract(commodityToken, issuer) - deploys a new sale contract with the factory’s platformWallet
  • getTokenSaleContracts(commodityToken) - returns all sale contracts for a specific token
  • getIssuerSaleContracts(issuer) - returns all sale contracts for an issuer
  • getSaleContractsPaginated(offset, limit) - paginated listing of all deployments

Interfaces

The suite includes four interfaces used for cross-contract communication:
  • ICommodityToken - minimal interface for sale contracts to call createMintRequest, transfer, gramsPerToken, and other token functions
  • IIdentityRegistry - full identity registry interface including all enums (InvestorType, KYCStatus, ClaimType, TransferRejectionReason), structs (Claim, TransferCheckResult), and function signatures
  • IComplianceModule - single canTransfer function with ComplianceRejectionReason return
  • IPriceOracle - getPrice() returning (price, decimals, timestamp) for USD-denominated price feeds