DEUSSToken Contract Documentation
Overview
The DEUSSToken contract implements a fungible token standard based on ERC-6909, specifically designed for bond tokens in the DEUSS system. It provides functionality for token minting, burning, transfers, token freezing mechanisms, and historical snapshot queries. The contract supports multiple token IDs, where each token ID represents a distinct bond.
Bond lifecycle mint/burn authority lives in BondRegistry, while wallet/account transfer and approval eligibility is resolved through EntityRegistry.
Prerequisites
- Contract must be initialized with owner, bond registry, entity registry, and EscrowManager addresses
- The implementation contract disables initializers in its constructor; initialize only the deployed proxy instance
- Proper roles must be assigned to authorized users:
FORCE_TRANSFER_ROLE: For governed forced transfersTOKEN_FREEZER_ROLE: For token freezing/unfreezing
- BondRegistry must be set; protocol minting and burning are restricted to
BondRegistry - EscrowManager is marked as protected custody atomically during initialization
Protected Custody
- The owner can permanently mark an address as protected custody with
protectAddress(address). - Protected custody blocks privileged outbound operations that route through
_beforeRoleTransfer, namelyforcedTransfer,batchForcedTransfer,burn, andburnBatch. - Protected custody also blocks inbound
forcedTransferandbatchForcedTransferoperations; aFORCE_TRANSFER_ROLEholder cannot push tokens into protected custody. - Protected custody addresses cannot be targeted by
freezePartialTokensorbatchFreezePartialTokens. - Protected custody also rejects pushed normal ERC-6909 transfers from other callers. A protected address can still pull tokens into custody through approved
transferFromflows where the protected address is the caller. - The suite deployment protects
EscrowManagerin theDEUSSTokeninitializer.
Governed Forced Transfer Control
forcedTransferandbatchForcedTransferare intentionally stricter than other token RBAC paths.- The caller must have
FORCE_TRANSFER_ROLEand must also be an enabled account inEntityRegistry. - In production,
FORCE_TRANSFER_ROLEshould be held byTimelockController, not an operator key. Bootstrap grants the role to the timelock and registers/enables the timelock as anEntityRegistryaccount so forced transfer calls can pass. GUARDremains a fast emergency stop for forced transfers by disabling the timelock/execution account inEntityRegistry; restoring forced-transfer capability then requires a reviewed re-enable plus the normal timelock execution path.- Forced transfers can move tokens from a disabled
fromwallet to support recovery from blocked or compromised accounts, but thetowallet must be enabled and must not be protected custody. - Freezer operations remain RBAC-only. A
TOKEN_FREEZER_ROLEcaller is not required to be enabled inEntityRegistry.
Freeze Enforcement Boundaries
- Normal ERC-6909 transfers require sufficient unfrozen balance.
- Privileged
BondRegistryburns andFORCE_TRANSFER_ROLEtransfers require sufficient total balance and may automatically reduce frozen balance to complete the operation. - A freeze should be treated as a restriction on normal holder movement, not as an absolute block against maturity burns, final-settlement burns, or authorized recovery transfers.
Contract Architecture
The DEUSSToken inherits from:
IDEUSSToken(interface defining fungible token functionality)DEUSSTokenStorage(defines storage layout)BaseToken(base token implementation, which includes ERC-6909 base logic and OpenZeppelinPausableUpgradeable)
Core Functions
mint(address to, uint256 tokenId, uint256 amount)
Mints new tokens to a specified address.
Prerequisites:
- Caller must be
BondRegistry tomust be an enabled account inEntityRegistry
Parameters:
to: Address to receive the tokenstokenId: Token ID to mintamount: Amount of tokens to mint
Errors:
Token__CallerNotBondRegistry(address): When caller is not the BondRegistryToken__ZeroAmount(): When amount is zeroToken__TransferNotAllowed(address, address, uint256): Whentois not enabled inEntityRegistry
Sequence Diagram:
burn(address from, uint256 tokenId, uint256 amount)
Burns tokens from a specified address.
Prerequisites:
- Caller must be
BondRegistry - User must have sufficient balance
frommay be disabled in EntityRegistry to support protocol recovery burns- If permitted by the registry burn kind, frozen tokens may be unfrozen to complete the burn
ISSUER_RECLAIMis constrained inBondRegistryto unfrozen issuer balance and will not consume frozen inventory
Parameters:
from: Address to burn tokens fromtokenId: Token ID to burnamount: Amount of tokens to burn
Events:
TokensUnfrozen(address, uint256, uint256): Emitted when frozen tokens are unfrozen for burning
Errors:
Token__CallerNotBondRegistry(address): When caller is not the BondRegistryToken__AddressProtected(address): Whenfromis a protected custody addressToken__ZeroAmount(): When amount is zeroToken__InsufficientBalance(): When account has insufficient balance
Sequence Diagram:
freezePartialTokens(address account, uint256 tokenId, uint256 amount)
Freezes a specified amount of tokens for an account.
Prerequisites:
- Freezer must have
TOKEN_FREEZER_ROLE - Contract must not be paused
- Account must not be a protected custody address
- Account must have sufficient balance
Parameters:
account: Address to freeze tokens fortokenId: Token ID to freezeamount: Amount of tokens to freeze
Events:
TokensFrozen(address, uint256, uint256): Emitted when tokens are frozen
State impact:
- Updates frozen-balance checkpoints for
(account, tokenId)atblock.number
Errors:
Ownable.Unauthorized: When caller lacksTOKEN_FREEZER_ROLEPausableUpgradeable.EnforcedPause(): When contract is pausedToken__ZeroAmount(): When amount is zeroToken__AddressProtected(address): Whenaccountis a protected custody addressToken__InsufficientBalance(): When total frozen would exceed balance
Sequence Diagram:
unfreezePartialTokens(address account, uint256 tokenId, uint256 amount)
Unfreezes a specified amount of tokens for an account.
Prerequisites:
- Freezer must have
TOKEN_FREEZER_ROLE - Contract must not be paused
- Account must have sufficient frozen balance
Parameters:
account: Address to unfreeze tokens fortokenId: Token ID to unfreezeamount: Amount of tokens to unfreeze
Events:
TokensUnfrozen(address, uint256, uint256): Emitted when tokens are unfrozen
State impact:
- Updates frozen-balance checkpoints for
(account, tokenId)atblock.number
Errors:
Ownable.Unauthorized: When caller lacksTOKEN_FREEZER_ROLEPausableUpgradeable.EnforcedPause(): When contract is pausedToken__ZeroAmount(): When amount is zeroToken__InsufficientBalance(): When account has insufficient frozen balance
Sequence Diagram:
forcedTransfer(address from, address to, uint256 tokenId, uint256 amount)
Forces transfer from one address to the receiver address.
Prerequisites:
- Sender must have
FORCE_TRANSFER_ROLE - Sender (caller) must be an enabled wallet in EntityRegistry
frommust have sufficient balancefrommay be disabled in EntityRegistry to support protocol recovery from blocked accountstomust be an enabled wallet in EntityRegistrytomust not be protected custody- If needed, frozen tokens will be unfrozen to complete the transfer
Parameters:
from: Address to transfer tokens fromto: Receiver addresstokenId: Token ID to transferamount: Tokens amount to transfer
Errors:
Ownable.Unauthorized: When caller lacks the required roleToken__CallerNotEnabled(address): When the caller is not an enabled wallet in EntityRegistryToken__ZeroAmount(): When amount is zeroToken__TransferNotAllowed(address, address, uint256): Whentois not enabled in EntityRegistryToken__ProtectedReceiverTransferNotAllowed(address,address,address,uint256): Whentois protected custodyToken__InsufficientBalance(): When user has insufficient balance
Sequence Diagram:
protectAddress(address account)
Permanently marks a custody address as protected from privileged outbound token movement.
Prerequisites:
- Caller must be owner
accountmust be non-zeroaccountmust not already be protected
State impact:
- Sets
isAddressProtected(account) == true - There is no unprotect function
Events:
ProtectedAddressSet(account, true)
Errors:
Unauthorized(): When caller is not ownerZeroAddress(): Whenaccountis zeroToken__AddressAlreadyProtected(address): When the address is already protected
Important Notes:
- Protected addresses cannot be the
fromaddress forforcedTransfer,batchForcedTransfer,burn, orburnBatch. - Protected addresses cannot be the
toaddress forforcedTransferorbatchForcedTransfer. - Other accounts cannot push tokens into protected custody. Approved custody flows remain available when the protected address pulls tokens as the transfer caller, as
EscrowManagerdoes during escrow creation.
isAddressProtected(address account)
Returns whether account has been marked as protected custody.
approve(address spender, uint256 tokenId, uint256 amount)
Overrides BaseToken.approve to keep allowance revocation available while preserving the registry approval policy for new spend permissions.
Behavior:
amount != 0delegates toBaseToken.approve, which follows ERC-6909 set semantics and overwrites any existing allowance afterEntityRegistry.canApprove(owner, spender, tokenId, amount)passes.amount == 0clears the allowance directly and remains allowed even if the owner or spender is no longer enabled.
setOperator(address spender, bool approved)
Grants or revokes global ERC-6909 operator approval for spender.
Prerequisites when approved == true:
- Contract must not be paused
- Owner and spender must be allowed by
EntityRegistry.canApprove(owner, spender, 0, 0)
Revocation:
approved == falseremains allowed even if the owner or operator is no longer enabled, so stale operator approvals can always be removed.
Errors:
Token__OperatorNotEnabled(address): When a new operator approval is not allowed by the EntityRegistry approval policy
transferFrom(address from, address to, uint256 tokenId, uint256 amount)
Performs a transfer from one address to the receiver address.
Prerequisites:
- Token ID must not be paused
- Transfer must be allowed by EntityRegistry
frommust have sufficient unfrozen balancefrommust be an enabled wallet in EntityRegistrytomust be an enabled wallet in EntityRegistrycallermust be an enabled wallet in EntityRegistry
Parameters:
from: Address to transfer tokens fromto: Receiver addresstokenId: Token ID to transferamount: Tokens amount to transfer
Errors:
Token__ZeroAmount(): When amount is zeroToken__TokenIdIsPaused(uint256): When token ID is pausedToken__TransferNotAllowed(address, address, uint256): When EntityRegistry does not allow the transferToken__InsufficientBalance(): When user has insufficient unfrozen balanceToken__ProtectedReceiverTransferNotAllowed(address,address,address,uint256): When another caller attempts to push tokens into a protected receiver
Sequence Diagram:
transfer(address receiver, uint256 tokenId, uint256 amount)
Transfer a specified amount of token type tokenId from the caller's account to the receiver address.
Parameters:
receiver: Receiver addresstokenId: Token ID to transferamount: Tokens amount to transfer
[!NOTE] This function calls DEUSSToken function transferFrom(address from, address to, uint256 tokenId, uint256 amount) and automatically sets
fromtomsg.sender.
Batch Operations
batchForcedTransfer(address[] calldata froms, address[] calldata tos, uint256 tokenId, uint256[] calldata amounts)
Forces multiple transfers between addresses for a given token ID.
Prerequisites:
- Sender must have
FORCE_TRANSFER_ROLE - Sender (caller) must be an enabled wallet in EntityRegistry
- Arrays must have matching lengths
fromsmust have sufficient balancestosmust be enabled wallets in EntityRegistrytosmust not include protected custody addressesfromsmay include disabled wallets to support protocol recovery from blocked accounts- If needed, frozen tokens will be unfrozen to complete each transfer
Parameters:
froms: Array of addresses from which to transfer tokenstos: Array of receiver addressestokenId: Token ID to transferamounts: Array of token amounts to transfer
Errors:
Ownable.Unauthorized: When caller lacks the required roleToken__CallerNotEnabled(address): When the caller is not an enabled wallet in EntityRegistryToken__InvalidArrayLength(): When arrays lengths don't matchToken__ZeroAmount(): When amount is zeroToken__TransferNotAllowed(address, address, uint256): Whentois not an enabled wallet in EntityRegistryToken__ProtectedReceiverTransferNotAllowed(address,address,address,uint256): When anytois protected custodyToken__InsufficientBalance(): When user has insufficient balance
Sequence Diagram:
batchTransferFrom(address from, address to, uint256[] calldata tokenIds, uint256[] calldata amounts)
Transfers multiple token IDs from one address to a single recipient.
Prerequisites:
- Arrays must have matching lengths
- Each token ID must not be paused
- Each transfer must be allowed by EntityRegistry
frommust have sufficient unfrozen balance for each token IDfrommust be an enabled wallet in EntityRegistrytomust be an enabled wallet in EntityRegistrycallermust be an enabled wallet in EntityRegistry
Parameters:
from: Address to transfer tokens fromto: Receiver addresstokenIds: Array of token IDs to transferamounts: Array of token amounts to transfer for each token ID
Errors:
Token__InvalidArrayLength(): When arrays lengths don't matchToken__ZeroAmount(): When amount is zeroToken__TokenIdIsPaused(uint256): When a token ID is pausedToken__TransferNotAllowed(address, address, uint256): When EntityRegistry does not allow the transferToken__InsufficientBalance(): When user has insufficient unfrozen balance
Sequence Diagram:
batchTransferFrom(address from, address[] calldata tos, uint256 tokenId, uint256[] calldata amounts)
Transfers a single token ID from one address to multiple recipients.
Prerequisites:
- Arrays must have matching lengths
- Token ID must not be paused
- Each transfer must be allowed by EntityRegistry
frommust have sufficient unfrozen balancefrommust be an enabled wallet in EntityRegistrytosmust be an enabled wallet in EntityRegistry
Parameters:
from: Address to transfer tokens fromtos: Array of receiver addressestokenId: Token ID to transferamounts: Array of token amounts to transfer to each receiver
Errors:
Token__InvalidArrayLength(): When arrays lengths don't matchToken__ZeroAmount(): When amount is zeroToken__TokenIdIsPaused(uint256): When token ID is pausedToken__TransferNotAllowed(address, address, uint256): When EntityRegistry does not allow the transferToken__InsufficientBalance(): When user has insufficient unfrozen balance
Sequence Diagram:
batchFreezePartialTokens(address[] calldata wallets, uint256 tokenId, uint256[] calldata amounts)
Freezes tokens for multiple wallets in a single transaction.
Prerequisites:
- Freezer must have
TOKEN_FREEZER_ROLE - Contract must not be paused
- Arrays must have matching lengths
- No wallet may be a protected custody address
- Each wallet must have sufficient balance
Parameters:
wallets: Array of wallet addressestokenId: Token ID to freezeamounts: Array of token amounts to freeze
Events:
TokensFrozen(address, uint256, uint256): Emitted for each wallet when tokens are frozen
State impact:
- Updates frozen-balance checkpoints for each
(wallet, tokenId)atblock.number
Errors:
Ownable.Unauthorized: When caller lacks the required rolePausableUpgradeable.EnforcedPause(): When contract is pausedToken__InvalidArrayLength(): When arrays lengths don't matchToken__ZeroAmount(): When amount is zeroToken__AddressProtected(address): When a wallet is a protected custody addressToken__InsufficientBalance(): When wallet has insufficient balance
Sequence Diagram:
batchUnfreezePartialTokens(address[] calldata wallets, uint256 tokenId, uint256[] calldata amounts)
Unfreezes tokens for multiple wallets in a single transaction.
Prerequisites:
- Freezer must have
TOKEN_FREEZER_ROLE - Contract must not be paused
- Arrays must have matching lengths
- Each wallet must have sufficient frozen tokens
Parameters:
wallets: Array of wallet addressestokenId: Token ID to unfreezeamounts: Array of token amounts to unfreeze
Events:
TokensUnfrozen(address, uint256, uint256): Emitted for each wallet when tokens are unfrozen
State impact:
- Updates frozen-balance checkpoints for each
(wallet, tokenId)atblock.number
Errors:
Ownable.Unauthorized: When caller lacks the required rolePausableUpgradeable.EnforcedPause(): When contract is pausedToken__InvalidArrayLength(): When arrays lengths don't matchToken__ZeroAmount(): When amount is zeroToken__InsufficientBalance(): When wallet has insufficient frozen tokens
Sequence Diagram:
burnBatch(address[] calldata froms, uint256 tokenId, uint256[] calldata amounts)
Burns tokens from multiple addresses in a single transaction.
Prerequisites:
- Caller must be
BondRegistry - Arrays must have matching lengths
- Users must have sufficient balances
fromsmay include disabled wallets to support protocol recovery burns- If permitted by the registry burn kind, frozen tokens may be unfrozen to complete each burn
ISSUER_RECLAIMis constrained inBondRegistryto unfrozen issuer balance and will not consume frozen inventory
Parameters:
froms: Array of addresses from which to burn tokenstokenId: Token ID to burnamounts: Array of token amounts to burn
Events:
TokensUnfrozen(address, uint256, uint256): Emitted when frozen tokens are unfrozen for burning
Errors:
Token__CallerNotBondRegistry(address): When caller is not the BondRegistryToken__InvalidArrayLength(): When arrays lengths don't matchToken__ZeroAmount(): When amount is zeroToken__InsufficientBalance(): When account has insufficient balance
Sequence Diagram:
Administrative Functions
initialize(address owner_, address bondRegistry_, address entityRegistry_, address escrowManager_)
Initializes the contract with owner, bond registry, entity registry, and protected EscrowManager custody addresses.
Prerequisites:
- Contract must not be already initialized
escrowManager_must be non-zero and not already protected
Parameters:
owner_: Address of the contract ownerbondRegistry_: Address of the bond registry contractentityRegistry_: Address of the entity registry contractescrowManager_: EscrowManager custody address to protect during initialization
Events:
ProtectedAddressSet(escrowManager_, true)
Errors:
ZeroAddress(): WhenescrowManager_,bondRegistry_, orentityRegistry_is zeroToken__AddressAlreadyProtected(address): WhenescrowManager_is already protected
Sequence Diagram:
View Functions
totalSupply(uint256 tokenId)
Returns the total supply of a given token ID.
Parameters:
tokenId: Token ID to query
Returns:
uint256: Total token supply for the given token ID
balanceOfAt(address account, uint256 tokenId, uint256 blockNumber)
Returns an account historical balance for a token ID at a specific block.
Parameters:
account: Address to querytokenId: Token ID to queryblockNumber: Block number to query
Returns:
uint256: Account balance at or beforeblockNumber
Errors:
Token__BlockInFuture(uint256 currentBlock, uint256 queriedBlock): WhenblockNumberis greater than the current block
totalSupplyAt(uint256 tokenId, uint256 blockNumber)
Returns historical total supply for a token ID at a specific block.
Parameters:
tokenId: Token ID to queryblockNumber: Block number to query
Returns:
uint256: Total supply at or beforeblockNumber
Errors:
Token__BlockInFuture(uint256 currentBlock, uint256 queriedBlock): WhenblockNumberis greater than the current block
frozenBalanceOfAt(address account, uint256 tokenId, uint256 blockNumber)
Returns historical frozen balance for an account/token ID at a specific block.
Parameters:
account: Address to querytokenId: Token ID to queryblockNumber: Block number to query
Returns:
uint256: Frozen balance at or beforeblockNumber
Errors:
Token__BlockInFuture(uint256 currentBlock, uint256 queriedBlock): WhenblockNumberis greater than the current block
availableBalanceOfAt(address account, uint256 tokenId, uint256 blockNumber)
Returns historical available (transferable) balance for an account/token ID at a specific block.
Computation:
available = balanceOfAt(account, tokenId, blockNumber) - frozenBalanceOfAt(account, tokenId, blockNumber)- If invariant drift ever occurs, the function returns
0instead of underflowing.
Parameters:
account: Address to querytokenId: Token ID to queryblockNumber: Block number to query
Returns:
uint256: Available balance at or beforeblockNumber
Errors:
Token__BlockInFuture(uint256 currentBlock, uint256 queriedBlock): WhenblockNumberis greater than the current block
supportsInterface(bytes4 interfaceId)
Indicates whether the contract supports a given interface, as per ERC-165.
Parameters:
interfaceId: The interface identifier, as specified in ERC-165
Returns:
bool: True if the contract implements the requested interface, false otherwise