Factory contract एक ऐसा contract है जो किसी contract के एक या अधिक instances को deploy करता है।
“Understanding Starknet’s Contract Deployment Model” अध्याय में, हमने सीखा कि Starknet के declare-deploy model के साथ, आपको पहले एक contract class को एक बार declare करना होता है और फिर आप मैन्युअली उससे कई instances deploy कर सकते हैं। हालाँकि, जब हम contract instances को मैन्युअली deploy करते हैं, तो हमें हर बार सही class hash खोजना पड़ता है और सही constructor arguments (यदि कोई हों) पास करने पड़ते हैं।
Factory contracts एक कंसिस्टेंट डिप्लॉयमेंट इंटरफ़ेस प्रदान करके इसे हल करते हैं। इसके बजाय कि class hashes को मैन्युअली हैंडल किया जाए, आप बस factory को कॉल करते हैं, जो इच्छित contract का एक नया contract instance deploy कर देता है:

Ethereum (और अन्य EVM chains) पर, factory contract नए child contracts को deploy करने के लिए आंतरिक रूप से CREATE या CREATE2 opcode का उपयोग करता है। Starknet पर, factories deploy_syscall के माध्यम से समान व्यवहार प्राप्त करती हैं।
इस लेख में, आप सीखेंगे कि deploy_syscall का उपयोग करके factory contract को कैसे लागू (implement) किया जाए।
Factories deploy_syscall का उपयोग कैसे करती हैं
Contract instances को deploy करने के लिए Factory contracts सीधे deploy_syscall को कॉल करते हैं। यहाँ deploy_syscall का function signature दिया गया है:
pub extern fn deploy_syscall(
class_hash: ClassHash,
contract_address_salt: felt252,
calldata: Span<felt252>,
deploy_from_zero: bool
) -> Result<(ContractAddress, Span<felt252>), Array<felt252>>
implicits(GasBuiltin, System) nopanic;
यह चार पैरामीटर्स लेता है:
class_hash: जिस contract को आप deploy करना चाहते हैं उसका class hashcontract_address_salt: address कैलकुलेशन के लिए एक salt वैल्यूcalldata: नए contract के लिए constructor argumentsdeploy_from_zero: यह निर्धारित करता है कि contract address कैलकुलेशन से deployer का address हटाया (exclude) गया है या नहीं। जब यहfalseहोता है, तो deployer का address शामिल (include) होता है। जब यहtrueहोता है, तो इसे हटा दिया जाता है और address की गणना ऐसे की जाती है जैसे कि इसे address0से deploy किया गया हो। ध्यान दें कि यह UDC इंटरफ़ेस मेंnot_from_zeroपैरामीटर के बिल्कुल विपरीत (inverse) है।
deploy_syscall दो संभावित परिणामों के साथ एक Result टाइप देता है:
- सफलता (success) पर, आपको नए deploy किए गए contract का
ContractAddressऔर सीरियलाइज़्ड constructor return डेटा का एकSpan<felt252>मिलता है। चूँकि Cairo constructors आमतौर पर वैल्यूज़ return नहीं करते हैं, इसलिए यह span आमतौर पर खाली होता है, लेकिन यदि constructor स्पष्ट रूप से (explicitly) वैल्यूज़ return करता है तो यह उपलब्ध रहता है। - विफलता (failure) पर, आपको एक
Array<felt252>मिलता है जिसमें error की जानकारी होती है जो यह बताती है कि deployment के दौरान क्या गलत हुआ।
नोट:
implicits(GasBuiltin, System)गैस ट्रैकिंग और सिस्टम ऑपरेशन्स के लिए इम्प्लिसिट पैरामीटर्स हैं, जिन्हें Cairo द्वारा स्वचालित रूप से हैंडल किया जाता है।nopanicका मतलब है कि फ़ंक्शन errors पर पैनिक (panic) करने के बजाय एकResultटाइप return करता है।
Factory contract में, आप createContract() जैसे किसी factory फ़ंक्शन को कॉल करते हैं, जो आंतरिक रूप से आवश्यक पैरामीटर्स: class_hash, salt, calldata, और deploy_from_zero के साथ deploy_syscall को कॉल करता है, जैसा कि नीचे दी गई छवि में दिखाया गया है:

