Starknet पर ERC-20 tokens बिल्कुल Ethereum की तरह ही काम करते हैं। असल में, STRK (Starknet का fee token) खुद एक ERC-20 token है; प्रोटोकॉल स्तर पर कोई विशेष “native” token नहीं है।
Starknet पर ETH और STRK दोनों मानक ERC-20 contracts के रूप में मौजूद हैं, ठीक वैसे ही जैसे कोई अन्य token बनाया जाता है।
इस ट्यूटोरियल में, आप सीखेंगे कि Starknet पर ERC-20 token contract कैसे बनाएं और टेस्ट करें। यह ट्यूटोरियल मानकर चलता है कि आप ERC-20 मानक (standard) से परिचित हैं, लेकिन यह रास्ते में प्रत्येक implementation स्टेप और Cairo सिंटैक्स को समझाता है।
ERC-20 tokens बनाने का पसंदीदा तरीका OpenZeppelin लाइब्रेरी का उपयोग करना है। इसे “Components” पर एक आगामी ट्यूटोरियल में कवर किया जाएगा। इस ट्यूटोरियल का उद्देश्य उन सभी चीज़ों को एक साथ जोड़ना है जो हमने पहले सीखी हैं।
प्रोजेक्ट सेटअप (Project Setup)
एक नया scarb प्रोजेक्ट बनाएं और डायरेक्टरी में नेविगेट करें:
scarb new erc20
cd erc20
Contract Interface
ERC-20 इंटरफ़ेस उस ब्लूप्रिंट को परिभाषित करता है जिसका पालन प्रत्येक fungible token को करना चाहिए। यह token बैलेन्स की जांच करने, tokens ट्रांसफर करने, खर्च करने की अनुमति (spending permissions) प्रबंधित करने, और token मेटाडेटा प्राप्त करने के लिए आवश्यक functions को निर्दिष्ट करता है।
Starknet पर सभी ERC-20 tokens Cairo में निम्नलिखित इंटरफ़ेस को implement करते हैं:
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;
}
यह इंटरफ़ेस Ethereum के ERC-20 मानक को दर्शाता है लेकिन Cairo-विशिष्ट सिंटैक्स और परंपराओं (conventions) का उपयोग करता है। अगले भाग में, हम देखेंगे कि यह Solidity से कैसे अलग है।
Cairo ERC-20 Interface सिंटैक्स Solidity से कैसे अलग है
State References: ध्यान दें कि ऊपर दिए गए Cairo IERC20 इंटरफ़ेस में, view functions के लिए self: @TContractState और state बदलने वाले functions के लिए ref self: TContractState का उपयोग किया गया है। @ प्रतीक (symbol) contract state का एक read-only स्नैपशॉट बनाता है, जबकि ref state में संशोधन (modifications) की अनुमति देता है। उदाहरण के लिए, STRK बैलेन्स चेक करने के लिए @ (view-only) का उपयोग होता है, लेकिन STRK ट्रांसफर करने के लिए ref (balances को संशोधित करता है) का उपयोग होता है।
<TContractState> एक generic टाइप है जो इसी इंटरफ़ेस को किसी भी ERC-20 contract के स्टोरेज लेआउट के साथ काम करने देता है।
Types: Cairo token राशि (amounts) के लिए u256 (Solidity के uint256 के समान) का उपयोग करता है और Ethereum के address टाइप के बजाय ContractAddress का उपयोग करता है। Token इंटरफ़ेस के name और symbol functions एक string के बजाय ByteArray रिटर्न करते हैं।
ये functions Ethereum के ERC-20 मानक के समान ही balances, transfers, allowances और metadata को implement करते हैं, बस सिंटैक्स में अंतर होता है।
ERC-20 Token Contract बनाना
हम Cairo में स्टेप-बाय-स्टेप ERC-20 contract बनाएंगे, बुनियादी संरचना (basic structure) से शुरू करते हुए और धीरे-धीरे इसमें कार्यक्षमता (functionality) जोड़ते हुए, साथ ही रास्ते में प्रमुख functions को टेस्ट भी करेंगे।
src/lib.cairo फ़ाइल में, एक खाली contract मॉड्यूल और इंटरफ़ेस बनाएं जिस पर हम आगे काम करेंगे:
#[starknet::interface]
pub trait IERC20<TContractState> {
// We'll add functions here as we implement them
}
#[starknet::contract]
pub mod ERC20 {}
स्टोरेज सेटअप (Storage Setup)
इसके बाद, हम स्टोरेज वेरिएबल्स को परिभाषित करेंगे जो balances, allowances, metadata और ओनरशिप डेटा को स्टोर करेंगे। हम एड्रेस प्रकारों के लिए Starknet से ContractAddress और स्टोरेज मैपिंग के Cairo संस्करण के लिए Map को इम्पोर्ट करेंगे। स्टोरेज वेरिएबल्स निम्नलिखित को ट्रैक करेंगे:
balances: प्रत्येक एड्रेस के पास कितने tokens हैंallowances: प्रत्येक एड्रेस दूसरे एड्रेस के बैलेन्स से कितना खर्च कर सकता हैtoken_name,symbol,decimalमानक ERC-20 मेटाडेटा हैंtotal_supply: सर्कुलेशन में कुल tokensowner: contract ओनर का एड्रेस
#[starknet::interface]
pub trait IERC20<TContractState> {
// We'll add functions here as we implement them
}
#[starknet::contract]
pub mod ERC20 {
use starknet::ContractAddress;
use starknet::storage::Map;
#[storage]
pub struct Storage {
// Maps each account address to their token balance
balances: Map<ContractAddress, u256>,
// Maps (owner, spender) pairs to approved spending amounts
allowances: Map<(ContractAddress, ContractAddress), u256>,
// Token metadata
token_name: ByteArray,
symbol: ByteArray,
decimal: u8,
// Total number of tokens that exist
total_supply: u256,
// Address that can mint new tokens
owner: ContractAddress,
}
}
यहां बताया गया है कि ये मैपिंग Solidity की तुलना में कैसी दिखती हैं:
mapping(address => uint256) balances;
mapping(address => mapping(address => uint256)) allowances;
Cairo Solidity के नेस्टेड स्ट्रक्चर के बजाय नेस्टेड मैपिंग के लिए tuples (ContractAddress, ContractAddress) का उपयोग करता है:
balances: Map<ContractAddress, u256>, // owner -> amount
allowances: Map<(ContractAddress, ContractAddress), u256>, // (owner, spender) -> amount
हम “RST” सिंबल (symbol) के साथ एक “Rare Token” बनाएंगे। चूंकि नाम, सिंबल और डेसिमल्स आमतौर पर नहीं बदलते हैं, इसलिए हम उन्हें कंस्ट्रक्टर (constructor) में सेट करेंगे। हम स्टोरेज में राइट एक्सेस (write access) सक्षम करने के लिए StoragePointerWriteAccess को भी इम्पोर्ट करते हैं:
#[starknet::interface]
pub trait IERC20<TContractState> {
// We'll add functions here as we implement them
}
#[starknet::contract]
pub mod ERC20 {
use starknet::ContractAddress;
use starknet::storage::{Map, StoragePointerWriteAccess};
#[storage]
pub struct Storage {
balances: Map<ContractAddress, u256>,
allowances: Map<(ContractAddress, ContractAddress), u256>, // (owner, spender) -> amount
token_name: ByteArray,
symbol: ByteArray,
decimal: u8,
total_supply: u256,
owner: ContractAddress,
}
//NEWLY ADDED
#[constructor]
fn constructor(ref self: ContractState, owner: ContractAddress) {
// Set the token's metadata
self.token_name.write("Rare Token");
self.symbol.write("RST");
self.decimal.write(18);
// Set owner
self.owner.write(owner); // Usually the deployer's address
}
}
कंस्ट्रक्टर token को नाम “Rare Token”, सिंबल “RST”, 18 डेसिमल्स (अधिकतर tokens के लिए मानक), और ओनर एड्रेस के साथ इनिशियलाइज़ करता है। ref self: ContractState पैरामीटर हमें contract के स्टोरेज को संशोधित करने की अनुमति देता है।
आप सोच सकते हैं कि हमने
get_caller_address()का उपयोग करके डिप्लॉयर को स्वचालित रूप से ओनर सेट करने के बजाय ओनर एड्रेस को पैरामीटर के रूप में क्यों पास किया।
यह डिज़ाइन विकल्प जानबूझकर लिया गया है और यह इससे संबंधित है कि Starknet पर contract डिप्लॉयमेंट कैसे काम करता है। जब किसी ऐसे contract को डिप्लॉय किया जाता है जो अपने कंस्ट्रक्टर मेंget_caller_address()का उपयोग करता है, तो Universal Deployer Contract (UDC) contract को डिप्लॉय करता है, न कि आपका अकाउंट सीधे। इसलिए,get_caller_address()आपके अकाउंट के एड्रेस के बजाय UDC का एड्रेस रिटर्न करता है। UDC के बारे में इस सीरीज़ में आगे “Deploying Contracts on Starknet” चैप्टर में विस्तार से समझाया गया है।
Events डिक्लेरेशन (Events Declaration)
ट्रांसफर्स और अप्रूवल को ट्रैक करने के लिए स्टोरेज सेक्शन के बाद निम्नलिखित events जोड़ें:
// Define the events that this contract can emit
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
Transfer: Transfer, // Emitted when tokens are transferred
Approval: Approval, // Emitted when spending approval is granted
}
// Event emitted whenever tokens are transferred between addresses
#[derive(Drop, starknet::Event)]
pub struct Transfer {
#[key] // Indexed field (can be filtered when querying events)
from: ContractAddress, // Address sending the tokens
#[key] // Indexed field (can be filtered when querying events)
to: ContractAddress, // Address receiving the tokens
amount: u256, // Number of tokens transferred
}
// Event emitted when an owner approves a spender to use their tokens
#[derive(Drop, starknet::Event)]
pub struct Approval {
#[key] // Indexed field (can be filtered when querying events)
owner: ContractAddress, // Address that owns the tokens
#[key] // Indexed field (can be filtered when querying events)
spender: ContractAddress, // Address approved to spend the tokens
value: u256, // Amount approved for spending
}
Event enum में वे सभी events शामिल हैं जिन्हें contract एमिट (emit) कर सकता है: Transfer और Approval।
Transfereventsfromऔरtoएड्रेस के साथ-साथamountद्वारा token मूवमेंट को ट्रैक करते हैं।Approvalevents भी खर्च करने की अनुमतियों (spending permissions) को ट्रैक करते हैं, जिसमें वहownerजो अनुमति देता है, वहspenderजो इसे प्राप्त करता है, और अप्रूव की गईvalueशामिल होती है।
पैरामीटर्स अनुक्रमित (indexed) होते हैं ताकि हम आसानी से विशिष्ट एड्रेस से ट्रांसफर्स या विशेष ओनर्स के लिए अप्रूवल्स को क्वेरी कर सकें।
Contract इम्प्लीमेंटेशन (Contract Implementation)
अब आइए contract functions को implement करें। चूंकि बाहरी contracts और उपयोगकर्ताओं को हमारे ERC20 token के साथ इंटरेक्ट करने की आवश्यकता होती है, इसलिए हमें अपने इम्प्लीमेंटेशन को contract के बाहर से कॉल करने योग्य (callable) बनाने की आवश्यकता है। हम ऐसा #[abi(embed_v0)] एट्रिब्यूट जोड़कर करते हैं, जो इम्प्लीमेंटेशन को contract के ABI में एम्बेड (embed) कर देता है:
#[abi(embed_v0)]
impl ERC20Impl of super::IERC20<ContractState> {
// Implementation functions go here
}
ERC20Impl हमारे द्वारा पहले परिभाषित किए गए IERC20 इंटरफ़ेस को implement करता है, जिसमें ContractState contract के स्टोरेज का प्रतिनिधित्व (representing) करता है।
मेटाडेटा Functions: name, symbol, और decimals
मेटाडेटा functions बुनियादी token विवरण रिटर्न करते हैं जैसे नाम, सिंबल, और डेसिमल प्रिसिजन (decimal precision)। आइए functions को implement करके शुरू करते हैं।
इंटरफ़ेस में उनके function सिग्नेचर जोड़ें:
#[starknet::interface]
pub trait IERC20<TContractState> {
fn name(self: @TContractState) -> ByteArray;
fn symbol(self: @TContractState) -> ByteArray;
fn decimals(self: @TContractState) -> u8;
}
फिर ERC20Impl ब्लॉक के अंदर functions को implement करें:
#[abi(embed_v0)]
impl ERC20Impl of super::IERC20<ContractState> {
// Returns the full name of the token
fn name(self: @ContractState) -> ByteArray {
self.token_name.read()
}
// Returns the token's symbol/ticker
fn symbol(self: @ContractState) -> ByteArray {
self.symbol.read()
}
// Returns the number of decimal places for the token
fn decimals(self: @ContractState) -> u8 {
self.decimal.read()
}
// other functions goes here
}
प्रत्येक function contract इनिशियलाइज़ेशन के दौरान कंस्ट्रक्टर में सेट की गई एक स्टोर्ड वैल्यू को रीड करता है। इन रीड्स को सक्षम करने के लिए StoragePointerReadAccess को शामिल करने हेतु contract मॉड्यूल के अंदर imports को अपडेट करें:
use starknet::storage::{
Map, StoragePointerWriteAccess, StoragePointerReadAccess
};
यहाँ तक का पूरा कोड इस प्रकार है:
#[starknet::interface]
pub trait IERC20<TContractState> {
fn name(self: @TContractState) -> ByteArray;
fn symbol(self: @TContractState) -> ByteArray;
fn decimals(self: @TContractState) -> u8;
}
#[starknet::contract]
pub mod ERC20 {
use starknet::ContractAddress;
use starknet::storage::{Map, StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
pub struct Storage {
balances: Map<ContractAddress, u256>,
allowances: Map<(ContractAddress, ContractAddress), u256>,
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, owner: ContractAddress) {
self.token_name.write("Rare Token");
self.symbol.write("RST");
self.decimal.write(18);
self.owner.write(owner);
}
#[abi(embed_v0)]
impl ERC20Impl of super::IERC20<ContractState> {
// Returns the full name of the token
fn name(self: @ContractState) -> ByteArray {
self.token_name.read()
}
// Returns the token's symbol/ticker
fn symbol(self: @ContractState) -> ByteArray {
self.symbol.read()
}
// Returns the number of decimal places for the token
fn decimals(self: @ContractState) -> u8 {
self.decimal.read()
}
}
}
टेस्ट सेटअप (Test Setup)
अपनी प्रोजेक्ट डायरेक्टरी में test/test_contract.cairo पर नेविगेट करें। केवल बुनियादी imports को छोड़ते हुए, बॉयलरप्लेट (boilerplate) टेस्ट्स को हटा दें:
use starknet::ContractAddress;
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait};
Contract कंस्ट्रक्टर को एक पैरामीटर के रूप में एक ओनर एड्रेस की आवश्यकता होती है:
#[constructor]
fn constructor(ref self: ContractState, owner: ContractAddress) {
self.token_name.write("Rare Token");
self.symbol.write("RST");
self.decimal.write(18);
self.owner.write(owner);
}
चूंकि कंस्ट्रक्टर को एक ओनर एड्रेस की अपेक्षा (expects) होती है, इसलिए हमें अपने टेस्ट्स में contract डिप्लॉय करते समय एक एड्रेस प्रदान करने की आवश्यकता है। इसे हैंडल करने के लिए, एक deploy_contract हेल्पर function बनाएं जो ओनर एड्रेस को पैरामीटर के रूप में लेता है और इसे कंस्ट्रक्टर को पास करता है।
साथ ही, डिप्लॉय किए गए contract के साथ इंटरेक्ट करने के लिए टेस्ट में डिस्पेचर (dispatcher) को इम्पोर्ट करें, ताकि कुल मिलाकर हमारे पास यह हो:
use starknet::ContractAddress;
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait};
//NEWLY ADDED BELOW//
use erc20::IERC20Dispatcher;
use erc20::IERC20DispatcherTrait;
// Helper function to deploy the ERC20 contract with a specified owner
fn deploy_contract(name: ByteArray, owner: ContractAddress) -> ContractAddress {
let contract = declare(name).unwrap().contract_class(); // Declare the contract class
let constructor_args = array![owner.into()]; // Pass owner to constructor
let (contract_address, _) = contract.deploy(@constructor_args).unwrap(); // Deploy the contract and return its address
contract_address
}
IERC20Dispatcher और IERC20DispatcherTrait डिस्पेचर्स हमें टेस्ट्स से contract functions को कॉल करने की अनुमति देते हैं।
deploy_contract function contract क्लास को डिक्लेयर (declare) करता है, constructor_args के माध्यम से ओनर एड्रेस को कंस्ट्रक्टर में पास करता है, और हमें इंटरेक्ट करने के लिए डिप्लॉय किए गए contract का एड्रेस रिटर्न करता है।
चूंकि प्रत्येक टेस्ट के लिए एक contract डिप्लॉयमेंट की आवश्यकता होती है, इसलिए हर बार एक नया एड्रेस बनाने के बजाय एक सुसंगत (consistent) टेस्ट ओनर एड्रेस प्रदान करने के लिए एक OWNER कांस्टेंट (constant) परिभाषित करें:
const OWNER: ContractAddress = 'OWNER'.try_into().unwrap();
इस तरह, प्रत्येक टेस्ट एक सुसंगत ओनर एड्रेस के साथ contract डिप्लॉय करने के लिए बस deploy_contract("ERC20", OWNER) को कॉल कर सकता है।
कंस्ट्रक्टर इनिशियलाइज़ेशन की टेस्टिंग
अगला कदम यह पुष्टि करना है कि कंस्ट्रक्टर मेटाडेटा को सही ढंग से इनिशियलाइज़ करता है। नीचे दिया गया टेस्ट contract को डिप्लॉय करता है, उसके मेटाडेटा functions (name(), symbol(), decimal()) को कॉल करता है, और रिटर्न की गई वैल्यूज़ की जांच करता है:
use starknet::ContractAddress;
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait};
use erc20::IERC20Dispatcher;
use erc20::IERC20DispatcherTrait;
fn deploy_contract(name: ByteArray, owner: ContractAddress) -> ContractAddress {
let contract = declare(name).unwrap().contract_class();
let constructor_args = array![owner.into()];
let (contract_address, _) = contract.deploy(@constructor_args).unwrap();
contract_address
}
const OWNER: ContractAddress = 'OWNER'.try_into().unwrap();
// NEWLY ADDED BELOW
#[test]
fn test_token_constructor() {
// Deploy the ERC20 contract with OWNER as the owner
let contract_address = deploy_contract("ERC20", OWNER);
// Create a dispatcher to interact with the deployed contract
let erc20_token = IERC20Dispatcher { contract_address };
// Retrieve token metadata from the contract
let token_name = erc20_token.name();
let token_symbol = erc20_token.symbol();
let token_decimal = erc20_token.decimals();
// Verify that the constructor set the correct values
assert(token_name == "Rare Token", 'Wrong token name');
assert(token_symbol == "RST", 'Wrong token symbol');
assert(token_decimal == 18, 'Wrong token decimal');
}
test_token_constructor में contract डिप्लॉय करने के बाद, हम contract के साथ इंटरेक्ट करने के लिए डिप्लॉय किए गए contract के एड्रेस के साथ एक IERC20Dispatcher इंस्टेंस बनाते हैं। फिर प्रत्येक मेटाडेटा function को कॉल करें और assert करें कि token नाम, सिंबल और डेसिमल्स उसी से मेल खाते हैं जो कंस्ट्रक्टर में सेट किया गया था। यदि कोई भी वैल्यू मेल नहीं खाती है, तो टेस्ट संबंधित एरर मैसेज के साथ फेल हो जाएगा।
यह पुष्टि करने के लिए कि टेस्ट पास होता है, scarb test test_token_constructor रन करें। आप अपेक्षित एरर्स (expected errors) देखने के लिए गलत वैल्यूज़ के साथ भी टेस्ट कर सकते हैं।
total_supply इम्प्लीमेंट करना
कितने tokens मौजूद हैं यह ट्रैक करने के लिए, हम इंटरफ़ेस में एक total supply function शामिल करेंगे और बनाए गए कुल tokens की संख्या को रीड और रिटर्न करने के लिए इसे implement करेंगे।
इंटरफ़ेस में function सिग्नेचर जोड़ें:
#[starknet::interface]
pub trait IERC20<TContractState> {
fn name(self: @TContractState) -> ByteArray;
fn symbol(self: @TContractState) -> ByteArray;
fn decimals(self: @TContractState) -> u8;
//NEWLY ADDED
fn total_supply(self: @TContractState) -> u256;
}
फिर इसे contract में implement करें:
#[abi(embed_v0)]
impl ERC20Impl of super::IERC20<ContractState> {
// ...previous functions....
fn total_supply(self: @ContractState) -> u256 {
// Read the total_supply value from contract storage
self.total_supply.read()
}
}
total_supply function को टेस्ट करने के लिए, हमें पहले tokens मिंट (mint) करने होंगे और फिर यह पुष्टि करनी होगी कि total supply मिंट की गई राशि को दर्शाती है। इसलिए, हमें tokens मिंट करने के लिए function को implement करने की आवश्यकता है।
mint इम्प्लीमेंट करना
mint के बिना, कोई भी tokens मौजूद नहीं हो सकता क्योंकि सभी balances शून्य से शुरू होते हैं।
mint function ERC-20 स्पेसिफिकेशन में नहीं है, लेकिन यह tokens बनाने और total supply बढ़ाने के लिए आवश्यक है।
इसे इंटरफ़ेस में जोड़ें:
use starknet::ContractAddress;
#[starknet::interface]
pub trait IERC20<TContractState> {
fn name(self: @TContractState) -> ByteArray;
fn symbol(self: @TContractState) -> ByteArray;
fn decimals(self: @TContractState) -> u8;
fn total_supply(self: @TContractState) -> u256;
//NEWLY ADDED
fn mint(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool;
}
हम ContractAddress को इम्पोर्ट करते हैं क्योंकि mint function इसे पैरामीटर टाइप के रूप में उपयोग करता है।
फिर contract में mint function को implement करें:
#[abi(embed_v0)]
impl ERC20Impl of super::IERC20<ContractState> {
// ....previous functions.....//
fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool {
// Get the address of whoever is calling this function
let caller = get_caller_address();
// Only the contract owner is allowed to mint new tokens
assert(caller == self.owner.read(), 'Call not owner');
// Read current values before updating
let previous_total_supply = self.total_supply.read();
let previous_balance = self.balances.entry(recipient).read();
// Increase total supply by the minted amount
self.total_supply.write(previous_total_supply + amount);
// Add the minted tokens to recipient's balance
self.balances.entry(recipient).write(previous_balance + amount);
// Emit transfer from zero address
let zero_address: ContractAddress = 0.try_into().unwrap();
self.emit(Transfer { from: zero_address, to: recipient, amount });
true // Return success
}
}
mint एक प्राप्तकर्ता (recipient) एड्रेस और अमाउंट को पैरामीटर्स के रूप में लेता है। केवल contract ओनर ही इस function को कॉल कर सकता है, इसीलिए caller == owner की जांच की जाती है।
जब tokens मिंट किए जाते हैं, तो total supply और प्राप्तकर्ता का बैलेन्स दोनों निर्दिष्ट (specified) अमाउंट से बढ़ जाते हैं।
Solidity से याद करें कि नए मिंट किए गए tokens हमेशा शून्य एड्रेस (zero address) से ट्रांसफर्स के रूप में दिखाई देते हैं क्योंकि वे शून्य से (from nothing) बनाए जाते हैं। हम यहां भी उसी पैटर्न का पालन करते हैं, शून्य एड्रेस से प्राप्तकर्ता को एक Transfer event एमिट (emit) करते हैं।
let zero_address: ContractAddress = 0.try_into().unwrap();
self.emit(Transfer { from: zero_address, to: recipient, amount });
हम StoragePathEntry इम्पोर्ट करते हैं क्योंकि हम Map कीज़ (keys) तक पहुंचने के लिए .entry() का उपयोग करते हैं, जो विशिष्ट मैपिंग एंट्रीज़ के लिए एक पाथ बनाता है, और वर्तमान कॉलर का एड्रेस प्राप्त करने के लिए get_caller_address को भी इम्पोर्ट करते हैं।
Imports को अपडेट करें:
use starknet::storage::{
Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess,
};
use starknet::{ContractAddress, get_caller_address};
यहाँ तक का पूरा कोड इस प्रकार है:
use starknet::ContractAddress;
#[starknet::interface]
pub trait IERC20<TContractState> {
fn name(self: @TContractState) -> ByteArray;
fn symbol(self: @TContractState) -> ByteArray;
fn decimals(self: @TContractState) -> u8;
fn total_supply(self: @TContractState) -> u256;
fn mint(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool;
}
#[starknet::contract]
pub mod ERC20 {
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>,
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, owner: ContractAddress) {
self.token_name.write("Rare Token");
self.symbol.write("RST");
self.decimal.write(18);
self.owner.write(owner);
}
#[abi(embed_v0)]
impl ERC20Impl of super::IERC20<ContractState> {
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 total_supply(self: @ContractState) -> u256 {
self.total_supply.read()
}
fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool {
let caller = get_caller_address();
assert(caller == self.owner.read(), 'Call 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
}
}
}
total_supply की टेस्टिंग
चूंकि केवल contract ओनर ही tokens मिंट कर सकता है, और टेस्ट डिफ़ॉल्ट रूप से ओनर के रूप में रन नहीं होते हैं, इसलिए ओनर एड्रेस को impersonate (प्रतिरूपण) करने की आवश्यकता है।
हम contract के एक्सेस कंट्रोल चेक्स को बायपास करते हुए, यह अस्थायी रूप से बदलने के लिए cheat_caller_address का उपयोग करेंगे कि contract के अनुसार उसे कौन कॉल कर रहा है। इस चीट (cheat) को केवल अगले function कॉल (mint()) पर लागू करने के लिए CheatSpan::TargetCalls(1) सेट करें।
snforge_std से cheat_caller_address और CheatSpan इम्पोर्ट करें, और मिंट किए गए tokens प्राप्त करने के लिए एक टेस्ट प्राप्तकर्ता एड्रेस जनरेट करने हेतु एक हेल्पर function जोड़ें, ताकि अंततः हमारे पास यह हो:
use starknet::ContractAddress;
use snforge_std::{
declare, ContractClassTrait, DeclareResultTrait,
cheat_caller_address, CheatSpan
};
use erc20::IERC20Dispatcher;
use erc20::IERC20DispatcherTrait;
fn deploy_contract(name: ByteArray, owner: ContractAddress) -> ContractAddress {
let contract = declare(name).unwrap().contract_class();
let constructor_args = array![owner.into()];
let (contract_address, _) = contract.deploy(@constructor_args).unwrap();
contract_address
}
const OWNER: ContractAddress = 'OWNER'.try_into().unwrap();
// NEWLY ADDED
// Test recipient address constant
const TOKEN_RECIPIENT: ContractAddress = 'RECIPIENT'.try_into().unwrap();
अब टेस्ट लिखें:
#[test]
fn test_total_supply() {
// Deploy the contract
let contract_address = deploy_contract("ERC20", OWNER);
// Create dispatcher to interact with the contract
let erc20_token = IERC20Dispatcher { contract_address };
// Calculate mint amount: 1000 tokens adjusted for decimals
let token_decimal = erc20_token.decimals();
let mint_amount = 1000 * token_decimal.into();
// Impersonate the owner for the next function call (mint)
cheat_caller_address(contract_address, OWNER, CheatSpan::TargetCalls(1));
erc20_token.mint(TOKEN_RECIPIENT, mint_amount);
// Get the total supply
let supply = erc20_token.total_supply();
// Verify total supply matches the minted amount
assert(supply == mint_amount, 'Incorrect Supply');
}
test_total_supply टेस्ट contract को डिप्लॉय करता है और 1000 tokens को डेसिमल स्थानों (18) से गुणा करके मिंट अमाउंट की गणना करता है। mint को कॉल करने से पहले, cheat_caller_address कॉलर को ओनर एड्रेस पर सेट करता है, जिससे मिंट को assert(caller == owner) चेक बायपास करने की अनुमति मिलती है। एक प्राप्तकर्ता एड्रेस पर मिंट करने के बाद, टेस्ट total supply प्राप्त करता है और सत्यापित करता है कि यह मिंट किए गए अमाउंट के बराबर है।
test_contract.cairo फ़ाइल में टेस्ट जोड़ें, फिर यह देखने के लिए कि यह पास होता है, scarb test test_total_supply रन करें।
Token ट्रांसफर इम्प्लीमेंट करना
transfer function कॉलर से प्राप्तकर्ता को tokens भेजने (moving) को हैंडल करता है। सबसे पहले, इंटरफ़ेस में function सिग्नेचर जोड़ें:
use starknet::ContractAddress;
#[starknet::interface]
pub trait IERC20<TContractState> {
fn name(self: @TContractState) -> ByteArray;
fn symbol(self: @TContractState) -> ByteArray;
fn decimals(self: @TContractState) -> u8;
fn total_supply(self: @TContractState) -> u256;
fn mint(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool;
//NEWLY ADDED
fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool;
}
अब, ERC20Impl ब्लॉक के अंदर transfer function को implement करें:
#[abi(embed_v0)]
impl ERC20Impl of super::IERC20<ContractState> {
//.....previous functions.....//
fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool {
// Get the address of whoever called this function
let sender = get_caller_address();
// Read current balances for both sender and recipient
let sender_prev_balance = self.balances.entry(sender).read();
let recipient_prev_balance = self.balances.entry(recipient).read();
// Check if sender has enough tokens to transfer
assert(sender_prev_balance >= amount, 'Insufficient amount');
// Update balances: subtract from sender, add to recipient
self.balances.entry(sender).write(sender_prev_balance - amount);
self.balances.entry(recipient).write(recipient_prev_balance + amount);
// Verify the transfer worked correctly
assert(
self.balances.entry(recipient).read() > recipient_prev_balance,
'Transaction failed',
);
// Emit an event to log this Transfer
self.emit(Transfer { from: sender, to: recipient, amount });
true // Return success
}
}
}
मान लें कि Alice के पास 100 RareTokens हैं और वह 30 Bob को भेजना चाहती है, जिसके पास 50 हैं। यह function जांचता है कि क्या Alice के पास पर्याप्त tokens हैं (100 >= 30), Alice का बैलेन्स 70 पर अपडेट करता है, और Bob का बैलेन्स 80 पर अपडेट करता है। इसके बाद यह पुष्टि करता है कि Bob का बैलेन्स बढ़ गया है और इस ट्रांजैक्शन को लॉग करने के लिए from: Alice, to: Bob, और amount: 30 के साथ एक Transfer event एमिट करता है, और सफल पूर्णता (successful completion) का संकेत देने के लिए true रिटर्न करता है।
ट्रांसफर function को टेस्ट करने के लिए, contract को विशिष्ट बिंदुओं (specific points) पर प्रत्येक अकाउंट के बैलेन्स की जांच करने के एक तरीके की आवश्यकता होती है।
balance_of इम्प्लीमेंट करना
Token balances क्वेरी करने के लिए आइए balance_of जोड़ें। इंटरफ़ेस में function सिग्नेचर जोड़ें:
use starknet::ContractAddress;
#[starknet::interface]
pub trait IERC20<TContractState> {
fn name(self: @TContractState) -> ByteArray;
fn symbol(self: @TContractState) -> ByteArray;
fn decimals(self: @TContractState) -> u8;
fn total_supply(self: @TContractState) -> u256;
fn mint(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool;
fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool;
//NEWLY ADDED
fn balance_of(self: @TContractState, account: ContractAddress) -> u256;
}
फिर इसे contract में implement करें:
#[abi(embed_v0)]
impl ERC20Impl of super::IERC20<ContractState> {
//.....previous functions.....//
fn balance_of(self: @ContractState, account: ContractAddress) -> u256 {
// Use .entry() to access the specific account's balance in the Map
let balance = self.balances.entry(account).read();
balance
}
}
किसी अकाउंट का RareToken बैलेन्स चेक करने के लिए, balance_of(account_address) balances मैपिंग में एड्रेस को खोजता है और संबंधित वैल्यू रिटर्न करता है।
transfer की टेस्टिंग
transfer function को टेस्ट करने के लिए, हमें पहले एक अकाउंट में tokens चाहिए, फिर यह सत्यापित करें कि ट्रांसफर करने से सेंडर से प्राप्तकर्ता तक tokens सही ढंग से जाते हैं। हम ओनर को tokens मिंट करेंगे, फिर कुछ प्राप्तकर्ता को ट्रांसफर करेंगे और दोनों balances की जांच करेंगे।
चूंकि मिंटिंग और ट्रांसफरिंग दोनों के लिए ओनर की अनुमति की आवश्यकता होती है, इसलिए हम स्पष्ट रूप से stop_cheat_caller_address के साथ रोके जाने तक लगातार कई कॉल्स के लिए ओनर को impersonate करने के लिए start_cheat_caller_address का उपयोग करेंगे।
अन्य imports के साथ snforge_std से start_cheat_caller_address और stop_cheat_caller_address को इम्पोर्ट करें:
use snforge_std::{
declare, ContractClassTrait, DeclareResultTrait,
cheat_caller_address, CheatSpan,
start_cheat_caller_address, stop_cheat_caller_address
};
अब यहाँ टेस्ट है:
#[test]
fn test_transfer() {
// Deploy the contract
let contract_address = deploy_contract("ERC20", OWNER);
let erc20_token = IERC20Dispatcher { contract_address };
// Get token decimals for proper amount calculation
let token_decimal = erc20_token.decimals();
// Define amounts: 10,000 tokens to mint, 5,000 to transfer
let amount_to_mint: u256 = 10000 * token_decimal.into();
let amount_to_transfer: u256 = 5000 * token_decimal.into();
// Start impersonating the owner for multiple calls
start_cheat_caller_address(contract_address, OWNER);
// Mint tokens to the owner
erc20_token.mint(OWNER, amount_to_mint);
// Verify the mint was successful
assert(erc20_token.balance_of(OWNER) == amount_to_mint, 'Incorrect minted amount');
// Track recipient's balance before transfer
let receiver_previous_balance = erc20_token.balance_of(TOKEN_RECIPIENT);
// Transfer tokens from owner to recipient
erc20_token.transfer(TOKEN_RECIPIENT, amount_to_transfer);
// Stop impersonating the owner
stop_cheat_caller_address(contract_address);
// Verify sender's balance decreased correctly
assert(erc20_token.balance_of(OWNER) < amount_to_mint, 'Sender balance not reduced');
assert(erc20_token.balance_of(OWNER) == amount_to_mint - amount_to_transfer, 'Wrong sender balance');
// Verify recipient's balance increased correctly
assert(erc20_token.balance_of(TOKEN_RECIPIENT) > receiver_previous_balance, 'Recipient balance unchanged');
assert(erc20_token.balance_of(TOKEN_RECIPIENT) == amount_to_transfer, 'Wrong recipient amount');
}
test_transfer() में contract डिप्लॉय करने के बाद, टेस्ट अमाउंट्स की गणना करता है: मिंटिंग के लिए 10,000 tokens और ट्रांसफर के लिए 5,000। यह start_cheat_caller_address के साथ ओनर को impersonate करना शुरू करता है, जो ओनर के अकाउंट में tokens मिंट करने की अनुमति देता है। मिंट सफल होने के बाद, टेस्ट ट्रांसफर करने से पहले प्राप्तकर्ता का बैलेन्स रिकॉर्ड करता है।
इसके बाद टेस्ट प्राप्तकर्ता को 5,000 tokens ट्रांसफर करता है और impersonation को रोकता है। अंतिम assertions ट्रांजैक्शन के दोनों पक्षों को सत्यापित करते हैं: कि ओनर का बैलेन्स ठीक 5,000 tokens कम हो गया, और प्राप्तकर्ता का बैलेन्स समान राशि से बढ़ गया। यह पुष्टि करता है कि transfer अकाउंट्स के बीच सही ढंग से tokens को मूव करता है।
टेस्ट को test_contract.cairo फ़ाइल में जोड़ें, फिर यह सत्यापित करने के लिए कि यह पास होता है, scarb test test_transfer रन करें।
ट्रांसफर के लिए Insufficient (अपर्याप्त) बैलेन्स की टेस्टिंग
आइए टेस्ट करें कि transfer सेंडर के पास मौजूद tokens से अधिक ट्रांसफर करने के प्रयासों (attempts) को ठीक से अस्वीकार (rejects) करता है।
5000 के बजाय 11,000 tokens ट्रांसफर करने का प्रयास करने के लिए हमारे टेस्ट में transfer कॉल को संशोधित (modify) करें:
erc20_token.transfer(TOKEN_RECIPIENT, 11000 * token_decimal.into());
जब हम scarb test test_transfer रन करते हैं, तो टेस्ट इस एरर के साथ फेल होना चाहिए:

यह पुष्टि करता है कि contract सही ढंग से काम कर रहा है, यह सेंडर के स्वामित्व (owns) वाले tokens से अधिक के ट्रांसफर को रोक रहा है, जिससे transfer function में assert(sender_prev_balance >= amount, 'Insufficient amount') चेक ट्रिगर होता है।
टेस्ट को पास रखने के लिए, अमाउंट को वापस
amount_to_transfer(5,000 tokens या ओनर के 10,000 के बैलेन्स से कम या उसके बराबर कोई भी अमाउंट) में बदल दें
विकल्प (Alternative): फेलियर केस के लिए एक समर्पित (dedicated) टेस्ट बनाएं
मौजूदा टेस्ट को संशोधित करने के बजाय, test_contract.cairo में #[should_panic] का उपयोग करके यह टेस्ट जोड़ें:
#[test]
#[should_panic(expected: ('Insufficient amount',))]
fn test_transfer_insufficient_balance() {
// Deploy the contract
let contract_address = deploy_contract("ERC20", OWNER);
let erc20_token = IERC20Dispatcher { contract_address };
let token_decimal = erc20_token.decimals();
// Define amounts: only 5,000 tokens minted, but attempting to transfer 10,000
let mint_amount: u256 = 5000 * token_decimal.into();
let transfer_amount: u256 = 10000 * token_decimal.into();
// Start impersonating the owner
start_cheat_caller_address(contract_address, OWNER);
// Mint only 5,000 tokens to the owner
erc20_token.mint(OWNER, mint_amount);
// Verify the mint was successful
assert(erc20_token.balance_of(OWNER) == mint_amount, 'Mint failed');
// Attempt to transfer more than balance (10,000 tokens when only 5,000 exist)
// This should panic with 'Insufficient amount'
erc20_token.transfer(TOKEN_RECIPIENT, transfer_amount);
// Stop impersonating the owner
stop_cheat_caller_address(contract_address);
}
यह टेस्ट सत्यापित करता है कि जब सेंडर अपने पास मौजूद tokens से अधिक भेजने का प्रयास करता है तो ट्रांसफर फेल हो जाता है। ओनर के पास केवल 5,000 tokens हैं लेकिन वह 10,000 ट्रांसफर करने की कोशिश करता है, जिससे transfer function में assert(sender_prev_balance >= amount, 'Insufficient amount') चेक ट्रिगर होता है। #[should_panic] एट्रिब्यूट टेस्ट फ्रेमवर्क को बताता है कि यह टेस्ट विशिष्ट एरर मैसेज 'Insufficient amount' के साथ पैनिक (panic) होने की अपेक्षा (expected) करता है।
यह सत्यापित करने के लिए कि यह पास होता है, scarb test test_transfer_insufficient_balance रन करें।
allowance इम्प्लीमेंट करना
allowance function यह जांचता है कि किसी एक एड्रेस को दूसरे की ओर से कितना खर्च करने की अनुमति है। आइए इंटरफ़ेस में function सिग्नेचर जोड़ें:
use starknet::ContractAddress;
#[starknet::interface]
pub trait IERC20<TContractState> {
// ... previous functions ...
//NEWLY ADDED
fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256;
}
फिर इसे contract में implement करें:
#[abi(embed_v0)]
impl ERC20Impl of super::IERC20<ContractState> {
//....previous functions....//
fn allowance(
self: @ContractState, owner: ContractAddress, spender: ContractAddress,
) -> u256 {
// Access the allowances Map using a tuple key (owner, spender)
self.allowances.entry((owner, spender)).read()
}
उदाहरण के लिए, यह देखने के लिए कि Bob, Alice के अकाउंट से कितने RareTokens खर्च कर सकता है, आप allowance(Alice, Bob) कॉल करेंगे।
approve इम्प्लीमेंट करना
approve function यह सेट करके खर्च करने की अनुमति देता है कि कोई व्यक्ति (spender) किसी अकाउंट के बैलेन्स (owner) से कितना निकाल सकता है।
इंटरफ़ेस में function सिग्नेचर जोड़ें:
use starknet::ContractAddress;
#[starknet::interface]
pub trait IERC20<TContractState> {
// ... previous functions ...
//NEWLY ADDED
fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool;
}
फिर इसे contract में implement करें:
#[abi(embed_v0)]
impl ERC20Impl of super::IERC20<ContractState> {
//.....previous functions.....//
fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool {
// Get the address of whoever is giving the approval (owner)
let caller = get_caller_address();
// Set the allowance: how much the spender can spend on behalf of the caller (owner)
self.allowances.entry((caller, spender)).write(amount);
// Emit an event to log this Approval
self.emit(Approval { owner: caller, spender, value: amount });
true // Return success
}
}
self.allowances.entry((caller, spender)).write(amount) लाइन में, spender उस एड्रेस को संदर्भित करता है जिसे caller द्वारा allowance दिया जा रहा है। caller (get_caller_address() से) spender को अपने अकाउंट से एक निश्चित मात्रा में tokens खर्च करने की अनुमति दे रहा है।
इसलिए, caller tokens का ओनर है, और spender वह व्यक्ति है जिसे ओनर द्वारा उनकी ओर से एक निश्चित मात्रा में tokens खर्च करने के लिए अप्रूव (approved) किया गया है। यह एंट्री allowances[(owner, spender)] = amount बनाता है जिसे transfer_from बाद में चेक करेगा और उपयोग करेगा।
approve की टेस्टिंग
आइए टेस्ट करें कि approve function सही ढंग से allowance सेट करता है और इसे वापस क्वेरी किया जा सकता है:
#[test]
fn test_approve() {
let contract_address = deploy_contract("ERC20", OWNER);
let erc20_token = IERC20Dispatcher { contract_address };
let token_decimal = erc20_token.decimals();
let mint_amount: u256 = 10000 * token_decimal.into();
let approval_amount: u256 = 5000 * token_decimal.into();
// Start impersonating the owner
start_cheat_caller_address(contract_address, OWNER);
// Mint tokens to the owner first
erc20_token.mint(OWNER, mint_amount);
// Verify mint succeeded
assert(erc20_token.balance_of(OWNER) == mint_amount, 'Mint failed');
// Owner approves the recipient to spend tokens
erc20_token.approve(TOKEN_RECIPIENT, approval_amount);
// Stop impersonating the owner
stop_cheat_caller_address(contract_address);
// Verify the allowance was set
assert(erc20_token.allowance(OWNER, TOKEN_RECIPIENT) > 0, 'Incorrect allowance');
assert(erc20_token.allowance(OWNER, TOKEN_RECIPIENT) == approval_amount, 'Wrong allowance amount');
}
टेस्ट contract को डिप्लॉय करता है और दो अमाउंट्स को परिभाषित करता है: मिंट करने के लिए 10,000 tokens और अप्रूवल के लिए 5,000 tokens। start_cheat_caller_address का उपयोग करके, टेस्ट कई लगातार कॉल्स के लिए ओनर को impersonate करता है।
सबसे पहले, टेस्ट ओनर को 10,000 tokens मिंट करता है और सत्यापित करता है कि मिंट सफल रहा। फिर, अभी भी ओनर को impersonate करते हुए, यह प्राप्तकर्ता को ओनर के बैलेन्स से 5,000 tokens खर्च करने की अनुमति देने के लिए approve को कॉल करता है। Impersonation रोकने के बाद, टेस्ट दो चीज़ों को सत्यापित करता है: पहला, कि allowance मौजूद है (0 से अधिक), और दूसरा, कि allowance अमाउंट बिल्कुल उसी से मेल खाता है जिसे अप्रूव किया गया था (5,000 tokens)। ये assertions पुष्टि करते हैं कि approve सही ढंग से allowances मैपिंग में खर्च करने की अनुमति (spending permission) को स्टोर करता है।
अपने test_contract.cairo फ़ाइल में टेस्ट जोड़ें, फिर यह सत्यापित करने के लिए कि यह पास होता है, scarb test test_approve रन करें।
डेलीगेटेड ट्रांसफर्स इम्प्लीमेंट करना: transfer_from
अब, आइए transfer_from को implement करें जो पूर्व-अनुमोदित (pre-approved) खर्च अनुमतियों का उपयोग करके एक एड्रेस से दूसरे एड्रेस में tokens मूव करता है।
Function सिग्नेचर शामिल करने के लिए इंटरफ़ेस को अपडेट करें:
#[starknet::interface]
pub trait IERC20<TContractState> {
// ... previous functions ...
fn transfer_from(
ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256,
) -> bool;
}
transfer_from इम्प्लीमेंटेशन:
#[abi(embed_v0)]
impl ERC20Impl of super::IERC20<ContractState> {
//.....previous functions.....//
fn transfer_from(ref self: ContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256) -> bool {
// Get the address of whoever is calling this function (the spender)
let spender = get_caller_address();
// Read current allowance: how much the spender is allowed to spend from sender's account
let spender_allowance = self.allowances.entry((sender, spender)).read();
// Read current balances for both sender and recipient
let sender_balance = self.balances.entry(sender).read();
let recipient_balance = self.balances.entry(recipient).read();
// Check if the transfer amount doesn't exceed the approved allowance
assert(amount <= spender_allowance, 'amount exceeds allowance');
// Check if sender has enough tokens to transfer
assert(amount <= sender_balance, 'amount exceeds balance');
// Update allowance: reduce by the amount being spent
self.allowances.entry((sender, spender)).write(spender_allowance - amount);
// Update balances: subtract from sender, add to recipient
self.balances.entry(sender).write(sender_balance - amount);
self.balances.entry(recipient).write(recipient_balance + amount);
// Emit an event to log this Transfer
self.emit(Transfer { from: sender, to: recipient, amount });
true // Return success
}
}
उपरोक्त कोड में, spender (get_caller_address() से) ट्रांसफर एक्सीक्यूट (executes) करता है, sender token ओनर है, और recipient tokens प्राप्त करता है। Function यह जांचता है कि allowances[(sender, spender)] को रीड करके spender के पास पर्याप्त allowance है, फिर ट्रांसफर किए गए अमाउंट से allowance को कम कर देता है।
उनके खर्च को घटाए बिना, spender के पास असीमित (unlimited) खर्च करने की शक्ति (spending power) होगी।
इस उदाहरण पर विचार करें जो दिखाता है कि approve और transfer_from एक साथ कैसे काम करते हैं:
Alice, Bob को अपने 50 RareTokens खर्च करने देने के लिए approve(Bob, 50) कॉल करती है। फिर Bob 30 tokens को Alice के अकाउंट से Charlie के पास भेजने के लिए transfer_from(Alice, Charlie, 30) का उपयोग कर सकता है, जिससे Bob के पास 20 बचा हुआ allowance रह जाता है।
यह approve-then-withdraw पैटर्न ही वह तरीका है जिससे DeFi प्रोटोकॉल, DEXs और अन्य स्मार्ट contracts यूज़र tokens के साथ इंटरेक्ट करते हैं।
transfer_from की टेस्टिंग
transfer_from टेस्ट के लिए तीन पक्षों की आवश्यकता होती है: tokens के साथ एक ओनर, अप्रूवल के साथ एक spender, और एक प्राप्तकर्ता एड्रेस।
चूंकि spender ओनर के अकाउंट से प्राप्तकर्ता तक tokens ले जाने के लिए अपने अप्रूवल का उपयोग करता है, इसलिए टेस्ट में विभिन्न चरणों (stages) में ओनर और spender दोनों को impersonate करने की आवश्यकता होती है:
#[test]
fn test_transfer_from() {
// Deploy the contract
let contract_address = deploy_contract("ERC20", OWNER);
let erc20_token = IERC20Dispatcher { contract_address };
let token_decimal = erc20_token.decimals();
// Define amounts: 10,000 tokens to mint, 5,000 to approve and transfer
let mint_amount: u256 = 10000 * token_decimal.into();
let transfer_amount: u256 = 5000 * token_decimal.into();
// Start impersonating the owner
start_cheat_caller_address(contract_address, OWNER);
// Mint tokens to the owner
erc20_token.mint(OWNER, mint_amount);
// Verify mint succeeded
assert(erc20_token.balance_of(OWNER) == mint_amount, 'Mint failed');
let spender:ContractAddress = 'SPENDER'.try_into().unwrap();
// Owner approves SPENDER to spend tokens on their behalf
erc20_token.approve(spender, transfer_amount);
// Stop impersonating owner
stop_cheat_caller_address(contract_address);
// Verify the allowance was set correctly
assert(erc20_token.allowance(OWNER, spender) == transfer_amount, 'Approval failed');
// Track balances before transfer
let owner_balance_before = erc20_token.balance_of(OWNER);
let recipient_balance_before = erc20_token.balance_of(TOKEN_RECIPIENT);
let allowance_before = erc20_token.allowance(OWNER, spender);
// Now impersonate the SPENDER to call transfer_from
cheat_caller_address(contract_address, spender, CheatSpan::TargetCalls(1));
erc20_token.transfer_from(OWNER, TOKEN_RECIPIENT, transfer_amount);
// Verify owner's balance decreased
assert(erc20_token.balance_of(OWNER) == owner_balance_before - transfer_amount, 'Owner balance wrong');
// Verify recipient's balance increased
assert(erc20_token.balance_of(TOKEN_RECIPIENT) == recipient_balance_before + transfer_amount, 'Recipient balance wrong');
// Verify allowance decreased
assert(erc20_token.allowance(OWNER, spender) == allowance_before - transfer_amount, 'Allowance not reduced');
}
test_transfer_from पूर्ण approve-and-spend पैटर्न को मान्य (validates) करता है। टेस्ट 10,000 tokens मिंट करने के लिए ओनर को impersonate करने से शुरू होता है और एक spender को उनमें से 5,000 का उपयोग करने के लिए अप्रूव करता है। ओनर impersonation को रोकने के बाद, यह सत्यापित करता है कि अप्रूवल सही ढंग से सेट किया गया था।
इसके बाद, टेस्ट वर्तमान state को कैप्चर करता है: ओनर का बैलेन्स, प्राप्तकर्ता का बैलेन्स, और spender का allowance। फिर यह spender को impersonate करता है और 5,000 tokens को ओनर से प्राप्तकर्ता तक ले जाने के लिए transfer_from को कॉल करता है।
// Now impersonate the SPENDER to call transfer_from
cheat_caller_address(contract_address, spender, CheatSpan::TargetCalls(1));
erc20_token.transfer_from(OWNER, TOKEN_RECIPIENT, transfer_amount);
अंतिम assertions तीन अपडेट्स को सत्यापित करते हैं: ओनर का बैलेन्स 5,000 कम हो गया, प्राप्तकर्ता का बैलेन्स 5,000 बढ़ गया, और spender का allowance 5,000 कम हो गया। ये चेक्स पुष्टि करते हैं कि transfer_from डेलीगेटेड ट्रांसफर्स को सही ढंग से हैंडल करता है और allowances को ठीक से अपडेट करता है।
टेस्ट को test_contract.cairo फ़ाइल में जोड़ें, फिर यह सत्यापित करने के लिए कि यह पास होता है, scarb test test_transfer_from रन करें।
Insufficient (अपर्याप्त) Allowance की टेस्टिंग
आइए टेस्ट करें कि transfer_from अप्रूव किए गए अमाउंट से अधिक खर्च करने के प्रयासों को ठीक से अस्वीकार करता है। यदि कोई spender अप्रूव किए गए tokens से अधिक ट्रांसफर करने का प्रयास करता है, तो ट्रांजैक्शन फेल होना चाहिए।
अप्रूव किए गए 5,000 के बजाय 6,000 tokens ट्रांसफर करने का प्रयास करने के लिए हमारे टेस्ट में transfer_from कॉल को संशोधित करें:
// Attempt to transfer more than approved (6,000 instead of 5,000)
cheat_caller_address(contract_address, spender, CheatSpan::TargetCalls(1));
erc20_token.transfer_from(OWNER, TOKEN_RECIPIENT, 6000 * token_decimal.into());
जब हम scarb test test_transfer_from रन करते हैं, तो टेस्ट इस एरर के साथ फेल होना चाहिए:

यह एरर पुष्टि करता है कि contract अनधिकृत खर्च प्रयास (unauthorized spending attempt) को पकड़ लेता है। Spender को केवल 5,000 tokens के लिए अप्रूव किया गया था, इसलिए 6,000 ट्रांसफर करने का प्रयास transfer_from function में assert(amount <= spender_allowance, 'amount exceeds allowance') चेक को ट्रिगर करता है।
टेस्ट को पास रखने के लिए अमाउंट को वापस transfer_amount (5,000 tokens) में बदल दें।
विकल्प (Alternative): फेलियर केस के लिए एक समर्पित (dedicated) टेस्ट बनाएं
मौजूदा टेस्ट को संशोधित करने के बजाय, हम #[should_panic] एट्रिब्यूट का उपयोग करके एक अलग टेस्ट बना सकते हैं जो फेलियर की अपेक्षा करता है:
#[test]
#[should_panic(expected: ('amount exceeds allowance',))]
fn test_transfer_from_insufficient_allowance() {
// Deploy the contract
let contract_address = deploy_contract("ERC20", OWNER);
let erc20_token = IERC20Dispatcher { contract_address };
let token_decimal = erc20_token.decimals();
// Define amounts: 10,000 tokens to mint, 5,000 to approve
let mint_amount: u256 = 10000 * token_decimal.into();
let approval_amount: u256 = 5000 * token_decimal.into();
// Start impersonating the owner
start_cheat_caller_address(contract_address, OWNER);
// Mint tokens to the owner
erc20_token.mint(OWNER, mint_amount);
let spender: ContractAddress = 'SPENDER'.try_into().unwrap();
// Owner approves SPENDER to spend 5,000 tokens
erc20_token.approve(spender, approval_amount);
// Stop impersonating owner
stop_cheat_caller_address(contract_address);
// Attempt to transfer more than approved (6,000 instead of 5,000)
// This should panic with 'amount exceeds allowance'
cheat_caller_address(contract_address, spender, CheatSpan::TargetCalls(1));
erc20_token.transfer_from(OWNER, TOKEN_RECIPIENT, 6000 * token_decimal.into());
}
फिर से, #[should_panic] एट्रिब्यूट टेस्ट फ्रेमवर्क को बताता है कि यह टेस्ट विशिष्ट एरर मैसेज 'amount exceeds allowance' के साथ फेल होने की अपेक्षा करता है। जब आप इस टेस्ट को अपनी test_contract.cairo फ़ाइल में जोड़ते हैं, फिर scarb test test_transfer_from_insufficient_allowance रन करते हैं तो यह टेस्ट पास हो जाएगा क्योंकि पैनिक (panic) अपेक्षा के अनुरूप हुआ था।
Insufficient (अपर्याप्त) बैलेन्स की टेस्टिंग
हम उस केस को भी टेस्ट कर सकते हैं जहाँ एक spender के पास पर्याप्त allowance है लेकिन ओनर के पास पर्याप्त tokens नहीं हैं। इस टेस्ट को test_contract.cairo में जोड़ें:
#[test]
#[should_panic(expected: ('amount exceeds balance',))]
fn test_transfer_from_insufficient_balance() {
// Deploy the contract
let contract_address = deploy_contract("ERC20", OWNER);
let erc20_token = IERC20Dispatcher { contract_address };
let token_decimal = erc20_token.decimals();
// Define amounts: only 1,000 tokens minted, but 2,000 approved and attempted
let mint_amount: u256 = 1000 * token_decimal.into();
let approval_amount: u256 = 2000 * token_decimal.into();
let transfer_amount: u256 = 2000 * token_decimal.into();
// Start impersonating the owner
start_cheat_caller_address(contract_address, OWNER);
// Mint only 1,000 tokens to the owner
erc20_token.mint(OWNER, mint_amount);
let spender: ContractAddress = 'SPENDER'.try_into().unwrap();
// Owner approves SPENDER to spend 2,000 tokens (more than balance)
erc20_token.approve(spender, approval_amount);
// Stop impersonating owner
stop_cheat_caller_address(contract_address);
// Spender has sufficient allowance but owner doesn't have enough balance
// This should panic with 'amount exceeds balance'
cheat_caller_address(contract_address, spender, CheatSpan::TargetCalls(1));
erc20_token.transfer_from(OWNER, TOKEN_RECIPIENT, transfer_amount);
}
ऊपर दिया गया test_transfer_from_insufficient_balance टेस्ट सत्यापित करता है कि पर्याप्त allowance होने पर भी, यदि token ओनर के पास पर्याप्त बैलेन्स नहीं है तो ट्रांसफर फेल हो जाता है। Spender को 2,000 tokens के लिए अप्रूव किया गया है, लेकिन ओनर के पास केवल 1,000 हैं, जो assert(amount <= sender_balance, 'amount exceeds balance') चेक को ट्रिगर करता है।
यह सत्यापित करने के लिए कि यह पास होता है, scarb test test_transfer_from_insufficient_balance रन करें।
यहाँ पूर्ण ERC-20 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; // For testing purposes
}
#[starknet::contract]
pub mod ERC20 {
use starknet::{ContractAddress, get_caller_address};
use starknet::storage::{
Map, StoragePointerWriteAccess, StoragePointerReadAccess, StoragePathEntry,
};
#[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, owner: ContractAddress) {
self.token_name.write("Rare Token");
self.symbol.write("RST");
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(), 'Call 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
}
}
}
निम्नलिखित पूर्ण टेस्ट है:
use erc20::{IERC20Dispatcher, IERC20DispatcherTrait};
use snforge_std::{
CheatSpan, ContractClassTrait, DeclareResultTrait, cheat_caller_address, declare,
start_cheat_caller_address, stop_cheat_caller_address,
};
use starknet::ContractAddress;
fn deploy_contract(name: ByteArray, owner: ContractAddress) -> ContractAddress {
let contract = declare(name).unwrap().contract_class();
let constructor_args = array![owner.into()];
let (contract_address, _) = contract.deploy(@constructor_args).unwrap();
contract_address
}
const OWNER: ContractAddress = 'OWNER'.try_into().unwrap();
const TOKEN_RECIPIENT: ContractAddress = 'RECIPIENT'.try_into().unwrap();
#[test]
fn test_token_constructor() {
let contract_address = deploy_contract("ERC20", OWNER);
let erc20_token = IERC20Dispatcher { contract_address };
let token_name = erc20_token.name();
let token_symbol = erc20_token.symbol();
let token_decimal = erc20_token.decimals();
assert(token_name == "Rare Token", 'Wrong token name');
assert(token_symbol == "RST", 'Wrong token symbol');
assert(token_decimal == 18, 'Wrong token decimal');
}
#[test]
fn test_total_supply() {
let contract_address = deploy_contract("ERC20", OWNER);
let erc20_token = IERC20Dispatcher { contract_address };
let token_decimal = erc20_token.decimals();
let mint_amount = 1000 * token_decimal.into();
// cheat caller address to be the owner
cheat_caller_address(contract_address, OWNER, CheatSpan::TargetCalls(1));
erc20_token.mint(TOKEN_RECIPIENT, mint_amount);
let supply = erc20_token.total_supply();
assert(supply == mint_amount, 'Incorrect Supply');
}
#[test]
fn test_transfer() {
let contract_address = deploy_contract("ERC20", OWNER);
let erc20_token = IERC20Dispatcher { contract_address };
let token_decimal = erc20_token.decimals();
let amount_to_mint: u256 = 10000 * token_decimal.into();
let amount_to_transfer: u256 = 5000 * token_decimal.into();
// Start impersonating the owner for multiple calls
start_cheat_caller_address(contract_address, OWNER);
erc20_token.mint(OWNER, amount_to_mint);
assert(erc20_token.balance_of(OWNER) == amount_to_mint, 'Incorrect minted amount');
let receiver_previous_balance = erc20_token.balance_of(TOKEN_RECIPIENT);
erc20_token.transfer(TOKEN_RECIPIENT, amount_to_transfer);
stop_cheat_caller_address(contract_address);
assert(erc20_token.balance_of(OWNER) < amount_to_mint, 'Sender balance not reduced');
assert(
erc20_token.balance_of(OWNER) == amount_to_mint - amount_to_transfer,
'Wrong sender balance',
);
assert(
erc20_token.balance_of(TOKEN_RECIPIENT) > receiver_previous_balance,
'Recipient balance unchanged',
);
assert(
erc20_token.balance_of(TOKEN_RECIPIENT) == amount_to_transfer, 'Wrong recipient amount',
);
}
#[test]
#[should_panic(expected: ('Insufficient amount',))]
fn test_transfer_insufficient_balance() {
// Deploy the contract
let contract_address = deploy_contract("ERC20", OWNER);
let erc20_token = IERC20Dispatcher { contract_address };
let token_decimal = erc20_token.decimals();
// Define amounts: only 5,000 tokens minted, but attempting to transfer 10,000
let mint_amount: u256 = 5000 * token_decimal.into();
let transfer_amount: u256 = 10000 * token_decimal.into();
// Start impersonating the owner
start_cheat_caller_address(contract_address, OWNER);
// Mint only 5,000 tokens to the owner
erc20_token.mint(OWNER, mint_amount);
// Verify the mint was successful
assert(erc20_token.balance_of(OWNER) == mint_amount, 'Mint failed');
// Attempt to transfer more than balance (10,000 tokens when only 5,000 exist)
// This should panic with 'Insufficient amount'
erc20_token.transfer(TOKEN_RECIPIENT, transfer_amount);
// Stop impersonating the owner
stop_cheat_caller_address(contract_address);
}
#[test]
fn test_approve() {
let contract_address = deploy_contract("ERC20", OWNER);
let erc20_token = IERC20Dispatcher { contract_address };
let token_decimal = erc20_token.decimals();
let mint_amount: u256 = 10000 * token_decimal.into();
let approval_amount: u256 = 5000 * token_decimal.into();
// Start impersonating the owner
start_cheat_caller_address(contract_address, OWNER);
// Mint tokens to the owner first
erc20_token.mint(OWNER, mint_amount);
// Verify mint succeeded
assert(erc20_token.balance_of(OWNER) == mint_amount, 'Mint failed');
// Owner approves the recipient to spend tokens
erc20_token.approve(TOKEN_RECIPIENT, approval_amount);
// Stop impersonating the owner
stop_cheat_caller_address(contract_address);
// Verify the allowance was set
assert(erc20_token.allowance(OWNER, TOKEN_RECIPIENT) > 0, 'Incorrect allowance');
assert(erc20_token.allowance(OWNER, TOKEN_RECIPIENT) == approval_amount, 'Wrong allowance amount');
}
#[test]
fn test_transfer_from() {
let contract_address = deploy_contract("ERC20", OWNER);
let erc20_token = IERC20Dispatcher { contract_address };
let token_decimal = erc20_token.decimals();
let mint_amount: u256 = 10000 * token_decimal.into();
let transfer_amount: u256 = 5000 * token_decimal.into();
start_cheat_caller_address(contract_address, OWNER);
erc20_token.mint(OWNER, mint_amount);
assert(erc20_token.balance_of(OWNER) == mint_amount, 'Mint failed');
let spender: ContractAddress = 'SPENDER'.try_into().unwrap();
erc20_token.approve(spender, transfer_amount);
stop_cheat_caller_address(contract_address);
assert(erc20_token.allowance(OWNER, spender) == transfer_amount, 'Approval failed');
let owner_balance_before = erc20_token.balance_of(OWNER);
let recipient_balance_before = erc20_token.balance_of(TOKEN_RECIPIENT);
let allowance_before = erc20_token.allowance(OWNER, spender);
cheat_caller_address(contract_address, spender, CheatSpan::TargetCalls(1));
erc20_token.transfer_from(OWNER, TOKEN_RECIPIENT, 5000 * token_decimal.into());
assert(
erc20_token.balance_of(OWNER) == owner_balance_before - transfer_amount,
'Owner balance wrong',
);
assert(
erc20_token.balance_of(TOKEN_RECIPIENT) == recipient_balance_before + transfer_amount,
'Recipient balance wrong',
);
assert(
erc20_token.allowance(OWNER, spender) == allowance_before - transfer_amount,
'Allowance not reduced',
);
}
#[test]
#[should_panic(expected: ('amount exceeds allowance',))]
fn test_transfer_from_insufficient_allowance() {
// Deploy the contract
let contract_address = deploy_contract("ERC20", OWNER);
let erc20_token = IERC20Dispatcher { contract_address };
let token_decimal = erc20_token.decimals();
// Define amounts: 10,000 tokens to mint, 5,000 to approve
let mint_amount: u256 = 10000 * token_decimal.into();
let approval_amount: u256 = 5000 * token_decimal.into();
// Start impersonating the owner
start_cheat_caller_address(contract_address, OWNER);
// Mint tokens to the owner
erc20_token.mint(OWNER, mint_amount);
let spender: ContractAddress = 'SPENDER'.try_into().unwrap();
// Owner approves SPENDER to spend 5,000 tokens
erc20_token.approve(spender, approval_amount);
// Stop impersonating owner
stop_cheat_caller_address(contract_address);
// Attempt to transfer more than approved (6,000 instead of 5,000)
// This should panic with 'amount exceeds allowance'
cheat_caller_address(contract_address, spender, CheatSpan::TargetCalls(1));
erc20_token.transfer_from(OWNER, TOKEN_RECIPIENT, 6000 * token_decimal.into());
}
#[test]
#[should_panic(expected: ('amount exceeds balance',))]
fn test_transfer_from_insufficient_balance() {
// Deploy the contract
let contract_address = deploy_contract("ERC20", OWNER);
let erc20_token = IERC20Dispatcher { contract_address };
let token_decimal = erc20_token.decimals();
// Define amounts: only 1,000 tokens minted, but 2,000 approved and attempted
let mint_amount: u256 = 1000 * token_decimal.into();
let approval_amount: u256 = 2000 * token_decimal.into();
let transfer_amount: u256 = 2000 * token_decimal.into();
// Start impersonating the owner
start_cheat_caller_address(contract_address, OWNER);
// Mint only 1,000 tokens to the owner
erc20_token.mint(OWNER, mint_amount);
let spender: ContractAddress = 'SPENDER'.try_into().unwrap();
// Owner approves SPENDER to spend 2,000 tokens (more than balance)
erc20_token.approve(spender, approval_amount);
// Stop impersonating owner
stop_cheat_caller_address(contract_address);
// Spender has sufficient allowance but owner doesn't have enough balance
// This should panic with 'amount exceeds balance'
cheat_caller_address(contract_address, spender, CheatSpan::TargetCalls(1));
erc20_token.transfer_from(OWNER, TOKEN_RECIPIENT, transfer_amount);
}
सभी टेस्ट रन करने के लिए, अपने टर्मिनल में scarb test कमांड का उपयोग करें। यह सभी टेस्ट functions को एक्सीक्यूट करेगा और परिणाम प्रदर्शित करेगा। आपको आउटपुट देखना चाहिए जो यह दर्शाता है कि प्रत्येक टेस्ट पास हो गया है:

अभ्यास (Exercise): mint function की टेस्टिंग
आपने जो सीखा है उसका अभ्यास करने के लिए mint function के लिए एक टेस्ट लिखें। टेस्ट को यह सत्यापित करना चाहिए कि:
- केवल ओनर ही tokens मिंट कर सकता है
- प्राप्तकर्ता का बैलेन्स मिंट किए गए अमाउंट से बढ़ता है
- Total supply मिंट किए गए अमाउंट से बढ़ती है
एक बार हो जाने के बाद, यह काम करता है या नहीं यह सत्यापित करने के लिए scarb test test_mint रन करें।
निष्कर्ष (Conclusion)
इस ट्यूटोरियल में Starknet पर ERC-20 token contract बनाने और टेस्ट करने को कवर किया गया। यहाँ से, contract को पॉज़िंग (pausing), एक्सेस कंट्रोल्स आदि जैसी सुविधाओं (features) के साथ विस्तारित (extended) किया जा सकता है।
विकल्प के रूप में, सब कुछ शुरुआत (scratch) से बनाने के बजाय Cairo के लिए OpenZeppelin के प्री-बिल्ट (pre-built) कंपोनेंट्स का उपयोग किया जा सकता है। OpenZeppelin के ERC20, Ownable, और Pausable कंपोनेंट्स को एक contract में कैसे एकीकृत (integrate) किया जाए, यह जानने के लिए “Component 2” चैप्टर देखें।
यह लेख Starknet पर Cairo प्रोग्रामिंग (Cairo Programming on Starknet) पर एक ट्यूटोरियल सीरीज़ का हिस्सा है