A robust implementation of a fungible token system in Daml with minting, transfer (with recipient acceptance), and strong privacy guarantees.
This project implements a secure and privacy-preserving currency token system following Daml best practices. The system supports:
- Token Ownership: Clear distinction between token owners (who can mint/burn) and holders (who own balances)
- Minting: Controlled token creation by authorized owners
- Transfers: Secure, two-step token transfers requiring recipient acceptance
- Burning: Owner can destroy tokens (fully or partially) and reduce total supply
- Security: Built-in validation and authorization controls
- Privacy: Only the holder and owner can see balances
Represents a fungible token balance for a holder.
Key Features:
- Parties: Owner (minter/controller) and Holder (current token owner)
- Fields: symbol, amount, metadata
- Privacy: Only the holder (signatory) and owner (observer) can see the contract
- Validation: Non-negative amounts, sufficient balance checks
Choices:
LockTokenForTransfer: Initiate a transfer by locking tokens for a recipient, creating a TokenTransferLock contract
Controls minting and burning of a token type, and tracks total supply.
Key Features:
- Owner Control: Only the owner can mint or burn tokens
- Supply Tracking: Maintains total supply
- Observers: Owner can specify additional observers for the master contract (not for balances)
Choices:
Mint: Create new tokens for the ownerBurn: Destroy tokens and reduce total supply. Can burn a partial amount, returning a new TokenLedger contract for the remaining balance if any. Takes anOptional Decimalfor the amount to burn: ifNone, burns the entire balance; ifSome amount, burns only that amount.
Represents a pending transfer that must be accepted or rejected by the recipient.
Key Features:
- Two-Step Transfer: Sender locks tokens, recipient must accept or reject
- Cancellation: Sender can cancel a pending transfer and reclaim tokens
- Privacy: Only sender, recipient, and owner can see the lock
Choices:
Accept: Recipient accepts and receives the tokensReject: Recipient rejects, sender can reclaimCancel: Sender cancels and reclaims tokens
Initializes a new token type and creates the first TokenMaster and TokenLedger contracts.
-- 1. Create token setup
setupCid <- submit owner do
createCmd TokenSetup with
owner = owner
initialSupply = 0.0
metadata = myTokenMetadata
observers = [alice, bob]
-- 2. Initialize the system
(masterCid, ownerLedgerCid) <- submit owner do
exerciseCmd setupCid Initialize
-- 3. Mint tokens to the owner
(masterCid2, ownerLedgerCid2) <- submit owner do
exerciseCmd masterCid Mint with amount = 100.0
-- Owner locks 30 tokens for Alice
(lockCid, ownerLedgerCid3Opt) <- submit owner do
exerciseCmd ownerLedgerCid2 LockTokenForTransfer with
recipient = alice
transferAmount = 30.0
let ownerLedgerCid3 = fromSome ownerLedgerCid3Opt
-- Alice accepts the transfer
aliceLedgerCid <- submit alice do
exerciseCmd lockCid Accept
-- Alice can now lock tokens for Bob, or reject/cancel as needed
-- Alice rejects a transfer
reverseLockCid <- submit alice do
exerciseCmd lockCid Reject
-- Sender (owner) accepts the reverse lock to reclaim tokens
ownerLedgerCid4 <- submit owner do
exerciseCmd reverseLockCid Accept
-- Sender can also cancel a pending transfer before recipient acts
ownerLedgerCid5 <- submit owner do
exerciseCmd lockCid Cancel
-- Owner burns 10 tokens from their balance (partial burn)
(masterCid3, ownerLedgerCid3Opt) <- submit owner do
exerciseCmd masterCid2 Burn with
tokenCid = ownerLedgerCid3
amountToBurn = Some 10.0
-- ownerLedgerCid3Opt is Some cid if there is a remaining balance, None if fully burned
-- Owner burns all remaining tokens (full burn)
(masterCid4, ownerLedgerCid4Opt) <- submit owner do
exerciseCmd masterCid3 Burn with
tokenCid = fromSome ownerLedgerCid3Opt
amountToBurn = None
-- ownerLedgerCid4Opt will be None if all tokens are burned
- Only the holder can initiate transfers from their balance
- Only the owner can mint or burn tokens
- All amounts and operations are validated
- Only the holder and owner can see a TokenLedger contract
- Only sender, recipient, and owner can see a TokenTransferLock
- No general observers for balances
- Total supply tracked at the master level
- Clear ownership and holder distinctions
- Comprehensive input validation
- Clear error messages with
assertMsg - Prevent double-spending and negative balances
- Metadata supports extensible token information
- Symbol-based token identification
The project includes comprehensive tests in TokenLedgerTest.daml:
# Run all tests
daml test --files ./daml/TokenLedgerTest.damlTest scenarios cover:
- Complete token lifecycle (setup → mint → lock → accept/reject/cancel → burn)
- Error handling (insufficient balance, negative/zero amounts, unauthorized actions)
- Privacy and contract visibility
- Double-use and contract archival edge cases
- Each party can only have a single TokenLedger contract per token type at a time
- No need for merge or split operations
- Transfers require recipient acceptance (lock/accept pattern)
- Sender can cancel, recipient can reject
- TokenMaster contract maintains authoritative supply record
- All mint/burn operations update the master contract
# Build the project
daml build
# Start Daml sandbox
daml sandbox
# Upload DAR to ledger
daml ledger upload-dar .daml/dist/exchange-0.0.1.dar
# Run tests
daml test --files ./daml/TokenLedgerTest.damlThe project includes a comprehensive party management system with improved functional programming patterns and modular architecture.
The party management system is organized into two modules:
Scripts.PartyUtils: Core party and user utilities with functional programming patternsScripts.PartyManagement: Registry operations and CLI command interface
Key Improvements:
- Functional Programming: Curried implementations using
fmapcomposition for cleaner code - Code Separation: Utility functions separated from business logic for better reusability
- Enhanced Type Safety: Improved admin validation and registry lookup with conflict prevention
- Automatic User Creation: All party creation includes automatic user setup
The PartyRegistry template manages registered parties who can participate in the token system. All token operations require parties to be registered first.
Key Features:
- Admin Control: Only admin can register/unregister parties with enhanced validation
- Registration Validation: All token operations check party registration status
- Dynamic Management: Parties can be added/removed at runtime
- Visibility: All registered parties can see the registry
- Conflict Prevention: Enhanced admin validation prevents registry cross-contamination
Use the party-mgr.sh script for easy party management. For detailed CLI documentation, see PARTY_MANAGEMENT_CLI.md.
# Make the script executable (first time only)
chmod +x party-mgr.sh
# Initialize the party registry
./party-mgr.sh init
# Add new parties
./party-mgr.sh add Alice
./party-mgr.sh add Bob
./party-mgr.sh add TokenOwner
# List all registered parties
./party-mgr.sh list
# Check if a party is registered
./party-mgr.sh check Alice
# Remove a party from registry
./party-mgr.sh remove Bob
# Run full demonstration
./party-mgr.sh demo
# Show help
./party-mgr.sh helpThe script connects to a local Daml ledger by default:
- Host:
localhost - Port:
6865 - DAR File:
.daml/dist/exchange-0.0.1.dar
To modify these settings, edit the configuration section in party-mgr.sh:
# Configuration
HOST="localhost"
PORT="6865"
DAR_FILE=".daml/dist/exchange-0.0.1.dar"You can also manage parties directly using Daml Script commands (after uploading the DAR):
# Upload DAR to ledger (if not already done)
daml ledger upload-dar .daml/dist/exchange-0.0.1.dar
# Initialize registry
daml script --dar .daml/dist/exchange-0.0.1.dar --script-name Scripts.PartyManagement:cmdInit
# Add parties
daml script --dar .daml/dist/exchange-0.0.1.dar --script-name Scripts.PartyManagement:cmdAddParty --input-file <(echo '"Alice"')
daml script --dar .daml/dist/exchange-0.0.1.dar --script-name Scripts.PartyManagement:cmdAddParty --input-file <(echo '"Bob"')
# List all registered parties
daml script --dar .daml/dist/exchange-0.0.1.dar --script-name Scripts.PartyManagement:cmdListParties
# Check registration status
daml script --dar .daml/dist/exchange-0.0.1.dar --script-name Scripts.PartyManagement:cmdCheckParty --input-file <(echo '"Alice"')
# Remove a party
daml script --dar .daml/dist/exchange-0.0.1.dar --script-name Scripts.PartyManagement:cmdRemoveParty --input-file <(echo '"Bob"')Curried Function Examples:
-- Old imperative style:
getPartyByName name = do
partyDetails <- getPartyDetailsByName name
case partyDetails of
Some partyDetails -> return (Some partyDetails.party)
None -> return None
-- New curried style (cleaner and more functional):
getPartyByName name = fmap (fmap (.party)) (getPartyDetailsByName name)
-- Admin party lookup using composition:
getAdminParty = fmap (fmap (.party)) (getPartyDetailsByName "Admin")Enhanced Registry Validation:
findExistingRegistryInternal expectedAdmin = do
optRegistries <- queryContractKey @PartyRegistry expectedAdmin expectedAdmin
case optRegistries of
Some (registryCid, registry) ->
if registry.admin == expectedAdmin
then return (Some (registryCid, registry))
else logMismatchAndReturnNoneBefore using the token system, ensure parties are registered:
- Build and Upload:
daml build && daml ledger upload-dar .daml/dist/exchange-0.0.1.dar - Initialize Registry:
./party-mgr.sh init - Register Token Owner:
./party-mgr.sh add TokenOwner - Register Token Holders:
./party-mgr.sh add Alice,./party-mgr.sh add Bob - Verify Registration:
./party-mgr.sh list
Then proceed with token operations using registered parties only.
For frontend integration:
- Use the generated TypeScript/JavaScript bindings
- Implement proper party authentication
- Handle contract IDs for token references
For enterprise deployment:
- Configure appropriate ledger participants
- Set up proper key management
- Implement monitoring for supply and transfers
- Consider privacy implications with observer lists (for TokenMaster only)
This template is provided as-is for educational and development purposes.