deploy_syscall के निष्पादन (execution) के दौरान, नेटवर्क Pedersen-आधारित contract address फ़ॉर्मूले का उपयोग करके contract address की गणना करता है, जिसे Understanding Starknet’s Contract Deployment Model में कवर किया गया है। deploy_from_zero पैरामीटर उस फ़ॉर्मूले में deployer_address वैल्यू निर्धारित करता है: जब यह false होता है, तो इसे factory contract के address पर सेट किया जाता है; जब यह true होता है, तो इसे 0 पर सेट किया जाता है।
नेटवर्क फिर उस कैलकुलेट किए गए address पर contract instance को deploy करता है और प्रदान किए गए calldata के साथ constructor को निष्पादित (execute) करता है।
Deployment पूरा होने के बाद, deploy_syscall नए ContractAddress को किसी भी constructor return डेटा के साथ factory को वापस कर देता है। यदि factory कार्यान्वयन (implementation) में कोई event शामिल है, तो यह मूल कॉलर को ContractAddress वापस करने से पहले इस बिंदु पर संबंधित deployment डेटा को एमिट (emit) करता है।
Universal Deployer Contract vs Custom Factory Contract
Universal Deployer Contract (UDC) जिसका उपयोग हम नियमित contract deployment के लिए करते हैं, वह स्वयं एक factory contract है। यह लो-लेवल deploy_syscall को रैप (wrap) करता है और किसी भी declared contract को deploy करने के लिए एक सरल इंटरफ़ेस प्रदान करता है। हालाँकि, UDC deploy किए गए contracts को ट्रैक नहीं करता है और यह पूरी तरह से सामान्य deployment उद्देश्यों के लिए है।
जब आपको बेसिक deployment से अधिक की आवश्यकता होती है, तो आप एक कस्टम factory contract बनाते हैं। उदाहरण के लिए, आप यह ट्रैक करना चाह सकते हैं कि कौन से contracts deploy किए गए थे और किसके द्वारा किए गए थे (registry), या कौन contract classes को deploy या अपडेट कर सकता है (access control) इसे प्रतिबंधित (restrict) करना चाह सकते हैं। अगले भाग में, हम यह दिखाने के लिए कि ये विचार कैसे काम करते हैं, एक token factory लागू करेंगे।
किसी भी नियमित contract की तरह, factory contract को UDC के माध्यम से deploy किया जाता है। लेकिन जब factory instances को deploy करती है, तो यह UDC से दोबारा जाने के बजाय सीधे
deploy_syscallको कॉल करती है।
एक Token Factory Contract बनाना
हम एक ERC-20 factory contract बनाएंगे जो class hashes और deployment पैरामीटर्स को एब्स्ट्रैक्ट (abstract) कर देता है, जिससे आप एक सरल इंटरफ़ेस के माध्यम से token instances को deploy कर सकते हैं।
Token Contract को अपडेट करना
हमें “ERC-20 Token on Starknet” अध्याय से अपने ERC-20 contract को मॉडिफाई करने की आवश्यकता है। मूल वर्ज़न में token name, symbol, और decimals के लिए हार्डकोड की गई वैल्यूज़ थीं। चूँकि factory को अलग-अलग नामों और प्रतीकों (symbols) वाले tokens deploy करने की आवश्यकता होती है, इसलिए इन वैल्यूज़ का कॉन्फ़िगर करने योग्य (configurable) होना आवश्यक है:
#[constructor]
fn constructor(
ref self: ContractState, token_name: ByteArray, symbol: ByteArray, owner: ContractAddress,
) {
self.token_name.write(token_name); //newly added
self.symbol.write(symbol); // newly added
self.decimal.write(18);
self.owner.write(owner);
}
token_name और symbol अब हार्डकोड की गई वैल्यूज़ के बजाय पैरामीटर्स हैं, owner को यह निर्धारित करने के लिए एक पैरामीटर के रूप में पास किया जाता है कि कौन tokens मिंट (mint) कर सकता है, और decimals 18 (ERC-20 मानक) पर निश्चित (fixed) रहते हैं।
अपने Scarb प्रोजेक्ट में src/erc20.cairo बनाएँ और पूरे अपडेट किए गए contract कोड को इसमें पेस्ट करें:
use starknet::ContractAddress;
#[starknet::interface]
pub trait IERC20<TContractState> {
fn total_supply(self: @TContractState) -> u256;
fn balance_of(self: @TContractState, account: ContractAddress) -> u256;
fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256;
fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool;
fn transfer_from(
ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256,
) -> bool;
fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool;
fn name(self: @TContractState) -> ByteArray;
fn symbol(self: @TContractState) -> ByteArray;
fn decimals(self: @TContractState) -> u8;
fn mint(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool;
}
#[starknet::contract]
pub mod ERC20Token {
use starknet::storage::{
Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess,
};
use starknet::{ContractAddress, get_caller_address};
#[storage]
pub struct Storage {
balances: Map<ContractAddress, u256>,
allowances: Map<
(ContractAddress, ContractAddress), u256,
>, // (owner, spender) -> amount, amount>
token_name: ByteArray,
symbol: ByteArray,
decimal: u8,
total_supply: u256,
owner: ContractAddress,
}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
Transfer: Transfer,
Approval: Approval,
}
#[derive(Drop, starknet::Event)]
pub struct Transfer {
#[key]
from: ContractAddress,
#[key]
to: ContractAddress,
amount: u256,
}
#[derive(Drop, starknet::Event)]
pub struct Approval {
#[key]
owner: ContractAddress,
#[key]
spender: ContractAddress,
value: u256,
}
#[constructor]
fn constructor(
ref self: ContractState, token_name: ByteArray, symbol: ByteArray, owner: ContractAddress,
) {
self.token_name.write(token_name);
self.symbol.write(symbol);
self.decimal.write(18);
self.owner.write(owner);
}
#[abi(embed_v0)]
impl ERC20Impl of super::IERC20<ContractState> {
fn total_supply(self: @ContractState) -> u256 {
self.total_supply.read()
}
fn balance_of(self: @ContractState, account: ContractAddress) -> u256 {
let balance = self.balances.entry(account).read();
balance
}
fn name(self: @ContractState) -> ByteArray {
self.token_name.read()
}
fn symbol(self: @ContractState) -> ByteArray {
self.symbol.read()
}
fn decimals(self: @ContractState) -> u8 {
self.decimal.read()
}
fn allowance(
self: @ContractState, owner: ContractAddress, spender: ContractAddress,
) -> u256 {
let allowance = self.allowances.entry((owner, spender)).read();
allowance
}
fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool {
let sender = get_caller_address();
let sender_prev_balance = self.balances.entry(sender).read();
let recipient_prev_balance = self.balances.entry(recipient).read();
assert(sender_prev_balance >= amount, 'Insufficient amount');
self.balances.entry(sender).write(sender_prev_balance - amount);
self.balances.entry(recipient).write(recipient_prev_balance + amount);
assert(
self.balances.entry(recipient).read() > recipient_prev_balance,
'Transaction failed',
);
self.emit(Transfer { from: sender, to: recipient, amount });
true
}
fn transfer_from(
ref self: ContractState,
sender: ContractAddress,
recipient: ContractAddress,
amount: u256,
) -> bool {
let spender = get_caller_address();
let spender_allowance = self.allowances.entry((sender, spender)).read();
let sender_balance = self.balances.entry(sender).read();
let recipient_balance = self.balances.entry(recipient).read();
assert(amount <= spender_allowance, 'amount exceeds allowance');
assert(amount <= sender_balance, 'amount exceeds balance');
self.allowances.entry((sender, spender)).write(spender_allowance - amount);
self.balances.entry(sender).write(sender_balance - amount);
self.balances.entry(recipient).write(recipient_balance + amount);
self.emit(Transfer { from: sender, to: recipient, amount });
true
}
fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool {
let caller = get_caller_address();
self.allowances.entry((caller, spender)).write(amount);
self.emit(Approval { owner: caller, spender, value: amount });
true
}
fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool {
let caller = get_caller_address();
assert(caller == self.owner.read(), 'Caller is not owner');
let previous_total_supply = self.total_supply.read();
let previous_balance = self.balances.entry(recipient).read();
self.total_supply.write(previous_total_supply + amount);
self.balances.entry(recipient).write(previous_balance + amount);
let zero_address: ContractAddress = 0.try_into().unwrap();
self.emit(Transfer { from: zero_address, to: recipient, amount });
true
}
}
}
Factory Interface (IERC20Factory) को परिभाषित करना
इससे पहले कि हम कोई implementation कोड लिखें, आइए परिभाषित करें कि हमारी factory क्या करेगी। हमारे factory contract में चार प्रमुख फ़ंक्शन्स होंगे:
- एक token deploy करना: एक नाम, प्रतीक (symbol) और owner address के साथ एक नया token instance बनाता है।
- किसी विशिष्ट address पर एक token deploy करना: उपयोगकर्ता द्वारा निर्दिष्ट (user-specified) salt के साथ एक नया token बनाता है, जिससे कॉलर परिणामी contract address को निर्धारित कर सकता है।
- Deploy किए गए tokens को क्वेरी (Query) करना: इस factory के माध्यम से deploy किए गए सभी tokens को प्राप्त करता है, चाहे वे विश्व स्तर पर (globally) हों या उपयोगकर्ता द्वारा फ़िल्टर किए गए हों।
- Token contract class को अपडेट करना: भविष्य के सभी token deployments के लिए उपयोग की जाने वाली ERC-20 contract class को बदलता है।
use starknet::{ContractAddress, ClassHash};
#[starknet::interface]
pub trait IERC20Factory<TContractState> {
// Deploy a new ERC20 token contract with a user-specified salt
fn create_token_at(
ref self: TContractState,
salt: felt252,
name: ByteArray,
symbol: ByteArray,
owner: ContractAddress,
) -> ContractAddress;
// Deploy a new ERC20 token contract using a default salt
fn create_token(
ref self: TContractState,
name: ByteArray,
symbol: ByteArray,
owner: ContractAddress,
) -> ContractAddress;
// Update the stored class hash used for new ERC20 deployments
fn update_erc20_class_hash(ref self: TContractState, new_class_hash: ClassHash);
// Returns an array of all token contract addresses created by this factory
fn get_all_created_tokens(self: @TContractState) -> Array<ContractAddress>;
// Returns all token contract addresses created by a specific user
fn get_user_tokens(self: @TContractState, user: ContractAddress) -> Array<ContractAddress>;
}
Storage Variables
ऊपर दिए गए इंटरफ़ेस से, हम यह निर्धारित कर सकते हैं कि factory contract को क्या स्टोर करने की आवश्यकता है। हमें उस token class hash को स्टोर करने की आवश्यकता है जहाँ से हम deploy करेंगे, हमारे द्वारा बनाए गए प्रत्येक token को ट्रैक करना होगा, उन्हें निर्माता (creator) के अनुसार व्यवस्थित करना होगा, और भविष्य के deployments के लिए उपयोग किए जाने वाले token class hash को अपडेट करने की अनुमति किसे है, इसे प्रतिबंधित करना होगा। इससे हम इन state variables को परिभाषित करते हैं:
#[storage]
struct Storage {
token_class_hash: ClassHash, // class hash of the token contract to deploy
created_tokens: Vec<ContractAddress>, // global list of all deployed token instances
user_tokens: Map<ContractAddress, Vec<ContractAddress>>, // tokens deployed per user
factory_owner: ContractAddress, // address with admin rights over the factory
}
token_class_hash उस ERC-20 contract class को स्टोर करता है जिसका उपयोग factory नए tokens को deploy करने के लिए करती है। इस factory के माध्यम से बनाया गया प्रत्येक token इस class hash का एक instance होगा। Factory owner बाद में बेहतर token वर्ज़न्स को deploy करने के लिए इस वैल्यू को अपडेट कर सकता है।
हम बनाए गए tokens के लिए दो अलग-अलग सूचियाँ (lists) बनाए रखते हैं:
created_tokensवेक्टर (vector) इस factory के माध्यम से deploy किए गए प्रत्येक token का एक पूरा रिकॉर्ड प्रदान करता है।- इस बीच,
user_tokensमैपिंग (mapping) प्रत्येक उपयोगकर्ता के लिए अलग-अलग सूचियाँ बनाती है, जिसमें केवल वे tokens स्टोर होते हैं जो उस विशिष्ट address ने बनाए हैं।
factory_owner उस address को स्टोर करता है जिसके पास token class hash को अपडेट करने की अनुमति (permission) होती है।
Factory Constructor
Factory constructor deploy करने के लिए ERC-20 class hash और उस owner address को स्टोर करता है जिसके पास factory के admin अधिकार हैं:
#[constructor]
fn constructor(ref self: ContractState, token_class_hash: ClassHash, owner: ContractAddress) {
self.token_class_hash.write(token_class_hash);
self.factory_owner.write(owner);
}
Event Definitions
हमें बाहरी (external) ऐप्लिकेशन्स के लिए deployments और class hash में होने वाले बदलावों को ट्रैक करने के लिए एक तरीके की आवश्यकता है। हम दो events परिभाषित करेंगे जो हमारी प्रमुख गतिविधियों को ब्रॉडकास्ट करेंगे:
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
TokenContractCreated: TokenContractCreated, // Emitted when a new token is created
ClassHashUpdated: ClassHashUpdated, // Emitted when the ERC20 class hash is updated
}
#[derive(Drop, starknet::Event)]
struct TokenContractCreated {
#[key]
creator: ContractAddress, // User who created the token
#[key]
token_address: ContractAddress, // Address of the new token contract
name: ByteArray,
symbol: ByteArray,
}
#[derive(Drop, starknet::Event)]
struct ClassHashUpdated {
old_class_hash: ClassHash,
new_class_hash: ClassHash,
}
जब कोई token deploy किया जाता है, तो TokenContractCreated कुशल खोज (efficient searching) के लिए इंडेक्स्ड फ़ील्ड्स (#[key]) के साथ क्रिएटर (creator), token address, name, और symbol को कैप्चर करता है। जब token टेम्प्लेट (class hash) बदलता है, तो ClassHashUpdated पुराने class hash से नए class hash में संक्रमण (transition) को रिकॉर्ड करता है।
Factory Contract Implementation
अब आइए factory इंटरफ़ेस में परिभाषित फ़ंक्शन्स को लागू (implement) करें। Factory tokens बनाने के दो तरीके प्रदान करती है, जो इस बात पर निर्भर करता है कि उपयोगकर्ताओं को अपना स्वयं का salt वैल्यू निर्दिष्ट करने की आवश्यकता है या नहीं:
1. Custom salt के साथ Token का निर्माण
create_token_at फ़ंक्शन का उपयोग कॉलर द्वारा निर्दिष्ट salt के साथ tokens deploy करने के लिए किया जाता है, जो परिणामी contract address को निर्धारित करता है:
fn create_token_at(
ref self: ContractState,
salt: felt252,
name: ByteArray,
symbol: ByteArray,
owner: ContractAddress,
) -> ContractAddress {
let caller = get_caller_address();
let erc20_class_hash = self.token_class_hash.read();
// prepare constructor arguments
let mut constructor_calldata = array![];
name.serialize(ref constructor_calldata);
symbol.serialize(ref constructor_calldata);
owner.serialize(ref constructor_calldata);
// deploy the token contract
let (token_address, _) = deploy_syscall(
erc20_class_hash, salt, constructor_calldata.span(), false // deploy_from_zero
)
.unwrap_syscall();
// add to the global token list
self.created_tokens.push(token_address);
// append to user's token list
let user_tokens_entry = self.user_tokens.entry(caller);
user_tokens_entry.push(token_address);
// emit event
self
.emit(
TokenContractCreated {
creator: caller, token_address, name: name.clone(), symbol: symbol.clone(),
},
);
token_address
}
फ़ंक्शन कॉलर का address प्राप्त करने और स्टोरेज से ERC-20 class hash को पढ़ने के साथ शुरू होता है, फिर constructor arguments को एक array में सीरियलाइज़ करता है। Cairo इन्हें felt252 वैल्यूज़ के एक array के रूप में अपेक्षा करता है जिसे CairoVM समझ सकता है:
let mut constructor_calldata = array![];
name.serialize(ref constructor_calldata);
symbol.serialize(ref constructor_calldata);
owner.serialize(ref constructor_calldata);
तो फ़ंक्शन token name, symbol, और owner address लेता है और उन्हें उसी सटीक क्रम (exact order) में सीरियलाइज़ करता है जो token contract के constructor से मेल खाता है। इस क्रम को गलत करने से deployment विफल हो जाएगा क्योंकि constructor को बेमेल (mismatched) argument प्रकार प्राप्त होंगे।
एक बार डेटा ठीक से सीरियलाइज़ हो जाने के बाद, फ़ंक्शन class hash, salt, और सीरियलाइज़्ड constructor arguments के साथ deploy_syscall को कॉल करता है।
// deploy the token contract
let (token_address, _) = deploy_syscall(
erc20_class_hash, salt, constructor_calldata.span(), false // deploy_from_zero
)
.unwrap_syscall();
जैसा कि deploy_syscall पैरामीटर अनुभाग में उल्लेख किया गया है, deploy_from_zero के लिए false पास करना Starknet को शून्य (zero) के बजाय factory के address से deploy करने के लिए कहता है, जो इस बात को प्रभावित करता है कि अंतिम contract address की गणना कैसे की जाती है।
Deployment सफल होने के बाद, factory नए token को दो जगहों पर ट्रैक करती है: बनाए गए सभी tokens की एक वैश्विक (global) सूची created_tokens और प्रत्येक उपयोगकर्ता के लिए एक व्यक्तिगत सूची user_tokens। यह एक TokenContractCreated event भी एमिट करता है, फिर जिसने भी फ़ंक्शन को कॉल किया था उसे नया contract address वापस कर देता है।
2. Default salt के साथ Token का निर्माण
create_token फ़ंक्शन एक नया token deploy करने का सबसे सरल तरीका प्रदान करता है। यह तीन पैरामीटर्स लेता है: token name, symbol, और owner address, और 0 के डिफ़ॉल्ट salt के साथ वास्तविक deployment का काम create_token_at को सौंप (delegate) देता है।
fn create_token(
ref self: ContractState,
name: ByteArray,
symbol: ByteArray,
owner: ContractAddress,
) -> ContractAddress {
// Use default salt of 0
self.create_token_at(0, name, symbol, owner)
}
0 का डिफ़ॉल्ट salt के रूप में उपयोग करना एक सामान्य परंपरा (convention) है, हालाँकि यदि आप अपनी खुद की factory लागू कर रहे हैं तो कोई भी निश्चित salt वैल्यू काम करेगी।
डिफ़ॉल्ट salt का उपयोग क्यों करें?
- उपयोगकर्ताओं को salt वैल्यूज़ को समझने या प्रबंधित करने की आवश्यकता नहीं है
- जब तक पैरामीटर्स अलग हैं, प्रत्येक deployment को स्वचालित रूप से एक विशिष्ट (unique) address मिल जाता है; अन्यथा आपको वही address मिलेगा (हालाँकि दूसरा deployment विफल हो जाएगा क्योंकि address पहले से ही उपयोग में है)
रजिस्ट्री (Registry) और क्वेरी (Query) फ़ंक्शन्स
get_all_created_tokens फ़ंक्शन उस प्रत्येक token को वापस कर देता है जिसे इस factory द्वारा कभी भी deploy किया गया है:
fn get_all_created_tokens(self: @ContractState) -> Array<ContractAddress> {
let mut tokens = array![];
for i in 0..self.created_tokens.len() {
tokens.append(self.created_tokens.at(i).read());
}
tokens
}
यह created_tokens स्टोरेज वेक्टर के माध्यम से लूप (loop) करता है, प्रत्येक address को एक tokens array में जोड़ता (append) है, और इसे वापस कर देता है।
get_user_tokens फ़ंक्शन इसी तरह काम करता है लेकिन किसी विशिष्ट उपयोगकर्ता द्वारा बनाए गए tokens पर ध्यान केंद्रित करता है:
fn get_user_tokens(self: @ContractState, user: ContractAddress) -> Array<ContractAddress> {
let user_tokens_entry = self.user_tokens.entry(user);
let mut tokens = array![];
for i in 0..user_tokens_entry.len() {
tokens.append(user_tokens_entry.at(i).read());
}
tokens
}
यह इनपुट के रूप में उपयोगकर्ता का address लेता है, user_tokens मैपिंग में उस उपयोगकर्ता की एंट्री (entry) को खोजता है, फिर return array बनाने के लिए उनकी व्यक्तिगत token सूची के माध्यम से लूप करता है। यह वॉलेट इंटरफेस या पोर्टफोलियो ट्रैकर्स के लिए विशेष रूप से उपयोगी है जहां उपयोगकर्ता केवल वही tokens देखना चाहते हैं जो उन्होंने बनाए हैं।
दोनों फ़ंक्शन्स एक ही पैटर्न का उपयोग करते हैं: वे एक म्यूटेबल (mutable) array बनाते हैं, प्रासंगिक स्टोरेज स्ट्रक्चर के माध्यम से पुनरावृति (iterate) करते हैं, और उन्हें मिलने वाले प्रत्येक address को जोड़ते (append) हैं। मुख्य अंतर यह है कि एक ग्लोबल रजिस्ट्री से पढ़ता है जबकि दूसरा उपयोगकर्ता-विशिष्ट मैपिंग से पढ़ता है।
Class hash अपडेट फ़ंक्शन
Factory द्वारा नए deployments के लिए उपयोग किए जाने वाले token contract class को अपडेट करने के लिए update_erc20_class_hash फ़ंक्शन का उपयोग किया जाता है:
fn update_erc20_class_hash(ref self: ContractState, new_class_hash: ClassHash) {
let caller = get_caller_address();
assert(caller == self.factory_owner.read(), 'Only owner can update');
let old_class_hash = self.token_class_hash.read();
self.token_class_hash.write(new_class_hash);
self.emit(ClassHashUpdated { old_class_hash, new_class_hash });
}
जब इसे कॉल किया जाता है, तो फ़ंक्शन सत्यापित (verify) करता है कि कॉलर factory owner है। यदि नहीं, तो ट्रांज़ैक्शन “Only owner can update” त्रुटि (error) के साथ विफल हो जाता है।
एक बार अनुमति (permission) जांच पास हो जाने के बाद, फ़ंक्शन वर्तमान class hash को पढ़ता है और इसे पुरानी वैल्यू के रूप में स्टोर करता है, फिर नए class hash को स्टोरेज में लिखता है। इसका मतलब है कि भविष्य के सभी token deployments अपडेट किए गए contract कार्यान्वयन (implementation) का उपयोग करेंगे, जबकि मौजूदा tokens उनके मूल वर्ज़न पर अपरिवर्तित (unchanged) रहेंगे। इसके बाद फ़ंक्शन एक ClassHashUpdated event एमिट करता है जिसमें दोनों class hash वैल्यूज़ होती हैं।
ध्यान दें कि class hash अपडेट केवल तभी काम करेगा जब नया contract class hash मूल वाले के समान constructor सिग्नेचर और सीरियलाइज़ेशन क्रम (order) का पालन करता हो। वर्तमान में, factory का create_token_at फ़ंक्शन यह अपेक्षा करता है:
fn create_token_at(
ref self: ContractState,
salt: felt252,
name: ByteArray,
symbol: ByteArray,
owner: ContractAddress,
) -> ContractAddress
किसी भी अपडेट किए गए class hash के constructor में इन्हीं सटीक पैरामीटर्स का समान क्रम (order) में होना आवश्यक है।
let mut constructor_calldata = array![];
name.serialize(ref constructor_calldata);
symbol.serialize(ref constructor_calldata);
owner.serialize(ref constructor_calldata);
यदि नए contract में अलग-अलग constructor पैरामीटर्स हैं, तो इनिशियलाइज़ेशन के दौरान सीरियलाइज़ेशन बेमेल (mismatches) के कारण token निर्माण विफल हो जाएगा।
इससे पहले कि हम factory को deploy और टेस्ट करें, पूरे ERC20 factory contract को कॉपी करें और src/erc20_factory.cairo में पेस्ट करें:
use starknet::{ClassHash, ContractAddress};
#[starknet::interface]
pub trait IERC20Factory<TContractState> {
fn create_token(
ref self: TContractState, name: ByteArray, symbol: ByteArray, owner: ContractAddress,
) -> ContractAddress;
fn create_token_at(
ref self: TContractState,
salt: felt252,
name: ByteArray,
symbol: ByteArray,
owner: ContractAddress,
) -> ContractAddress;
fn update_erc20_class_hash(ref self: TContractState, new_class_hash: ClassHash);
fn get_all_created_tokens(self: @TContractState) -> Array<ContractAddress>;
fn get_user_tokens(self: @TContractState, user: ContractAddress) -> Array<ContractAddress>;
}
#[starknet::contract]
mod ERC20TokenFactory {
use starknet::storage::{
Map, MutableVecTrait, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess,
Vec, VecTrait,
};
use starknet::syscalls::deploy_syscall;
use starknet::{ClassHash, ContractAddress, SyscallResultTrait, get_caller_address};
#[storage]
struct Storage {
token_class_hash: ClassHash,
created_tokens: Vec<ContractAddress>,
user_tokens: Map<ContractAddress, Vec<ContractAddress>>,
factory_owner: ContractAddress,
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
TokenContractCreated: TokenContractCreated,
ClassHashUpdated: ClassHashUpdated,
}
#[derive(Drop, starknet::Event)]
struct TokenContractCreated {
#[key]
creator: ContractAddress,
#[key]
token_address: ContractAddress,
name: ByteArray,
symbol: ByteArray,
}
#[derive(Drop, starknet::Event)]
struct ClassHashUpdated {
old_class_hash: ClassHash,
new_class_hash: ClassHash,
}
#[constructor]
fn constructor(ref self: ContractState, token_class_hash: ClassHash, owner: ContractAddress) {
self.token_class_hash.write(token_class_hash);
self.factory_owner.write(owner);
}
#[abi(embed_v0)]
impl ERC20FactoryImpl of super::IERC20Factory<ContractState> {
fn create_token(
ref self: ContractState, name: ByteArray, symbol: ByteArray, owner: ContractAddress,
) -> ContractAddress {
// Use default salt of 0
self.create_token_at(0, name, symbol, owner)
}
fn create_token_at(
ref self: ContractState,
salt: felt252,
name: ByteArray,
symbol: ByteArray,
owner: ContractAddress,
) -> ContractAddress {
let caller = get_caller_address();
let erc20_class_hash = self.token_class_hash.read();
// prepare constructor arguments
let mut constructor_calldata = array![];
name.serialize(ref constructor_calldata);
symbol.serialize(ref constructor_calldata);
owner.serialize(ref constructor_calldata);
// deploy the token contract
let (token_address, _) = deploy_syscall(
erc20_class_hash, salt, constructor_calldata.span(), false // deploy_from_zero
)
.unwrap_syscall();
// track the created token
self.created_tokens.push(token_address);
// track user's tokens
let user_tokens_entry = self.user_tokens.entry(caller);
user_tokens_entry.push(token_address);
// emit event
self
.emit(
TokenContractCreated {
creator: caller, token_address, name: name.clone(), symbol: symbol.clone(),
},
);
token_address
}
fn update_erc20_class_hash(ref self: ContractState, new_class_hash: ClassHash) {
let caller = get_caller_address();
assert(caller == self.factory_owner.read(), 'Only owner can update');
let old_class_hash = self.token_class_hash.read();
self.token_class_hash.write(new_class_hash);
self.emit(ClassHashUpdated { old_class_hash, new_class_hash });
}
fn get_all_created_tokens(self: @ContractState) -> Array<ContractAddress> {
let mut tokens = array![];
for i in 0..self.created_tokens.len() {
tokens.append(self.created_tokens.at(i).read());
}
tokens
}
fn get_user_tokens(self: @ContractState, user: ContractAddress) -> Array<ContractAddress> {
let user_tokens_entry = self.user_tokens.entry(user);
let mut tokens = array![];
for i in 0..user_tokens_entry.len() {
tokens.append(user_tokens_entry.at(i).read());
}
tokens
}
}
}
src/lib.cairo फ़ाइल में, दोनों contracts को पब्लिक (public) मॉड्यूल्स के रूप में घोषित किया जाना चाहिए ताकि उन्हें Cairo कंपाइलर द्वारा एक्सेस किया जा सके:
pub mod erc20;
pub mod erc20_factory;
lib.cairo फ़ाइल Cairo प्रोजेक्ट के लिए एंट्री पॉइंट के रूप में कार्य करती है। यह कंपाइलर को बताता है कि बिल्ड में कौन से मॉड्यूल्स को शामिल करना है। दोनों मॉड्यूल्स को pub के रूप में घोषित करने से वे प्रोजेक्ट के अन्य मॉड्यूल्स के लिए सुलभ (accessible) हो जाते हैं।
अंतिम प्रोजेक्ट संरचना (structure) कुछ इस प्रकार दिखनी चाहिए:
src/
├── lib.cairo
└── erc20.cairo
└── erc20_factory.cairo
sncast का उपयोग करके Factory को Deploy करना
अब जब हमने दोनों contracts लिख लिए हैं, तो आइए उनके class hashes प्राप्त करने के लिए उन्हें declare करें, फिर factory contract को deploy करें, जो बाद में tokens बनाने के लिए ERC-20 class hash का उपयोग करेगा।
चरण 1: ERC-20 Token Contract को Declare करें
सबसे पहले, हमें Starknet पर इसे रजिस्टर करने और इसका class hash प्राप्त करने के लिए अपने token contract को declare करना होगा:
sncast \
--account <ACCOUNT_NAME> \
declare \
--url https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/<YOUR_API_KEY> \
--contract-name ERC20Token
ACCOUNT_NAME को वास्तविक अकाउंट नाम से और अपने YOUR_API_KEY को Alchemy से प्राप्त अपनी API Key से बदलें, फिर कमांड चलाएँ। आपको इसके जैसा आउटपुट दिखाई देगा:

हमें इस class hash को सेव करना होगा, factory deployment के लिए इसकी आवश्यकता होगी।
चरण 2: Factory Contract को Declare करें
इसके बाद, हम अपने factory contract को declare करने के लिए नीचे दी गई sncast कमांड चलाएंगे:
sncast \
--account <ACCOUNT_NAME> \
declare \
--url https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/<YOUR_API_KEY> \
--contract-name ERC20TokenFactory
यह हमारे factory contract के लिए class hash लौटाएगा:

चरण 3: Factory Contract को Deploy करें
अब हम अपने factory contract को उसके class hash का उपयोग करके deploy करते हैं:
sncast \
--account <ACCOUNT_NAME> \
deploy \
--class-hash <ERC20TOKENFACTORY_CLASS_HASH> \
--arguments '<ERC20_CLASS_HASH>, <OWNER_ADDRESS>' \
--url https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/<YOUR_API_KEY>
Constructor calldata में दो arguments होते हैं:
- ERC-20 class hash (
0xea2b282ed...): factory को बताता है कि किस contract class का उपयोग करना है - Factory owner address (
0x014154fb...): यह सेट करता है कि factory के class hash को कौन अपडेट कर सकता है
सफल deployment के बाद, हमें एक contract address मिलेगा:

UDC के माध्यम से Factory Deployment को सत्यापित (Verify) करना
हमारे factory contract के deploy होने के साथ, आइए जांच करें कि वास्तव में इसे कैसे deploy किया गया था ताकि “Custom Factory Contract vs UDC” अनुभाग में हमारे पहले के स्पष्टीकरण (explanation) की पुष्टि हो सके।
Voyager पर, ट्रांज़ैक्शन हैश (contract address नहीं) का उपयोग करके ट्रांज़ैक्शन को खोजें।
internal calls (deployContract फिर “More Details” पर क्लिक करें) को देखकर, हम सत्यापित कर सकते हैं कि factory contract स्वयं Universal Deployer Contract (UDC) के माध्यम से deploy किया गया था:

- UDC deployment call: UDC (शीर्ष पर लाल बॉक्स में हाइलाइट किया गया) factory deployment अनुरोध (request) प्राप्त करता है और आवश्यक पैरामीटर्स के साथ अपने
deployContractफ़ंक्शन को कॉल करता है। - Factory class hash:
classHashपैरामीटर (पीले रंग में हाइलाइट किया गया)0x1843d25804e7cc40c7b77d415b96d2316a6176a3e0ff454bb5a529d1696990aदिखाता है - यह हमारे factory contract का class hash है जिसे पहले declare किया गया था। - Salt:
saltपैरामीटर विशिष्ट (unique) contract addresses सुनिश्चित करने के लिएsncastद्वारा उत्पन्न0x8bab3046b4b8227दिखाता है। - Constructor parameters:
calldataarray में, हम देख सकते हैं:- पहला पैरामीटर (
0x2466cbc06f94c3e7b9a95bfc7ef94295f1546fa1917ded31710510b30d58e3d): यह ERC-20 token contract class hash है जिसका उपयोग हमारी factory tokens को deploy करने के लिए करेगी - दूसरा पैरामीटर (
0x14154fb6dd088b5ceb46df635ecce6e1a9b0455357931ac7df4263a7dbf39a9): यह वह address है जिसे हमने factory owner के रूप में पास किया था
- पहला पैरामीटर (
- Deployment result: आउटपुट deploy किए गए factory contract address को दिखाता है:
0xcdcb37a51075426d2b91ff49769f6909b51e8384ae1aae8dd2b82fdb923e4d
Factory contract व्यक्तिगत (individual) token instances बनाने के लिए सीधे deploy_syscall का उपयोग करता है, जिससे प्रत्येक token deployment के लिए UDC से गुज़रने की आवश्यकता नहीं होती है।
Tokens बनाने के लिए Factory का उपयोग करना
आइए देखें कि उपयोगकर्ता अपने स्वयं के tokens बनाने के लिए हमारी deploy की गई factory के साथ कैसे इंटरैक्ट कर सकते हैं। Token निर्माण प्रक्रिया दिखाने के लिए हम Voyager के इंटरफ़ेस का उपयोग करेंगे।
Factory के माध्यम से एक token बनाना
जब हम Voyager पर 0xcdcb37a51075426d2b91ff49769f6909b51e8384ae1aae8dd2b82fdb923e4d पर अपने factory contract पर जाते हैं, और Write Contract टैब पर जाते हैं, तो हम उपलब्ध write फ़ंक्शन्स देख सकते हैं। हमारे पास सबसे पहला create_token फ़ंक्शन इंटरफ़ेस है जहाँ उपयोगकर्ता अपने token पैरामीटर्स इनपुट कर सकते हैं।
एक token बनाने के लिए, हम आवश्यक फ़ील्ड्स (fields) भरते हैं:
- name: “SarcToken” (हमारे token का पूरा नाम)
- symbol: “SRC” (ट्रेडिंग प्रतीक/symbol)
- owner: वह address जो token का मालिक होगा और उसे नियंत्रित करेगा

अपने वॉलेट को कनेक्ट करने और “Transact” पर क्लिक करने के बाद, factory एक नया ERC-20 token contract deploy करती है। यह ट्रांज़ैक्शन हमारे नए बनाए गए token का address लौटाता है: 0x849daeb52f488856b408df096efcb3cba66243373a5ecb6bd62c1abb7c51d9।
बनाए गए Token को सत्यापित (Verify) करना
अब हम अपने नए बनाए गए token के साथ उसके contract address पर इंटरैक्ट कर सकते हैं। Token का विवरण (details) सत्यापित करने के लिए Read Contract टैब पर जाएं। Token मानक (standard) ERC-20 कार्यक्षमता (functionality) लागू करता है; उपयोगकर्ता tokens मिंट (mint) कर सकते हैं (यदि वे owner हैं), उन्हें ट्रांसफर कर सकते हैं, बैलेंस की जांच कर सकते हैं, और स्पेंडिंग अलाउंस (spending allowances) को मंज़ूरी (approve) दे सकते हैं।
get_all_created_tokens को कॉल करने से उन सभी token contracts का एक array प्राप्त हो सकता है जिन्हें इस factory के माध्यम से deploy किया गया है:

Token Class hash को अपडेट करना
update_erc20_class_hash के माध्यम से class hash को अपडेट करने पर, factory बाद के (subsequent) token deployments के लिए नए टेम्प्लेट का उपयोग करेगी। अपग्रेड करने से पहले deploy किए गए tokens अपने मौजूदा कार्यान्वयन (old class hash) के साथ अपरिवर्तित (unchanged) रहते हैं।
एक बार फिर, class hash को अपडेट करने से पहले, सुनिश्चित करें कि नए contract class में वर्तमान वाले के समान ही constructor सिग्नेचर और सीरियलाइज़ेशन क्रम (order) हो। यदि वे मेल नहीं खाते हैं, तो factory नए contract में गलत constructor पैरामीटर्स पास कर देगी, जिससे deployment विफल हो जाएगा।

निष्कर्ष (Conclusion)
Starknet पर सभी deployments अंततः deploy_syscall फ़ंक्शन का उपयोग करते हैं, लेकिन factories इस लो-लेवल कार्यक्षमता को सरल इंटरफेस में रैप करती हैं। हमारी ERC-20 factory दिखाती है कि कैसे एक contract कई उपयोगकर्ताओं के लिए token निर्माण को संभाल सकता है और साथ ही क्या deploy किया गया है उसका ट्रैक भी रख सकता है।
update_erc20_class_hash फ़ंक्शन नए token वर्ज़न्स में अपडेट करने के लिए अच्छी तरह से काम करता है जो समान constructor पैरामीटर्स का उपयोग करते हैं। उन स्थितियों के लिए जहां आप चाहते हैं कि factory अलग-अलग इनिशियलाइज़ेशन या सीरियलाइज़ेशन आवश्यकताओं के साथ tokens बनाए, आप replace_class_syscall का उपयोग करके स्वयं factory को अपग्रेड कर सकते हैं, जिस पर एक अन्य लेख में चर्चा की जाएगी। इसी तरह, व्यक्तिगत (individual) ERC20 tokens अपग्रेड कार्यक्षमता शामिल कर सकते हैं ताकि आवश्यकता पड़ने पर उनके owners token लॉजिक को अपग्रेड कर सकें।