Creating RGB20 Tokens
RGB20 is the fungible token standard for RGB, analogous to ERC-20 on Ethereum but with superior privacy and scalability. This guide walks through creating your own RGB20 token from scratch using the actual RGB standard library APIs.
Overview​
Creating an RGB20 token involves:
- Defining token specifications (ticker, name, precision)
- Creating a contract builder with the NonInflatableAsset schema
- Configuring global state (spec, terms, issued supply)
- Allocating initial tokens to genesis seals
- Issuing the contract and persisting to stock
Prerequisites​
- RGB development environment set up
- Bitcoin wallet (testnet/regtest recommended for learning)
- Basic understanding of RGB concepts
- Familiarity with Bitcoin UTXOs and transaction outputs
RGB20 Schema Structure​
NonInflatableAsset (NIA) Schema​
RGB20 tokens use the NonInflatableAsset schema, which implements a fixed-supply fungible token. The schema defines:
Global State Types:
GS_NOMINAL(0x3000): Asset specification (ticker, name, precision)GS_TERMS(0x3001): Contract terms and media attachmentsGS_ISSUED_SUPPLY(0x3002): Total issued supply
Owned State Types:
OS_ASSET(0x1000): Fungible asset amounts (64-bit unsigned)
Transitions:
TS_TRANSFER: Transfer assets between owners
Schema Code​
The NIA schema uses AluVM scripts for validation:
// Genesis validation ensures issued supply matches pedersen commitments
pub(crate) fn nia_lib() -> Lib {
let code = rgbasm! {
// Transfer validation
put a8[0],ERRNO_NON_EQUAL_IN_OUT;
pcvs OS_ASSET; // Verify sum of inputs = sum of outputs
test;
ret;
// Genesis validation
put a8[0],ERRNO_ISSUED_MISMATCH;
ldg GS_ISSUED_SUPPLY,a8[1],s16[0];
extr s16[0],a64[0],a16[0];
pcas OS_ASSET; // Verify commitments match issued supply
test;
ret;
};
Lib::assemble(&code).expect("invalid NIA script")
}
Interface Implementation​
The RGB20 interface maps schema states to standardized names:
fn nia_rgb20() -> IfaceImpl {
IfaceImpl {
version: VerNo::V1,
schema_id: nia_schema().schema_id(),
iface_id: Rgb20::FIXED.iface_id(),
global_state: tiny_bset! {
NamedField::with(GS_NOMINAL, fname!("spec")),
NamedField::with(GS_TERMS, fname!("terms")),
NamedField::with(GS_ISSUED_SUPPLY, fname!("issuedSupply")),
},
assignments: tiny_bset! {
NamedField::with(OS_ASSET, fname!("assetOwner")),
},
transitions: tiny_bset! {
NamedField::with(TS_TRANSFER, fname!("transfer")),
},
errors: tiny_bset![
NamedVariant::with(ERRNO_ISSUED_MISMATCH, vname!("issuedMismatch")),
NamedVariant::with(ERRNO_NON_EQUAL_IN_OUT, vname!("nonEqualAmounts")),
],
}
}
Complete Token Creation Example​
Using NonInflatableAsset​
Here's a complete example creating an RGB20 token:
use amplify::hex::FromHex;
use bp::dbc::Method;
use bp::{Outpoint, Txid};
use ifaces::Rgb20;
use rgbstd::containers::{ConsignmentExt, FileContent};
use rgbstd::interface::{FilterIncludeAll, FungibleAllocation};
use rgbstd::invoice::Precision;
use rgbstd::persistence::Stock;
use schemata::NonInflatableAsset;
fn main() {
// 1. Define beneficiary UTXO (where tokens will be issued)
let beneficiary_txid =
Txid::from_hex("14295d5bb1a191cdb6286dc0944df938421e3dfcbf0811353ccac4100c2068c5")
.unwrap();
let beneficiary = Outpoint::new(beneficiary_txid, 1);
// 2. Create token contract using testnet helper
let contract = NonInflatableAsset::testnet(
"ssi:anonymous", // Issuer identity
"TEST", // Ticker (max 8 chars)
"Test Asset", // Name
None, // Optional details
Precision::CentiMicro, // 8 decimal places (like Bitcoin)
[(
Method::TapretFirst, // Commitment method
beneficiary, // Receiving UTXO
1_000_000_000_00u64 // Amount: 1 billion with 8 decimals
)]
).expect("invalid contract data");
let contract_id = contract.contract_id();
println!("Contract ID: {}", contract_id);
// 3. Save contract to file
contract.save_file("rgb20-token.rgb")
.expect("unable to save contract");
contract.save_armored("rgb20-token.rgba")
.expect("unable to save armored contract");
// 4. Import into stock for state management
let mut stock = Stock::in_memory();
stock.import_contract(contract, NoResolver).unwrap();
// 5. Read contract state through RGB20 interface
let contract = stock.contract_iface_class::<Rgb20>(contract_id).unwrap();
println!("\nToken Specification:");
println!("{}", serde_json::to_string(&contract.spec()).unwrap());
println!("\nAllocations:");
let allocations = contract.allocations(&FilterIncludeAll);
for FungibleAllocation { seal, state, witness, .. } in allocations {
let witness_id = witness
.as_ref()
.map(|w| w.to_string())
.unwrap_or("~".to_owned());
println!(" amount={}, owner={}, witness={}", state, seal, witness_id);
}
println!("\nTotal Supply: {}", contract.total_supply());
}
Using ContractBuilder (Advanced)​
For more control over contract creation:
use rgbstd::containers::Kit;
use rgbstd::persistence::Stock;
use rgbstd::stl::{AssetSpec, ContractTerms, RicardianContract};
use rgbstd::{Amount, Allocation, GenesisSeal, XChain};
use bp::seals::txout::CloseMethod;
use bp::Txid;
use std::str::FromStr;
fn create_token_advanced() -> Result<ValidContract, Box<dyn std::error::Error>> {
// 1. Define token specification
let spec = AssetSpec {
ticker: Ticker::from("MYTKN"),
name: Name::from("My Token"),
details: Some(Details::from("A demonstration token")),
precision: Precision::try_from(8)?, // 8 decimals
};
// 2. Define contract terms
let terms = ContractTerms {
text: RicardianContract::default(),
media: None, // Optional: attach contract documents
};
// 3. Define issued supply
let issued_supply = Amount::from(21_000_000_00000000u64); // 21M tokens
// 4. Create genesis seal (blinded UTXO)
let genesis_txid = Txid::from_str(
"8d54c98d4c29a1ec4fd90635f543f0f7a871a78eb6a6e706342f831d92e3ba19"
)?;
let seal = XChain::with(
Layer1::Bitcoin,
GenesisSeal::from(BlindSeal::with_blinding(
CloseMethod::TapretFirst, // Use taproot commitment
genesis_txid,
0, // Output index
654321, // Blinding factor
)),
);
// 5. Load schema kit
let kit = Kit::load_file("NonInflatableAsset.rgb")?.validate()?;
let mut stock = Stock::in_memory();
stock.import_kit(kit)?;
// 6. Build contract
let contract = stock.contract_builder(
"ssi:anonymous",
NonInflatableAsset::schema().schema_id(),
"RGB20",
)?
.add_global_state("spec", spec)?
.add_global_state("terms", terms)?
.add_global_state("issuedSupply", issued_supply)?
.add_fungible_state(
"assetOwner",
seal,
issued_supply.value(),
)?
.issue_contract()?;
Ok(contract)
}
Token Properties and Configuration​
Precision Settings​
Precision determines decimal places (0-18):
// Common precision values
Precision::Indivisible // 0 decimals (whole units only)
Precision::Deci // 1 decimal
Precision::Centi // 2 decimals (like USD cents)
Precision::Milli // 3 decimals
Precision::Micro // 6 decimals
Precision::CentiMicro // 8 decimals (Bitcoin-like)
Precision::Nano // 9 decimals
Supply Configuration​
Define initial token allocation:
// Single allocation to treasury
let allocations = vec![
(Method::TapretFirst, treasury_utxo, 1_000_000_00000000u64)
];
// Multiple allocations
let allocations = vec![
(Method::TapretFirst, treasury_utxo, 900_000_00000000u64), // 90%
(Method::TapretFirst, team_utxo, 100_000_00000000u64), // 10%
];
Commitment Methods​
RGB supports different Bitcoin commitment methods:
- TapretFirst: Taproot commitment (recommended)
- OpretFirst: OP_RETURN commitment (legacy)
use bp::dbc::Method;
// Use taproot for better privacy and efficiency
let method = Method::TapretFirst;
Working with Stock​
Importing and Querying Contracts​
use rgbstd::persistence::Stock;
use ifaces::Rgb20;
// Create in-memory stock
let mut stock = Stock::in_memory();
// Import contract
stock.import_contract(contract, NoResolver)?;
// Query via interface
let rgb20 = stock.contract_iface_class::<Rgb20>(contract_id)?;
// Access token data
println!("Ticker: {}", rgb20.spec().ticker);
println!("Name: {}", rgb20.spec().name);
println!("Precision: {}", rgb20.spec().precision);
println!("Total Supply: {}", rgb20.total_supply());
Persistent Stock​
For production use with filesystem persistence:
use rgb_persist_fs::StockpileDir;
// Create file-backed stock
let data_dir = "/path/to/rgb/data";
let stock = Stock::load(StockpileDir::new(data_dir)?)?;
Contract Verification​
Schema Validation​
Verify interface implementation matches schema:
let iface = NonInflatableAsset::FEATURES.iface();
let schema = NonInflatableAsset::schema();
let iface_impl = NonInflatableAsset::issue_impl();
if let Err(errors) = iface_impl.check(&iface, &schema) {
for error in errors {
eprintln!("Validation error: {}", error);
}
panic!("Invalid interface implementation");
}
Deterministic Contract IDs​
For reproducible contract generation:
use chrono::DateTime;
use rgbstd::containers::BuilderSeal;
use rgbstd::BlindingFactor;
let created_at = 1713261744; // Unix timestamp
let contract = builder
.add_asset_tag("assetOwner", AssetTag::new_deterministic(
"domain",
AssignmentType::with(0),
DateTime::from_timestamp(created_at, 0).unwrap(),
123456, // Salt
))?
.add_fungible_state_det(
"assetOwner",
BuilderSeal::from(seal),
issued_supply,
BlindingFactor::from_str("a3401bcceb26...")?, // Deterministic blinding
)?
.issue_contract_det(created_at)?;
// Contract ID is now deterministic and reproducible
assert_eq!(
contract.contract_id().to_string(),
"rgb:pOIzGFyQ-mA!yQq2-QH8vB5!-5fAplY!-x2lW!vz-JHDbYPg"
);
Error Handling​
Common Errors​
Issued Mismatch Error:
// Occurs when issued supply doesn't match allocation amounts
NamedVariant::with(ERRNO_ISSUED_MISMATCH, vname!("issuedMismatch"))
Non-Equal Amounts Error:
// Occurs in transfers when inputs != outputs
NamedVariant::with(ERRNO_NON_EQUAL_IN_OUT, vname!("nonEqualAmounts"))
Validation Results​
match stock.import_contract(contract, resolver) {
Ok(_) => println!("Contract imported successfully"),
Err(e) => eprintln!("Import failed: {}", e),
}
Testing Your Token​
Regtest Testing​
# Start Bitcoin regtest
bitcoind -regtest -daemon
# Generate test blocks
bitcoin-cli -regtest generatetoaddress 101 <address>
# Create RGB20 token
# (Run Rust program from above)
cargo run --example rgb20
# Verify contract file
ls -lh rgb20-token.rgb
Unit Tests​
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_token_creation() {
let contract = NonInflatableAsset::testnet(
"ssi:anonymous",
"TEST",
"Test Token",
None,
Precision::CentiMicro,
[(Method::TapretFirst, test_outpoint(), 1000_00000000u64)]
).expect("token creation failed");
assert_eq!(contract.contract_id().to_string().len() > 0, true);
}
#[test]
fn test_stock_import() {
let contract = create_test_token();
let mut stock = Stock::in_memory();
stock.import_contract(contract.clone(), NoResolver)
.expect("import failed");
let rgb20 = stock.contract_iface_class::<Rgb20>(contract.contract_id())
.expect("interface query failed");
assert_eq!(rgb20.spec().ticker.to_string(), "TEST");
}
}
Production Deployment​
Mainnet Checklist​
- Test thoroughly on regtest/testnet
- Audit contract logic and validation scripts
- Verify deterministic contract ID generation
- Document token specifications
- Prepare distribution strategy
- Set up persistent stock storage
- Configure backup procedures
- Test consignment creation and validation
Security Considerations​
Genesis Seal Security:
- Use fresh UTXOs for genesis
- Ensure UTXO has sufficient confirmations
- Protect blinding factors (treat like private keys)
Stock Management:
- Use encrypted filesystem for stock data
- Regular backups of contract state
- Multi-signature for treasury operations
Validation:
- Always validate consignments before acceptance
- Verify contract ID matches expectations
- Check issued supply constraints
Advanced Features​
Custom Asset Specifications​
use rgbstd::stl::{AssetSpec, Ticker, Name, Details};
let spec = AssetSpec {
ticker: Ticker::from("USDT"),
name: Name::from("Tether USD"),
details: Some(Details::from("Stablecoin pegged to USD")),
precision: Precision::Centi, // 2 decimals for USD
};
Contract Terms and Media​
use rgbstd::stl::{ContractTerms, RicardianContract, Attachment, MediaType};
let terms = ContractTerms {
text: RicardianContract::from("Legal contract text..."),
media: Some(Attachment {
ty: MediaType::with("application/pdf"),
digest: Bytes::from_byte_array(pdf_hash),
}),
};
Multiple Genesis Allocations​
// Distribute tokens to multiple recipients at genesis
let allocations = vec![
(Method::TapretFirst, founder1_utxo, 200_000_00000000u64),
(Method::TapretFirst, founder2_utxo, 200_000_00000000u64),
(Method::TapretFirst, treasury_utxo, 600_000_00000000u64),
];
let contract = NonInflatableAsset::testnet(
"ssi:company",
"COMP",
"Company Token",
Some("Equity token"),
Precision::CentiMicro,
allocations
)?;
Troubleshooting​
Common Issues​
"Invalid contract data" error:
- Check that issued supply matches sum of allocations
- Verify precision is within valid range (0-18)
- Ensure ticker is 1-8 characters
"Unable to save contract" error:
- Check file permissions
- Verify directory exists
- Ensure sufficient disk space
"Import failed" error:
- Validate contract before import
- Check resolver configuration
- Verify schema is loaded in stock
Debug Output​
// Print contract details
eprintln!("{}", contract);
// Print JSON-formatted spec
eprintln!("{}", serde_json::to_string_pretty(&contract.spec())?);
// Debug allocations
for allocation in contract.allocations(&FilterIncludeAll) {
eprintln!("{:?}", allocation);
}
Next Steps​
- Transferring Assets - How to transfer RGB20 tokens
- Wallet Integration - Building RGB-enabled wallets
- Rust SDK - Complete SDK documentation
Related Documentation​
Example Code: See /tmp/rgb-repos/rgb-schemata/examples/rgb20.rs for complete working examples.