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| Variable | Type | Description |
|---|---|---|
admin | address | TrussetDAO multisig controlling the factory |
commodityTokenImplementation | address | Current default implementation for new deployments |
identityRegistry | address | Default KYC registry passed to new tokens |
platformWallet | address | Platform fee wallet passed to new tokens |
approvedImplementations | mapping(address => bool) | Implementations approved for token upgrades |
allTokens | address[] | All deployed token proxy addresses |
isFactoryToken | mapping(address => bool) | Quick lookup for factory-deployed tokens |
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 (
transferMainIssuerthenacceptMainIssuer). - 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
configureIssuerwith granularisMinter/isBurnerflags and per-addressmintLimitcaps.
Reserve Enforcement
Every token is backed bygramsPerToken grams of the underlying commodity (scaled to 18 decimals). The contract enforces this invariant:
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 aMintRequest specifying the recipient, amount, grams added to reserve, and one of four request types:
| Type | Behavior |
|---|---|
INSTANT | Auto-approved and executed in the same transaction |
MANUAL_APPROVAL | Requires explicit approveMintRequest call before execution |
TIMELOCK | Executable only after timelockSeconds have elapsed |
CONDITIONAL | Requires an EIP-712 signature from the main issuer or delegate |
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 callscreateBurnRequest 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:
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 themainIssuer 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:-
Identity Registry (if
kycRequiredis true andidentityRegistryis set): callsIIdentityRegistry.canTransfer()which validates identity status, expiry, freeze state, and runs all attached compliance modules. The fullTransferCheckResultincludes sender/receiver KYC status and machine-readable rejection reasons. -
Whitelist/Blacklist (issuer-controlled via
transferRestrictionMode):NONE- no restrictionsWHITELIST- only whitelisted recipients can receive tokensBLACKLIST- blacklisted addresses cannot send or receive
setWhitelistBatch, setBlacklistBatch).
Upgrade Governance
Token upgrades follow a propose-accept pattern:DAO proposes
The
upgradeAuthority (TrussetDAO) calls proposeUpgrade(newImplementation). This sets pendingImplementation and resets upgradeAccepted to false.Issuer decides
The
mainIssuer calls acceptUpgrade() or rejectUpgrade(). Rejection clears the pending implementation.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| Role | Purpose | Typical Holder |
|---|---|---|
DEFAULT_ADMIN_ROLE | Manages all other roles, compliance modules, pause | Gnosis Safe multisig |
LEGAL_OPERATOR_ROLE | Authorizes contract upgrades | Named legal entity (BaFin requirement) |
KYC_PROVIDER_ROLE | Verifies/updates identities, manages claims | Licensed KYC service |
REGISTER_OPERATOR_ROLE | Revokes identities | Security register operator |
COMPLIANCE_ROLE | Freezes/unfreezes addresses | Compliance officer |
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 allowedhardExpiry- KYC is invalid, transfers blocked
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- booleanreason- machine-readableTransferRejectionReasonenumsenderStatus/receiverStatus- currentKYCStatusfor both parties
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.
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 bybytes32 sale IDs.
Sale Configuration
Each sale round specifies:
| Field | Description |
|---|---|
mintOnPurchase | If true, calls createMintRequest on the token. If false, transfers pre-funded tokens. |
fixedPriceUsd | Fixed USD price per token (18 decimals). Set 0 to use oracle. |
priceOracle | IPriceOracle address for dynamic pricing |
paymentToken | ERC-20 address for payment, or address(0) for native ETH |
minPurchase / maxPurchase | Per-purchase minimum and per-buyer maximum (in tokens) |
totalAllocation | Total tokens available in this round |
feeBps | Sale fee in basis points (max 500 = 5%), split 75/25 issuer/platform |
startTime / endTime | Sale window (0 = no constraint) |
mintRequestType | Request type passed to createMintRequest (0-3) |
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
DeploysCommodityTokenSale 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’splatformWalletgetTokenSaleContracts(commodityToken)- returns all sale contracts for a specific tokengetIssuerSaleContracts(issuer)- returns all sale contracts for an issuergetSaleContractsPaginated(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 callcreateMintRequest,transfer,gramsPerToken, and other token functionsIIdentityRegistry- full identity registry interface including all enums (InvestorType,KYCStatus,ClaimType,TransferRejectionReason), structs (Claim,TransferCheckResult), and function signaturesIComplianceModule- singlecanTransferfunction withComplianceRejectionReasonreturnIPriceOracle-getPrice()returning(price, decimals, timestamp)for USD-denominated price feeds
