Foundry में “cheatcode” एक ऐसा मैकेनिज्म (mechanism) है जो कॉन्ट्रैक्ट टेस्ट्स (contract tests) को एन्वायरनमेंट वेरिएबल्स (environment variables) जैसे caller address, वर्तमान timestamp, आदि को नियंत्रित करने की अनुमति देता है।
इस आर्टिकल में, आप सीखेंगे कि Starknet Foundry के सबसे अधिक उपयोग किए जाने वाले cheatcodes का उपयोग करके Cairo स्मार्ट कॉन्ट्रैक्ट्स को कैसे टेस्ट किया जाए।
caller_address Cheatcodes
Starknet स्मार्ट कॉन्ट्रैक्ट्स में, get_caller_address() उस वर्तमान अकाउंट का एड्रेस लौटाता है जो कॉन्ट्रैक्ट में किसी फंक्शन के साथ इंटरैक्ट कर रहा है, ठीक Ethereum में msg.sender की तरह। कॉन्ट्रैक्ट्स एक्सेस कंट्रोल, विशेषाधिकारों (privileges), या कस्टम उपयोग के लिए इस पर निर्भर करते हैं। उदाहरण के लिए, निम्नलिखित कोड यह जाँचता है कि कॉलर कॉन्ट्रैक्ट का ओनर (owner) है या नहीं, और उसके बाद ही निष्पादन (execution) को आगे बढ़ने की अनुमति देता है:
// Get who is calling this function
let caller = get_caller_address();
assert(caller == self.owner.read(), 'Only owner');
टेस्टिंग के दौरान, जब कोई फंक्शन ऊपर दिए गए कोड की तरह कॉलर एड्रेस की जाँच करता है, तो हमें यह नियंत्रित करने की आवश्यकता होती है कि get_caller_address() क्या लौटाता है ताकि यह टेस्ट किया जा सके कि एक्सेस कंट्रोल सही तरीके से काम कर रहा है, वह भी बिना वास्तविक अकाउंट्स (वॉलेट एड्रेस) का उपयोग किए। यहीं पर caller_address cheatcodes काम आते हैं।
Starknet Foundry के caller_address cheatcodes हमें अपनी आवश्यकता के किसी भी एड्रेस से कॉल्स को सिमुलेट (simulate) करके ऐसा करने की अनुमति देते हैं। ये बिल्कुल Solidity Foundry में prank फंक्शन्स की तरह काम करते हैं। उपलब्ध फंक्शन्स हैं:
Starknet Foundry caller_address cheatcodes |
यह क्या करता है | Solidity Foundry समकक्ष (equivalent) |
|---|---|---|
cheat_caller_address(target, caller_address, span) |
एक टारगेट कॉन्ट्रैक्ट के लिए कॉलर का प्रतिरूपण (impersonate) करता है, जो CheatSpan द्वारा सीमित होता है |
कोई सीधा समकक्ष नहीं (Solidity का vm.prank(caller_address) अगली कॉल को विश्व स्तर पर प्रभावित करता है, न कि टारगेट-विशिष्ट) |
start_cheat_caller_address(target, caller_address) |
एक टारगेट कॉन्ट्रैक्ट के लिए कॉलर का प्रतिरूपण शुरू करता है | कोई सीधा समकक्ष नहीं (Solidity में टारगेट-विशिष्ट pranking नहीं है) |
start_cheat_caller_address_global(caller_address) |
सभी कॉन्ट्रैक्ट्स में विश्व स्तर (globally) पर कॉलर का प्रतिरूपण शुरू करता है, जिसमें टारगेट कॉन्ट्रैक्ट और उसके द्वारा इनवोक (invoke) किए गए कोई भी कॉन्ट्रैक्ट शामिल हैं | vm.startPrank(caller_address) |
stop_cheat_caller_address(target) |
एक टारगेट कॉन्ट्रैक्ट के लिए कॉलर का प्रतिरूपण रोकता है | कोई सीधा समकक्ष नहीं |
stop_cheat_caller_address_global() |
ग्लोबल कॉलर प्रतिरूपण को रोकता है | vm.stopPrank() |
ये caller_address cheatcodes व्यवहार में कैसे काम करते हैं, यह प्रदर्शित करने के लिए, एक नया Scarb प्रोजेक्ट इनिशियलाइज़ करें (scarb new cheatcodes) और टेस्ट रनर के रूप में Starknet Foundry को चुनें।
src/lib.cairo फाइल में, Scarb द्वारा जनरेट किया गया एक डिफ़ॉल्ट बैलेंस मैनेजमेंट कॉन्ट्रैक्ट है जो हमें कॉन्ट्रैक्ट के स्टोरेज से बैलेंस बढ़ाने और प्राप्त करने की अनुमति देता है।
increase_balance() फंक्शन में ओनर-आधारित एक्सेस कंट्रोल शामिल करने के लिए इस बॉयलरप्लेट (boilerplate) कॉन्ट्रैक्ट को अपडेट करें। अपडेट किया गया कॉन्ट्रैक्ट एक owner एड्रेस और एक balance स्टोर करेगा जिसे केवल ओनर द्वारा ही मॉडिफाई किया जा सकता है। increase_balance() फंक्शन यह जाँचने के लिए get_caller_address() का उपयोग करेगा कि इसे कौन कॉल कर रहा है और केवल ओनर को ही आगे बढ़ने की अनुमति देगा। अपडेट किए गए कॉन्ट्रैक्ट में ओनर के एड्रेस की जाँच करने के लिए get_owner() फंक्शन भी शामिल होगा, जो टेस्ट लिखते समय उपयोगी होगा।
नीचे दिए गए अपडेटेड कॉन्ट्रैक्ट को कॉपी करें और इसे src/lib.cairo में पेस्ट करें:
use starknet::ContractAddress;
/// Interface representing `HelloContract`.
/// This interface allows modification and retrieval of the contract balance.
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
/// Increase contract balance.
fn increase_balance(ref self: TContractState, amount: u256);
/// Retrieve contract balance.
fn get_balance(self: @TContractState) -> u256;
fn get_owner(self: @TContractState) -> ContractAddress;
}
/// Simple contract for managing balance.
#[starknet::contract]
mod HelloStarknet {
use starknet::{ContractAddress, get_caller_address};
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
owner: ContractAddress,
balance: u256,
}
#[constructor]
fn constructor(ref self: ContractState, owner: ContractAddress) {
// Set the owner when the contract is deployed
self.owner.write(owner);
}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn increase_balance(ref self: ContractState, amount: u256) {
let caller = get_caller_address();
//NEWLY ADDED
//checks only the owner can increase balance
assert(caller == self.owner.read(), 'Only owner');
assert(amount != 0, 'Amount cannot be 0');
// Update the balance by adding the new amount
self.balance.write(self.balance.read() + amount);
}
fn get_balance(self: @ContractState) -> u256 {
self.balance.read()
}
fn get_owner(self: @ContractState) -> ContractAddress {
self.owner.read()
}
}
}
यह ओनर-आधारित एक्सेस कंट्रोल पैटर्न DeFi प्रोटोकॉल में आम है जहाँ विशिष्ट एड्रेस को फंड निकालने जैसे कुछ फंक्शन्स करने की अनुमति होती है।
चूँकि increase_balance() कॉन्ट्रैक्ट के ओनर तक ही सीमित है, इसलिए हमें ओनर एड्रेस से कॉल्स को सिमुलेट करने के लिए caller_address cheatcode की आवश्यकता है।
cheat_caller_address का उपयोग करके किसी एड्रेस का प्रतिरूपण (Impersonating) करना
cheat_caller_address cheatcode हमें कॉन्ट्रैक्ट फंक्शन्स को कॉल करते समय किसी भी एड्रेस का प्रतिरूपण (impersonate) करने की अनुमति देता है। इसका मतलब है कि हम टेस्ट कॉल्स को ऐसा दिखा सकते हैं जैसे वे विशिष्ट एड्रेस से आ रहे हैं, जैसे कि कॉन्ट्रैक्ट ओनर, जिससे हमें एक्सेस कंट्रोल लॉजिक का टेस्ट करने की अनुमति मिलती है।
cheat_caller_address cheatcode में निम्नलिखित फंक्शन सिग्नेचर होता है:
fn cheat_caller_address(target: ContractAddress, caller_address: ContractAddress, span: CheatSpan)
यह तीन पैरामीटर्स लेता है:
target: वह विशिष्ट कॉन्ट्रैक्ट जो इम्पर्सनेटेड कॉलर को देखेगाcaller_address: प्रतिरूपण (impersonate) करने वाला एड्रेसspan: एकCheatSpanenum जो यह परिभाषित करता है कि चीट कितने समय तक चलना चाहिए। इसके दो वेरिएंट्स (variants) हैं:CheatSpan::Indefinite: चीट तब तक एक्टिव रहता है जब तक कि इसे मैन्युअल रूप से रोका न जाएCheatSpan::TargetCalls(n):nफंक्शन कॉल्स के लिए चीट लागू करें
अपने टेस्ट्स में cheat_caller_address का उपयोग करने के लिए, अपने प्रोजेक्ट डायरेक्टरी में tests/test_contract.cairo पर नेविगेट करें। बॉयलरप्लेट टेस्ट्स को साफ़ करें और इस प्रकार cheat_caller_address और CheatSpan को शामिल करने के लिए इम्पोर्ट्स (imports) को अपडेट करें:
use starknet::ContractAddress;
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait, cheat_caller_address, CheatSpan};
यह देखने के लिए कि cheat_caller_address व्यवहार में कैसे काम करता है, हम दो टेस्ट बनाएँगे: एक जो cheat_caller_address cheatcode के बिना विफलता (failure) के मामले को प्रदर्शित करता है, और दूसरा जो दिखाता है कि cheatcode का सही ढंग से उपयोग कैसे किया जाए।
चूँकि अपडेटेड HelloContract कंस्ट्रक्टर अब एक ओनर एड्रेस की अपेक्षा करता है, इसलिए हमें अपने टेस्ट्स में कॉन्ट्रैक्ट डिप्लॉय करते समय एक एड्रेस प्रदान करने की आवश्यकता है। हम एक deploy_contract हेल्पर फंक्शन बनाएँगे जो ओनर एड्रेस को पैरामीटर के रूप में लेता है और इसे कंस्ट्रक्टर को पास करता है, साथ ही एक OWNER कॉन्स्टेंट जो टेस्टिंग के लिए एक रीयूजेबल मॉक एड्रेस प्रदान करता है।
फिर हम टेस्ट में डिप्लॉय किए गए कॉन्ट्रैक्ट के साथ इंटरैक्ट करने के लिए आवश्यक डिस्पैचर्स (dispatchers) को इम्पोर्ट करेंगे। कुल मिलाकर, हमारे पास निम्नलिखित है:
use starknet::ContractAddress;
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait, cheat_caller_address, CheatSpan};
//NEWLY ADDED BELOW//
use cheatcodes::IHelloStarknetDispatcher;
use cheatcodes::IHelloStarknetDispatcherTrait;
const OWNER: ContractAddress = 'OWNER'.try_into().unwrap();
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 addres
contract_address
}
IHelloStarknetDispatcher और IHelloStarknetDispatcherTrait डिस्पैचर्स हमें टेस्ट्स से कॉन्ट्रैक्ट फंक्शन्स को कॉल करने की अनुमति देते हैं।
OWNER एक कॉन्स्टेंट है जो स्ट्रिंग लिटरल 'OWNER' को ContractAddress टाइप में बदलता है जो हमारे टेस्ट्स में रीयूजेबल (reusable) है।
deploy_contract फंक्शन कॉन्ट्रैक्ट क्लास को डिक्लेयर (declare) करता है, constructor_args के माध्यम से ओनर एड्रेस को कंस्ट्रक्टर में पास करता है, और डिप्लॉय किए गए कॉन्ट्रैक्ट का एड्रेस लौटाता है ताकि हम उसके साथ इंटरैक्ट कर सकें।
टेस्ट 1: विफलता (failure) के मामले को टेस्ट करना
यह पहला टेस्ट दिखाता है कि क्या होता है जब हम cheat_caller_address cheatcode का उपयोग किए बिना increase_balance() को कॉल करने का प्रयास करते हैं। हम ओनर के रूप में OWNER के साथ कॉन्ट्रैक्ट डिप्लॉय करेंगे, फिर बैलेंस बढ़ाने का प्रयास करेंगे। यह विफल (fail) हो जाएगा क्योंकि टेस्ट एन्वायरनमेंट का एड्रेस कॉन्ट्रैक्ट में स्टोर किए गए ओनर एड्रेस से अलग है।
इस test_environment_address_owner_check टेस्ट कोड को अपनी टेस्ट फाइल में जोड़ें:
use starknet::ContractAddress;
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait, cheat_caller_address, CheatSpan};
use cheatcodes::IHelloStarknetDispatcher;
use cheatcodes::IHelloStarknetDispatcherTrait;
const OWNER: ContractAddress = 'OWNER'.try_into().unwrap();
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
}
//NEWLY ADDED//
#[test]
fn test_environment_address_owner_check() {
// Deploy the HelloStarknet contract with OWNER as the owner
let contract_address = deploy_contract("HelloStarknet", OWNER);
let dispatcher = IHelloStarknetDispatcher { contract_address };
// Verify the owner was set correctly during deployment
assert(dispatcher.get_owner() == OWNER, 'Owner not set correctly');
// This call should fail because the test environment address != OWNER
// The get_caller_address() inside increase_balance will return the environment address,
// which is not the OWNER, so the owner check should fail
dispatcher.increase_balance(42);
}
scarb test test_environment_address_owner_check रन करें। आपको यह विफलता दिखाई देनी चाहिए:

विफलता इसलिए होती है क्योंकि जब dispatcher.increase_balance(42) निष्पादित (execute) होता है, तो increase_balance() के अंदर get_caller_address() फंक्शन टेस्ट एन्वायरनमेंट का एड्रेस लौटाता है, न कि OWNER। चूँकि कॉन्ट्रैक्ट का ओनर OWNER पर सेट है, इसलिए assert(caller == self.owner.read(), 'Only owner') स्टेटमेंट विफल हो जाता है।
टेस्ट 2: cheat_caller_address cheatcode का उपयोग करना
अब देखते हैं कि cheat_caller_address इस एक्सेस कंट्रोल टेस्टिंग समस्या को कैसे हल करता है। अपनी टेस्ट फाइल में इस प्रकार test_cheat_caller_address टेस्ट जोड़ें:
use starknet::ContractAddress;
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait, cheat_caller_address, CheatSpan};
use cheatcodes::IHelloStarknetDispatcher;
use cheatcodes::IHelloStarknetDispatcherTrait;
const OWNER: ContractAddress = 'OWNER'.try_into().unwrap();
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
}
//NEWLY ADDED//
#[test]
fn test_cheat_caller_address() {
// Deploy the HelloStarknet contract with OWNER as the owner
let contract_address = deploy_contract("HelloStarknet", OWNER);
let dispatcher = IHelloStarknetDispatcher { contract_address };
// Verify the owner was set correctly during deployment
assert(dispatcher.get_owner() == OWNER, 'Owner not set correctly');
// cheat caller address to be the owner
cheat_caller_address(contract_address, OWNER, CheatSpan::TargetCalls(1));
dispatcher.increase_balance(42); // This function call uses the cheat
assert(dispatcher.get_balance() == 42, 'Balance not 42');
// The cheat has expired after 1 call (CheatSpan::TargetCalls(1))
// Any subsequent calls would fail the owner check
}
cheat_caller_address(contract_address, OWNER, CheatSpan::TargetCalls(1)) कॉल get_caller_address() के रिटर्न को ओवरराइड करती है। यह कॉन्ट्रैक्ट को यह विश्वास दिलाती है कि अगली फंक्शन कॉल टेस्ट एन्वायरनमेंट के बजाय OWNER से आती है।
जब dispatcher.increase_balance(42) निष्पादित होता है, तो get_caller_address() OWNER लौटाता है, जिससे ओनर चेक पास हो जाता है।
scarb test test_cheat_caller_address रन करें और आपको टेस्ट पास होता हुआ दिखाई देगा:

CheatSpan::TargetCalls(1) पैरामीटर snforge को केवल अगले फंक्शन कॉल (increase_balance(42)) के लिए कॉलर चीट लागू करने के लिए कहता है। उसके बाद, कॉलर एड्रेस सामान्य स्थिति में आ जाता है।
यदि हम दूसरे चीट के बिना या TargetCalls बढ़ाए बिना फिर से increase_balance() को कॉल करने का प्रयास करते हैं, तो यह विफल हो जाएगा क्योंकि कॉलर अब ओनर नहीं होगा।
start_cheat_caller_address और stop_cheat_caller_address के साथ लगातार (Persistent) कॉलर प्रतिरूपण
cheat_caller_address के विपरीत जिसके लिए अवधि को नियंत्रित करने के लिए CheatSpan पैरामीटर की आवश्यकता होती है, start_cheat_caller_address सभी बाद की कॉल्स के लिए अनिश्चित काल तक कॉलर एड्रेस सेट करता है जब तक कि मैन्युअल रूप से stop_cheat_caller_address के साथ रोका न जाए।
start_cheat_caller_address के लिए दो तर्कों (arguments) की आवश्यकता होती है: एक target (विशिष्ट कॉन्ट्रैक्ट जिसे इम्पर्सनेटेड कॉलर को देखना चाहिए) और एक caller_address (प्रतिरूपण करने वाला एड्रेस), जैसा कि नीचे दिखाया गया है:
fn start_cheat_caller_address(target: ContractAddress, caller_address: ContractAddress
जबकि stop_cheat_caller_address उस विशिष्ट कॉन्ट्रैक्ट के लिए प्रतिरूपण को रोकने के लिए केवल target लेता है:
fn stop_cheat_caller_address(target: ContractAddress)
इन cheatcodes का उपयोग करने के लिए, मौजूदा इम्पोर्ट्स के साथ start_cheat_caller_address और stop_cheat_caller_address cheatcodes को शामिल करने के लिए snforge लाइब्रेरी इम्पोर्ट्स को अपडेट करें:
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait, start_cheat_caller_address, stop_cheat_caller_address, cheat_caller_address, CheatSpan};
निम्नलिखित टेस्ट यह दिखाता है कि कई फंक्शन कॉल्स के पार लगातार कॉलर प्रतिरूपण के लिए start_cheat_caller_address का उपयोग कैसे किया जाए:
#[test]
fn test_persistent_caller_cheat() {
let contract_address = deploy_contract("HelloStarknet", OWNER);
let dispatcher = IHelloStarknetDispatcher { contract_address };
// Start impersonating OWNER for all calls to this specific contract until we explicitly stop it
start_cheat_caller_address(contract_address, OWNER);
// multiple calls will all use OWNER as caller
dispatcher.increase_balance(10);
dispatcher.increase_balance(2);
dispatcher.increase_balance(45);
assert(dispatcher.get_balance() == 57, 'Balance should be 57');
// Stop the caller impersonation
stop_cheat_caller_address(contract_address);
}
test_persistent_caller_cheat() में, हम स्टोर किए गए ओनर के रूप में OWNER के साथ कॉन्ट्रैक्ट डिप्लॉय करते हैं, फिर उस कॉन्ट्रैक्ट के सभी बाद के कॉल्स के लिए ओनर का प्रतिरूपण शुरू करने के लिए start_cheat_caller_address(contract_address, OWNER) को कॉल करते हैं।
ऊपर दिए गए टेस्ट को tests/test_contract.cairo में कॉपी करें और इसे scarb test test_persistent_caller_cheat का उपयोग करके रन करें।
increase_balance के तीनों कॉल्स सफल होंगे क्योंकि चीट सभी फंक्शन कॉल्स के दौरान एक्टिव रहता है। हर बार जब फंक्शन get_caller_address() की जाँच करता है, तो यह टेस्ट एन्वायरनमेंट के एड्रेस के बजाय OWNER लौटाता है। चीट तब तक एक्टिव रहता है जब तक कि हम स्पष्ट रूप से stop_cheat_caller_address(contract_address) को कॉल नहीं करते।

महत्वपूर्ण नोट:
start_cheat_caller_addressटारगेट-विशिष्ट (target-specific) है, जिसका अर्थ है कि यह केवल निर्दिष्ट कॉन्ट्रैक्ट एड्रेस के कॉल्स को प्रभावित करता है। यदि आपने किसी भिन्न कॉन्ट्रैक्ट (contractB) पर किसी फंक्शन को कॉल किया, जबकि चीट contractA के लिए एक्टिव है, तो contractB को इम्पर्सनेटेड एड्रेस के बजाय सामान्य टेस्ट एन्वायरनमेंट एड्रेस दिखाई देगा। चीट केवलtargetपैरामीटर में निर्दिष्ट कॉन्ट्रैक्ट पर ही लागू होता है।
जब आपको निर्दिष्ट कॉन्ट्रैक्ट में एक ही एड्रेस के रूप में कई लगातार कॉल करने की आवश्यकता हो, तो start_cheat_caller_address का उपयोग करें।
start_cheat_caller_address_global और stop_cheat_caller_address_global के साथ ग्लोबल कॉलर प्रतिरूपण
कई कॉन्ट्रैक्ट्स के बीच इंटरैक्शन को टेस्ट करने के लिए, start_cheat_caller_address_global सभी कॉन्ट्रैक्ट कॉल्स के लिए एक यूनिवर्सल कॉलर एड्रेस सेट करता है जब तक कि स्पष्ट रूप से stop_cheat_caller_address_global का उपयोग करके रोका न जाए। यह Foundry के Solidity वाले startPrank/stopPrank की तरह ही काम करता है।
इन ग्लोबल कॉलर cheatcodes का उपयोग करने के लिए, उन्हें मौजूदा snforge लाइब्रेरी इम्पोर्ट्स में जोड़ें:
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait, start_cheat_caller_address, stop_cheat_caller_address, cheat_caller_address, CheatSpan, start_cheat_caller_address_global, stop_cheat_caller_address_global};
नीचे दिया गया टेस्ट एक ही स्पूफ (spoofed) किए गए कॉलर का उपयोग करके दो कॉन्ट्रैक्ट्स के साथ इंटरैक्ट करने के लिए इस start_cheat_caller_address_global cheatcode का उपयोग करता है। हम HelloStarknet कॉन्ट्रैक्ट के दो अलग-अलग इंस्टेंसेस (instances) डिप्लॉय करेंगे और वैश्विक स्तर (globally) पर ओनर का प्रतिरूपण करते हुए दोनों को कॉल करेंगे:
#[test]
fn test_global_caller_cheat() {
// Deploy two separate instances of the HelloStarknet contract
// Both contracts have OWNER as their owner
let contract1 = deploy_contract("HelloStarknet", OWNER);
let contract2 = deploy_contract("HelloStarknet", OWNER);
// Create dispatchers to interact with each contract
let dispatcher1 = IHelloStarknetDispatcher { contract_address: contract1 };
let dispatcher2 = IHelloStarknetDispatcher { contract_address: contract2 };
// Start global caller impersonation - affects ALL contracts
// Every contract call will now appear to come from OWNER
start_cheat_caller_address_global(OWNER);
// Both calls succeed because both contracts see OWNER as the caller
dispatcher1.increase_balance(100);
dispatcher2.increase_balance(200);
// Confirm each contract has the correct balance
assert(dispatcher1.get_balance() == 100, 'Contract1 balance wrong');
assert(dispatcher2.get_balance() == 200, 'Contract2 balance wrong');
// Stop the global cheat
stop_cheat_caller_address_global();
}
ऊपर दिए गए टेस्ट कोड को अपनी test_contract.cairo फाइल में जोड़ें और इसे scarb test test_global_caller_cheat के साथ रन करें।
टेस्ट पास हो जाएगा क्योंकि test_global_caller_cheat() टेस्ट में start_cheat_caller_address_global एक साथ सभी कॉन्ट्रैक्ट्स को प्रभावित करता है। दोनों कॉन्ट्रैक्ट्स (contract1 और contract2) कॉलर को OWNER के रूप में देखते हैं, इसलिए प्रत्येक कॉन्ट्रैक्ट के लिए अलग-अलग चीट की आवश्यकता के बिना दोनों ऑपरेशन्स सफल हो जाते हैं।
यह ग्लोबल कॉलर cheatcode कई कॉन्ट्रैक्ट्स के बीच इंटरैक्शन को टेस्ट करने के लिए विशेष रूप से उपयोगी है जहाँ सभी कॉल्स एक ही एड्रेस से उत्पन्न होनी चाहिए। एक व्यावहारिक उदाहरण स्टेकिंग प्रोटोकॉल है, जहाँ एक उपयोगकर्ता को कई कॉन्ट्रैक्ट्स के साथ इंटरैक्ट करने की आवश्यकता होती है, ERC-20 कॉन्ट्रैक्ट पर टोकन को अप्रूव करना, फिर उन्हीं टोकन को स्टेकिंग कॉन्ट्रैक्ट में स्टेक करना, और वह भी समान कॉलर एड्रेस का उपयोग करके। ग्लोबल कॉलर चीट का उपयोग करने से इन सभी इंटरकनेक्टेड ऑपरेशन्स में कॉलर की पहचान समान रहती है।
जिस तरह caller_address cheatcodes हमें यह नियंत्रित करने देते हैं कि कॉन्ट्रैक्ट फंक्शन्स को कौन कॉल करता है, उसी तरह हमें कॉन्ट्रैक्ट्स को समय-आधारित लॉजिक (time-dependent logic) के साथ टेस्ट करने का एक तरीका भी चाहिए, वह भी वास्तविक समय के बीतने का इंतज़ार किए बिना। कई स्मार्ट कॉन्ट्रैक्ट्स में समय-आधारित प्रतिबंध शामिल होते हैं जैसे विथड्रावल डिले (withdrawal delays), वेस्टिंग शेड्यूल्स (vesting schedules), या कूलडाउन पीरियड्स (cooldown periods)। इन फीचर्स को टेस्ट करने के लिए सामान्यतः वास्तविक समय बीतने का इंतजार करना पड़ेगा, जिससे टेस्ट अव्यावहारिक हो जाएंगे। ब्लॉक टाइमस्टैम्प (Block timestamp) cheatcodes हमें कॉन्ट्रैक्ट के समय की धारणा (perception of time) को नियंत्रित करने की अनुमति देकर इस समस्या को हल करते हैं।
block_timestamp Cheatcodes
block_timestamp cheatcodes वास्तविक समय बीतने की प्रतीक्षा किए बिना समय-आधारित व्यवहार को सिमुलेट करना संभव बनाते हैं। इस cheatcode के लिए उपलब्ध फंक्शन्स हैं:
Starknet foundry block_timestamp cheatcode |
यह क्या करता है | Solidity foundry समकक्ष (equivalent) |
|---|---|---|
cheat_block_timestamp(target, timestamp, span) |
एक टारगेट कॉन्ट्रैक्ट के लिए ब्लॉक टाइमस्टैम्प सेट करता है, जो CheatSpan द्वारा सीमित होता है |
कोई सीधा समकक्ष नहीं (vm.warp(timestamp) केवल ग्लोबल है) |
start_cheat_block_timestamp(target, timestamp) |
एक टारगेट कॉन्ट्रैक्ट के लिए टाइमस्टैम्प सेट करना शुरू करता है | कोई सीधा समकक्ष नहीं |
start_cheat_block_timestamp_global(timestamp) |
सभी कॉन्ट्रैक्ट्स में विश्व स्तर पर ब्लॉक टाइमस्टैम्प सेट करता है | vm.warp(timestamp) |
stop_cheat_block_timestamp(target) |
एक टारगेट कॉन्ट्रैक्ट के लिए टाइमस्टैम्प संशोधन (modification) रोकता है | कोई सीधा समकक्ष नहीं |
stop_cheat_block_timestamp_global() |
ग्लोबल टाइमस्टैम्प संशोधन को रोकता है | vm.warp(original_timestamp) के साथ मैन्युअल रूप से रीसेट करें |
इन block_timestamp cheatcodes के काम करने के तरीके को स्पष्ट करने के लिए, हम HelloStarknet कॉन्ट्रैक्ट को संशोधित करेंगे ताकि इसमें टाइम-लॉक्ड कार्यक्षमता (time-locked functionality) शामिल हो सके। संशोधित कॉन्ट्रैक्ट में दो नए फंक्शन्स शामिल होंगे:
set_lock_time(duration)जो ओनर को वर्तमान समय प्राप्त करने के लिएget_block_timestamp()को कॉल करके और उसमें अवधि (duration) जोड़कर टाइम लॉक सेट करने की अनुमति देता है।time_locked_withdrawal(amount)जो ओनर को फंड निकालने की अनुमति देता है, लेकिन केवल लॉक टाइम बीत जाने के बाद, यह जाँच कर कि क्या वर्तमान टाइमस्टैम्प (get_block_timestamp()) स्टोर किए गए लॉक टाइम से अधिक या उसके बराबर है।
नीचे दिए गए अपडेटेड कोड को कॉपी करें और अपनी src/lib.cairo फाइल की सामग्री को बदलें:
use starknet::ContractAddress;
/// Interface representing `HelloContract`.
/// This interface allows modification and retrieval of the contract balance with time-locked functionality.
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
fn increase_balance(ref self: TContractState, amount: u256);
fn get_balance(self: @TContractState) -> u256;
fn get_owner(self: @TContractState) -> ContractAddress;
// NEWLY ADDED
fn time_locked_withdrawal(ref self: TContractState, amount: u256);
fn set_lock_time(ref self: TContractState, duration: u64);
}
#[starknet::contract]
mod HelloStarknet {
use starknet::{ContractAddress, get_caller_address, get_block_timestamp};
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
owner: ContractAddress,
balance: u256,
lock_until: u64,
}
#[constructor]
fn constructor(ref self: ContractState, owner: ContractAddress) {
// Set the owner when the contract is deployed
self.owner.write(owner);
}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn increase_balance(ref self: ContractState, amount: u256) {
// Get who is calling this function
let caller = get_caller_address();
// Only owner can increase balance
assert(caller == self.owner.read(), 'Only owner');
assert(amount != 0, 'Amount cannot be 0');
// Add the amount to current balance
self.balance.write(self.balance.read() + amount);
}
fn get_balance(self: @ContractState) -> u256 {
self.balance.read()
}
fn get_owner(self: @ContractState) -> ContractAddress {
self.owner.read()
}
// NEWLY ADDED: Time lock functionality
fn set_lock_time(ref self: ContractState, duration: u64) {
let caller = get_caller_address();
// Only owner can set lock time
assert(caller == self.owner.read(), 'Only owner');
assert(duration > 0, 'Duration must be positive');
// Set lock_until = current timestamp + duration
self.lock_until.write(get_block_timestamp() + duration);
}
// NEWLY ADDED: Time-locked withdrawal function
fn time_locked_withdrawal(ref self: ContractState, amount: u256) {
let caller = get_caller_address();
// Only owner can withdraw
assert(caller == self.owner.read(), 'Only owner');
// Check if enough time has passed since lock was set
// This is the key time-based check we'll test with cheatcodes
assert(get_block_timestamp() >= self.lock_until.read(), 'Still locked');
// Ensure sufficient balance for withdrawal
assert(amount <= self.balance.read(), 'Insufficient balance');
// Subtract the withdrawal amount from balance
self.balance.write(self.balance.read() - amount);
}
}
}
इस टाइम-लॉक्ड कॉन्ट्रैक्ट के लागू होने के साथ, आइए देखें कि block_timestamp cheatcodes का उपयोग करके इसे कैसे टेस्ट किया जाए।
cheat_block_timestamp cheatcode का उपयोग करना
cheat_block_timestamp cheatcode नियंत्रित संख्या में कॉल्स के लिए किसी विशिष्ट कॉन्ट्रैक्ट के ब्लॉक टाइमस्टैम्प को बदल (warp) देता है। यहाँ फंक्शन सिग्नेचर दिया गया है:
fn cheat_block_timestamp(target: ContractAddress, timestamp: u64, span: CheatSpan)
फ़ंक्शन तीन पैरामीटर लेता है:
target: वह विशिष्ट कॉन्ट्रैक्ट जिसे संशोधित टाइमस्टैम्प देखना चाहिएtimestamp: सेट किया जाने वाला टाइमस्टैम्प मानspan: कितनी कॉल्स को इस टाइमस्टैम्प को देखना चाहिए
ध्यान दें कि टेस्ट एन्वायरनमेंट में,
get_block_timestamp()डिफ़ॉल्ट रूप से 0 लौटाता है, इसलिए हम अपने टेस्ट्स में टाइमस्टैम्प असर्शन (assertions) के लिए इस पर निर्भर नहीं रह सकते हैं। इसके बजाय, हमें उन मानों के आधार पर टाइमस्टैम्प को मैन्युअल रूप से कैलकुलेट और ट्रैक करने की आवश्यकता है जिन्हें हम cheatcodes के साथ सेट करते हैं।
टाइम-लॉक्ड कार्यक्षमता (time-locked functionality) का टेस्ट करने के लिए, अपने मौजूदा snforge लाइब्रेरी इम्पोर्ट्स में cheat_block_timestamp cheatcode जोड़ें।
हम पहले दिखाएंगे कि टाइम-लॉक्ड विथड्रावल (time-locked withdrawals) किसी भी टाइमस्टैम्प हेरफेर के बिना विफल हो जाते हैं, फिर दिखाएंगे कि cheatcodes कैसे हमें समय प्रतिबंध (time restriction) को बायपास करने में सक्षम बनाते हैं:
#[test]
fn test_time_locked_withdrawal_fails_without_cheat() {
let contract_address = deploy_contract("HelloStarknet", OWNER);
let dispatcher = IHelloStarknetDispatcher { contract_address };
// Set up as owner for initial state
start_cheat_caller_address(contract_address, OWNER);
// Set up the contract state
dispatcher.increase_balance(1000);
// Set 1-hour lock: lock_until = current_time + 3600
dispatcher.set_lock_time(3600);
// Try to withdraw immediately without advancing time
// This will cause the test to fail with "Still locked" error when you run scarb test
dispatcher.time_locked_withdrawal(100);
// This assertion will never be reached because the withdrawal above fails
assert(dispatcher.get_balance() == 900, 'Withdrawal should fail');
}
जब आप scarb test test_time_locked_withdrawal_fails_without_cheat रन करते हैं, तो यह टेस्ट ‘Still locked’ त्रुटि (error) के साथ विफल हो जाएगा, जो यह साबित करता है कि टाइम लॉक मैकेनिज्म सही ढंग से काम करता है।

अब एक ऐसा टेस्ट देखते हैं जो cheat_block_timestamp का उपयोग करके “टाइम ट्रैवल” करता है ताकि टाइम-लॉक्ड विथड्रावल के सफल होने के लिए पर्याप्त समय बीतने का अनुकरण (simulate) किया जा सके।
#[test]
fn test_cheat_block_timestamp() {
let contract_address = deploy_contract("HelloStarknet", OWNER);
let dispatcher = IHelloStarknetDispatcher { contract_address };
// Set up initial state: we need 2 owner calls (increase_balance + set_lock_time)
cheat_caller_address(contract_address, OWNER, CheatSpan::TargetCalls(2));
// Add 1000 to the balance (first owner call)
dispatcher.increase_balance(1000); // Balance: 0 + 1000 = 1000
// Set a 1-hour time lock from current timestamp (second owner call)
dispatcher.set_lock_time(3600); // Lock until: current_time + 3600 seconds
// "Time travel" to 2 hours in the future (7200 seconds from block 0)
let future_timestamp = 7200;
cheat_block_timestamp(contract_address, future_timestamp, CheatSpan::TargetCalls(1));
// Need to impersonate owner again for the withdrawal call
cheat_caller_address(contract_address, OWNER, CheatSpan::TargetCalls(1));
// This withdrawal succeeds because get_block_timestamp() now returns 7200 which is > lock_until (3600)
dispatcher.time_locked_withdrawal(100); // Balance: 1000 - 100 = 900
// confirm the withdrawal was successful
assert(dispatcher.get_balance() == 900, 'Withdrawal failed');
}
उपरोक्त कोड में, हमने कॉन्ट्रैक्ट डिप्लॉय किया, ओनर के बैलेंस में 1000 जोड़े, और set_lock_time(3600) के साथ 1 घंटे का लॉक सेट किया।
cheat_block_timestamp(contract_address, future_timestamp, CheatSpan::TargetCalls(1)) को कॉल करने से कॉन्ट्रैक्ट को लगता है कि 2 घंटे (7200 सेकंड) बीत चुके हैं। जब time_locked_withdrawal() get_block_timestamp() का उपयोग करके वर्तमान समय की जाँच करता है, तो यह 7200 सेकंड देखता है, जो lock_until (3600) से अधिक है, इसलिए विथड्रावल सफल हो जाता है।
let future_timestamp = 7200; // 2 hours later
cheat_block_timestamp(contract_address, future_timestamp, CheatSpan::TargetCalls(1));
CheatSpan::TargetCalls(1) पैरामीटर का अर्थ है कि केवल अगली फ़ंक्शन कॉल (time_locked_withdrawal) ही इस संशोधित टाइमस्टैम्प को देखेगी।
cheat_block_timestamp वास्तविक देरी (delays) के बिना समय की प्रगति (time progression) को सिमुलेट करता है, जिससे हम समय-आधारित लॉजिक का तुरंत टेस्ट कर सकते हैं।
start_cheat_block_timestamp cheatcode का उपयोग करना
cheat_block_timestamp के विपरीत जिसके लिए अवधि को नियंत्रित करने के लिए CheatSpan पैरामीटर की आवश्यकता होती है, start_cheat_block_timestamp टारगेट कॉन्ट्रैक्ट को सभी बाद की कॉल्स के लिए सिमुलेटेड टाइमस्टैम्प दिखाता है जब तक कि इसे मैन्युअल रूप से रोका न जाए। यहाँ फंक्शन सिग्नेचर है:
fn start_cheat_block_timestamp(target: ContractAddress, timestamp: u64)
मौजूदा cheatcodes के साथ start_cheat_block_timestamp और stop_cheat_block_timestamp cheatcodes को शामिल करने के लिए snforge लाइब्रेरी इम्पोर्ट्स को अपडेट करें, ताकि हम देख सकें कि वे कैसे काम करते हैं:
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait, start_cheat_caller_address, stop_cheat_caller_address, cheat_caller_address, CheatSpan, start_cheat_caller_address_global, stop_cheat_caller_address_global, cheat_block_timestamp, start_cheat_block_timestamp, stop_cheat_block_timestamp};
नीचे दिए गए इस टेस्ट कोड पर विचार करें जो दिखाता है कि start_cheat_block_timestamp और stop_cheat_block_timestamp का उपयोग करके टाइम-लॉक्ड कार्यक्षमता को टेस्ट करने के लिए टाइमस्टैम्प चीट्स को रीस्टार्ट करके समय को कैसे आगे बढ़ाया जाए:
#[test]
fn test_start_cheat_block_timestamp() {
let contract_address = deploy_contract("HelloStarknet", OWNER);
let dispatcher = IHelloStarknetDispatcher { contract_address };
// Set a specific starting timestamp (August 6th, 2025)
let start_time = 1754439529;
start_cheat_caller_address(contract_address, OWNER);
// Set up the contract state
dispatcher.increase_balance(1000);
// Make all contract calls see this timestamp until we change it
start_cheat_block_timestamp(contract_address, start_time);
// Set 1-hour lock: lock_until = start_time + 3600
dispatcher.set_lock_time(3600); // Lock until: 1754439529 + 3600 = 1754443129
// move 2 hours forward (7200 seconds)
let future_time = start_time + 7200; // New time: 1754439529 + 7200 = 1754446729
// Stop the current timestamp cheat
stop_cheat_block_timestamp(contract_address);
// Start a new timestamp cheat with the future time
// This simulates 2 hours passing (future_time > lock_until, so withdrawal allowed)
start_cheat_block_timestamp(contract_address, future_time);
// Withdrawal succeeds because get_block_timestamp() returns future_time (1754446729)
// which is greater than lock_until (1754443129)
dispatcher.time_locked_withdrawal(100);
// confirm the withdrawal was successful
assert(dispatcher.get_balance() == 900, 'Withdrawal failed');
// stop both cheats
stop_cheat_caller_address(contract_address);
stop_cheat_block_timestamp(contract_address);
}
test_start_cheat_block_timestamp() में, हम एक विशिष्ट टाइमस्टैम्प (start_time) सेट करके शुरू करते हैं जिसे सभी कॉन्ट्रैक्ट कॉल्स देखेंगे, फिर बैलेंस जोड़कर और टाइम लॉक बनाकर कॉन्ट्रैक्ट स्थिति सेट करते हैं।
समय बीतने को सिमुलेट करने के लिए, हम वर्तमान टाइमस्टैम्प चीट को रोकते हैं और future_time (2 घंटे बाद) के साथ एक नया चीट शुरू करते हैं, जो विथड्रावल को सफल होने की अनुमति देता है क्योंकि कॉन्ट्रैक्ट अब बाद का टाइमस्टैम्प देखता है।
start_cheat_block_timestamp के साथ टाइमस्टैम्प को अपडेट करने के लिए, हमें वर्तमान चीट को रोकना होगा और एक नया शुरू करना होगा, प्रभावी रूप से start_time से future_time में बदलकर समय की प्रगति का सिमुलेशन करना होगा।
start_cheat_block_timestamp का उपयोग तब उपयोगी होता है जब आपको बाद की कार्रवाइयों के लिए समय बढ़ाने से पहले ठीक एक ही सिमुलेटेड समय पर कई ऑपरेशन्स करने की आवश्यकता होती है। start_cheat_caller_address के समान, यह cheatcode टारगेट-विशिष्ट (target-specific) है; यह केवल निर्दिष्ट कॉन्ट्रैक्ट एड्रेस पर किए गए कॉल्स को प्रभावित करता है। यदि आपको एक ही टेस्ट में कई अलग-अलग कॉन्ट्रैक्ट इंस्टेंसेस के साथ इंटरैक्शन के लिए अलग-अलग समय सेट करने की आवश्यकता है, तो आपको इसके बजाय start_cheat_block_timestamp_global cheatcode का उपयोग करना चाहिए।
caller_address और block_timestamp cheatcodes द्वारा कवर किए गए सभी परिदृश्यों (scenarios) में, टेस्टिंग के लिए यह सत्यापित करना आवश्यक है कि शर्तें पूरी होने पर फ़ंक्शन सही ढंग से काम करते हैं और जब उन्हें विफल होना चाहिए तो वे विफल होते हैं। यहीं पर हमें यह सुनिश्चित करने की आवश्यकता है कि कॉन्ट्रैक्ट ठीक से रिवर्ट (revert) हो।
एक Revert की उम्मीद करना (Expecting a revert)
यह टेस्ट करते समय कि किसी फ़ंक्शन को कुछ शर्तों के तहत विफल होना चाहिए, Starknet Foundry #[should_panic] एट्रिब्यूट (attribute) प्रदान करता है जो solidity के Foundry में vm.expectRevert() के समान है। यह एट्रिब्यूट अपने आप में कोई cheatcode नहीं है, लेकिन विफलता के परिदृश्यों (failure scenarios) का टेस्ट करने के लिए अन्य cheatcodes के साथ काम करता है:
#[should_panic(expected: ('Still locked',))]
#[should_panic] एट्रिब्यूट टेस्ट फ्रेमवर्क को बताता है:
- इस टेस्ट के पैनिक (panic) होने की अपेक्षा करें; यदि यह पैनिक नहीं होता है, तो टेस्ट विफल हो जाता है
- विशिष्ट त्रुटि संदेश (error message) की अपेक्षा करें; पैनिक में सटीक संदेश ‘Still locked’ होना चाहिए
- टेस्ट तभी पास होता है जब वह सही ढंग से पैनिक होता है; पैनिक और त्रुटि संदेश दोनों मैच होने चाहिए
जब कोई #[should_panic] टेस्ट पास होता है, तो यह पुष्टि करता है कि फ़ंक्शन अपेक्षानुसार पैनिक हुआ। यह सत्यापित करने के लिए कि टेस्ट इच्छित कारण से विफल होता है, सही त्रुटि संदेश के साथ expected पैरामीटर को शामिल करना महत्वपूर्ण है।
यहाँ एक बेसिक रिवर्ट टेस्ट का उदाहरण दिया गया है जो सत्यापित करता है कि लॉक अवधि समाप्त होने से पहले प्रयास किए जाने पर टाइम-लॉक्ड विथड्रावल विफल हो जाते हैं:
#[test]
#[should_panic(expected: ('Still locked',))]
fn test_time_locked_withdrawal_fails_too_early() {
let contract_address = deploy_contract("HelloStarknet", OWNER);
let dispatcher = IHelloStarknetDispatcher { contract_address };
start_cheat_caller_address(contract_address, OWNER);
// Set up the contract state
dispatcher.increase_balance(1000);
dispatcher.set_lock_time(3600); // Lock for 1 hour from timestamp 0
dispatcher.time_locked_withdrawal(100);
}
कोड को टेस्ट करने के लिए scarb test test_time_locked_withdrawal_fails_too_early रन करें:

चूँकि टेस्ट एन्वायरनमेंट टाइमस्टैम्प 0 से शुरू होता है और हम 3600-सेकंड का लॉक सेट करते हैं, विथड्रावल का प्रयास assert(get_block_timestamp() >= self.lock_until.read(), 'Still locked') लाइन पर हिट करता है और पैनिक करता है।
#[should_panic(expected: ('Still locked',))] एट्रिब्यूट टेस्ट फ्रेमवर्क को बताता है कि यह पैनिक अपेक्षित (expected) है और जब ऐसा होता है तो टेस्ट पास हो जाना चाहिए।
हमें test_time_locked_withdrawal_fails_too_early टेस्ट में stop_cheat_caller_address की आवश्यकता नहीं थी क्योंकि यह किसी भी क्लीनअप कोड तक पहुँचने से पहले ही पैनिक हो जाता है।
Safe Dispatcher का उपयोग करना
कभी-कभी हम अपने टेस्ट को पैनिक किए बिना त्रुटि (error) की जाँच करना चाहते हैं। इसके लिए, हम “Safe Dispatcher” का उपयोग कर सकते हैं।
Safe Dispatcher हमारे कॉन्ट्रैक्ट डिस्पैचर का एक स्वचालित रूप से जनरेट किया गया वेरिएंट (variant) है जो सीधे पैनिक होने के बजाय Result<T, Array<felt252>> लौटाता है।
जब हम IHelloStarknet जैसा कॉन्ट्रैक्ट इंटरफ़ेस परिभाषित करते हैं, तो कंपाइलर कई डिस्पैचर-संबंधित आइटम्स जनरेट करता है, लेकिन टेस्टिंग के लिए प्रासंगिक मुख्य आइटम्स हैं:
- Regular Dispatcher (
IHelloStarknetDispatcherऔरIHelloStarknetDispatcherTrait): त्रुटियों पर पैनिक करता है - Safe Dispatcher (
IHelloStarknetSafeDispatcherऔरIHelloStarknetSafeDispatcherTrait): Result टाइप्स लौटाता है
Safe Dispatcher का उपयोग तब करें जब आपको आवश्यकता हो:
- सटीक त्रुटि संदेश की जाँच करने की
- एक ही टेस्ट में कई त्रुटि स्थितियों का टेस्ट करने की
- पैनिक किए बिना प्रोग्रामेटिक रूप से त्रुटियों को हैंडल करने की
निम्नलिखित टेस्ट उदाहरण एक्सेस कंट्रोल को सत्यापित करने के लिए Safe Dispatcher का उपयोग करता है, यह सुनिश्चित करके कि अनधिकृत कॉल्स (unauthorized calls) विफल होते हैं और सही त्रुटि संदेश लौटाते हैं। कॉन्ट्रैक्ट इंटरैक्शन को सक्षम करने के लिए Safe Dispatchers (IHelloStarknetSafeDispatcher और IHelloStarknetSafeDispatcherTrait) को इम्पोर्ट करें:
const USER: ContractAddress = 'USER'.try_into().unwrap();
#[test]
#[feature("safe_dispatcher")]
fn test_non_owner_error_with_safe_dispatcher() {
// Deploy the HelloStarknet contract with OWNER as the owner
let contract_address = deploy_contract("HelloStarknet", OWNER);
// Use the safe dispatcher variant to handle errors gracefully
let safe_dispatcher = IHelloStarknetSafeDispatcher { contract_address };
// Impersonate USER who is NOT the owner
start_cheat_caller_address(contract_address, USER);
// Call increase_balance - this will fail but return a Result instead of panicking
match safe_dispatcher.increase_balance(100) {
// If the call succeeds, the test should fail because non-owners shouldn't have access
Result::Ok(_) => core::panic_with_felt252('Should have panicked'),
// If the call fails (expected), confirm we get the correct error message
Result::Err(panic_data) => {
// Check that the first element of panic_data contains our expected error message
assert(*panic_data.at(0) == 'Only owner', 'Wrong error message');
},
};
// stop the caller impersonation
stop_cheat_caller_address(contract_address);
}
ऊपर दिए गए test_non_owner_error_with_safe_dispatcher टेस्ट में, जब USER_1 बैलेंस बढ़ाने का प्रयास करता है, तो safe dispatcher सफलता (Ok) या विफलता (Err) लौटाता है:
match safe_dispatcher.increase_balance(100) {
Result::Ok(_) => core::panic_with_felt252('Should have panicked'), //success
Result::Err(panic_data) => {
assert(*panic_data.at(0) == 'Only owner', 'Wrong error message'); //failure
},
};
यदि कॉल अप्रत्याशित रूप से सफल होती है, तो टेस्ट ‘Should have panicked’ के साथ विफल हो जाता है क्योंकि गैर-ओनर्स (non-owners) के पास एक्सेस नहीं होना चाहिए। यदि यह अपेक्षानुसार विफल होता है, तो हम panic_data ऐरे के पहले एलिमेंट की जाँच करके यह सत्यापित करते हैं कि त्रुटि संदेश बिल्कुल ‘Only owner’ है।

इस तरह हम सत्यापित कर सकते हैं कि फ़ंक्शन विफल होता है और यह भी कि वह सही त्रुटि संदेश के साथ विफल होता है।
Storage में लिखना (Writing to Storage)
store cheatcode हमें टेस्टिंग के दौरान सीधे कॉन्ट्रैक्ट स्टोरेज स्लॉट्स में मान (values) लिखने देता है, वह भी कॉन्ट्रैक्ट के फंक्शन्स को इनवोक किए बिना या इसके सामान्य लॉजिक फ्लो को निष्पादित किए बिना। इसका मतलब है कि हम चेक्स (checks), वैलिडेशन्स (validations), एक्सेस कंट्रोल, और अन्य स्टेट ट्रांज़िशन्स (state transitions) को बायपास कर सकते हैं जो सामान्य रूप से फंक्शन कॉल्स के माध्यम से होंगे। यह विशेष रूप से विशिष्ट कॉन्ट्रैक्ट स्टेट्स को सेट करने या रेगुलर फंक्शन कॉल्स के बिना एज केसेस (edge cases) को टेस्ट करने के लिए उपयोगी है।
HelloStarknet कॉन्ट्रैक्ट में, हमारे पास नीचे दिखाए अनुसार स्टोरेज है:
#[storage]
struct Storage {
owner: ContractAddress,
balance: u256,
lock_until: u64,
}
प्रत्येक स्टोरेज वेरिएबल का एक विशिष्ट स्लॉट होता है जिसे हम store cheatcode का उपयोग करके सीधे लिख सकते हैं।
store cheatcode
fn store(target: ContractAddress, storage_address: felt252, serialized_value: Span<felt252>)
यह तीन पैरामीटर्स लेता है:
target: संशोधित किया जाने वाला कॉन्ट्रैक्ट एड्रेसstorage_address: स्टोरेज स्लॉट लोकेशन (map_entry_addressका उपयोग करके कैलकुलेट किया गया)serialized_value: स्टोर किया जाने वाला मान, जिसेfelt252ऐरे में बदल दिया गया हो
स्टोरेज एड्रेस खोजना (Finding Storage Addresses)
store cheatcode का उपयोग करने के लिए, हमें सबसे पहले उस वेरिएबल के लिए सटीक स्टोरेज एड्रेस कैलकुलेट करना होगा जिसे हम संशोधित करना चाहते हैं। हम स्टोरेज लोकेशन की गणना करने के लिए map_entry_address का उपयोग करेंगे।
मौजूदा वालों के साथ snforge_std लाइब्रेरी से store और map_entry_address दोनों को इम्पोर्ट करें:
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait, start_cheat_caller_address, stop_cheat_caller_address, cheat_caller_address, CheatSpan, start_cheat_caller_address_global, stop_cheat_caller_address_global, cheat_block_timestamp, start_cheat_block_timestamp, stop_cheat_block_timestamp, store, map_entry_address};
यहाँ बताया गया है कि हम balance वेरिएबल के लिए एड्रेस कैसे प्राप्त करते हैं:
let balance_storage_addr = map_entry_address(
map_selector: selector!("balance"),
keys: array![].span()
);
map_entry_address फंक्शन किसी वेरिएबल के लिए सटीक स्टोरेज स्लॉट एड्रेस की गणना करता है। यह दो पैरामीटर लेता है:
map_selector: एक स्टोरेज आइडेंटिफ़ायर जो विशिष्ट रूप से स्टोरेज वेरिएबल की पहचान करता हैkeys: मैप किए गए स्टोरेज तक पहुँचने के लिए उपयोग की जाने वाली कीज़ (keys) का एक ऐरे
हमारे उदाहरण में:
selector!("balance")स्टोरेज वेरिएबल नाम (balance) को आवश्यक स्टोरेज आइडेंटिफ़ायर में परिवर्तित करता हैkeys: array![].span()एक खाली ऐरे है क्योंकिbalanceएक साधारण स्टोरेज वेरिएबल है, कोई मैपिंग नहीं। यदिbalanceLegacyMap<ContractAddress, u256>जैसी कोई मैपिंग होती, तो हम यहाँ एड्रेस की (key) पास करते
परिणाम, balance_storage_addr, वह स्टोरेज स्लॉट एड्रेस है जिसे अब हम store cheatcode में पास कर सकते हैं।
balance (नॉन-मैपिंग) जैसे साधारण स्टोरेज वेरिएबल्स के लिए, आप छोटे सिंटैक्स (syntax) का भी उपयोग कर सकते हैं:
let balance_storage_addr = selector!("balance");
किसका उपयोग कब करें:
- केवल साधारण स्टोरेज वेरिएबल्स (जैसे
u256,felt252,bool) के लिएselector!()का उपयोग करें - निम्नलिखित के लिए
map_entry_address()का उपयोग करें:LegacyMapटाइप्स (keysऐरे में मैप की प्रदान करें)- Arrays (
keysऐरे में इंडेक्स प्रदान करें) - कोई भी स्टोरेज टाइप जहाँ आपको कीज़ या इंडिसेस (indices) निर्दिष्ट करने की आवश्यकता हो
- साधारण वेरिएबल्स (खाली कीज़ का उपयोग करते हुए:
array![].span()) - हालाँकि इस मामले के लिएselector!()छोटा है
दोनों विधियाँ उस सटीक स्टोरेज एड्रेस की गणना करती हैं जहाँ कॉन्ट्रैक्ट वेरिएबल को स्टोर करता है, जिससे हम नए मानों को सीधे उस लोकेशन पर लिख सकते हैं।
मानों को सीरियलाइज़ करना (Serializing Values)
विभिन्न डेटा टाइप्स को अलग-अलग सीरियलाइज़ेशन फ़ॉर्मेट की आवश्यकता होती है:
ContractAddressके लिए: सिंगलfelt252
let serialized_owner = array![OWNER.into()];
u64के लिए: सिंगलfelt252
let timestamp: u64 = 1641070800;
let serialized_timestamp = array![timestamp.into()];
u256के लिए (हमारा बैलेंस टाइप): इसे लो (low) और हाई (high) पार्ट्स की आवश्यकता होती है क्योंकिu256सिंगलfelt252की क्षमता से बड़ा होता है।
let balance: u256 = 5000;
let serialized_balance = array![balance.low.into(), balance.high.into()];
निम्नलिखित टेस्ट यह प्रदर्शित करता है कि स्टोरेज राइट्स सभी एक्सेस कंट्रोल को बायपास करते हैं; किसी increase_balance() कॉल या ओनरशिप जाँच की आवश्यकता नहीं है। हम किसी भी कॉन्ट्रैक्ट फ़ंक्शन को इनवोक किए बिना सीधे HelloStarknet कॉन्ट्रैक्ट के बैलेंस को 5000 में बदल देंगे:
#[test]
fn test_store_balance_directly() {
let contract_address = deploy_contract("HelloStarknet", OWNER);
let dispatcher = IHelloStarknetDispatcher { contract_address };
//calculate the storage address where the "balance" variable is stored
let balance_storage_addr = map_entry_address(
map_selector: selector!("balance"),
keys: array![].span()
);
// value to write directly to storage
let new_balance: u256 = 5000;
// Serialize u256 into low and high parts (u256 = {low: u128, high: u128})
// In Cairo, u256 values are serialized as 2 felt252 values - one for lower 128 bits, one for upper 128 bits
let serialized_value = array![new_balance.low.into(), new_balance.high.into()];
// Check balance before direct storage write
let balance_before = dispatcher.get_balance();
assert(balance_before == 0, 'Initial balance should be 0');
// write directly to storage
store(contract_address, balance_storage_addr, serialized_value.span());
assert(dispatcher.get_balance() == 5000, 'Direct storage write failed');
}
load cheatcode के साथ सीधे स्टोरेज से पढ़ना
स्टोर किए गए मानों को सत्यापित करने के लिए कॉन्ट्रैक्ट फंक्शन्स का उपयोग करने के बजाय, हम स्टोरेज से सीधे पढ़ने के लिए load cheatcode का उपयोग कर सकते हैं। यहाँ फ़ंक्शन सिग्नेचर है:
fn load(target: ContractAddress, storage_address: felt252, size: felt252) -> Array<felt252>
यह तीन पैरामीटर लेता है:
target: वह कॉन्ट्रैक्ट एड्रेस जहाँ से पढ़ना हैstorage_address: पढ़ा जाने वाला स्टोरेज स्लॉट लोकेशनsize: पढ़े जाने वालेfelt252मानों की संख्या
snforge_std से load इम्पोर्ट करें। यहाँ एक टेस्ट दिया गया है जो store का उपयोग करके बैलेंस लिखता है और load का उपयोग करके इसे वापस पढ़ता है:
#[test]
fn test_load_balance_directly() {
let contract_address = deploy_contract("HelloStarknet", OWNER);
// Calculate the storage address where the "balance" variable is stored
let balance_storage_addr = selector!("balance");
// Value to write directly to storage
let new_balance: u256 = 5000;
// Serialize u256 into low and high parts (u256 = {low: u128, high: u128})
// In Cairo, u256 values are serialized as 2 felt252 values - one for lower 128 bits, one for upper 128 bits
let serialized_value = array![new_balance.low.into(), new_balance.high.into()];
// Write directly to storage
store(contract_address, balance_storage_addr, serialized_value.span());
// Read the raw storage data from the balance storage slot
let stored_data = load(contract_address, balance_storage_addr, 2);
// Extract the low and high parts from the storage data array
let stored_balance_low = *stored_data.at(0);
let stored_balance_high = *stored_data.at(1);
// Reconstruct the u256 from its low and high components
let stored_balance: u256 = u256 {
low: stored_balance_low.try_into().unwrap(),
high: stored_balance_high.try_into().unwrap(),
};
// Confirm that the directly read storage value matches our expected balance
assert(stored_balance == 5000, 'Direct storage read failed');
}
ध्यान दें कि हमने पढ़ने के लिए आकार (size) के रूप में 2 का उपयोग किया है:
let stored_data = load(contract_address, balance_storage_addr, 2);
ऐसा इसलिए है क्योंकि balance u256 प्रकार का है और Cairo में, u256 मानों को 2 felt252 मानों के रूप में सीरियलाइज़ किया जाता है; एक जिसमें निचले 128 बिट्स होते हैं और दूसरे में ऊपरी 128 बिट्स होते हैं जैसा कि पहले उल्लेख किया गया है। यही कारण है कि हमें 2 felts पढ़ने और पूर्ण u256 मान को फिर से बनाने की आवश्यकता है। यदि स्टोर किया गया मान u512 प्रकार का होता, तो हम 4 felt252 मान लोड कर रहे होते।
store और load दोनों सीधे स्टोरेज एक्सेस प्रदान करते हैं जो विशिष्ट टेस्ट परिदृश्यों (test scenarios) को जल्दी से सेट करने और यह टेस्ट करने के लिए उपयोगी है कि आपका कॉन्ट्रैक्ट विभिन्न स्टेट कंडीशन (state conditions) के तहत कैसे काम करता है।
यह जाँचना कि क्या कोई Event एमिट (Emit) हुआ था
Starknet Foundry कॉन्ट्रैक्ट निष्पादन के दौरान विशिष्ट इवेंट्स के एमिट (emit) होने को कैप्चर और सत्यापित करने के लिए spy_events cheatcode भी प्रदान करता है। cheatcode द्वारा प्रदान किए गए मुख्य फंक्शन्स में शामिल हैं:
spy_events()- इवेंट्स कैप्चर करना शुरू करेंget_events()- कैप्चर किए गए इवेंट्स प्राप्त करें- इवेंट फ़िल्टरिंग और असर्शन (assertion) यूटिलिटीज़
cheatcodes के साथ इवेंट टेस्टिंग के विस्तृत उदाहरणों और व्यापक कवरेज के लिए, Starknet में Events पर हमारा आर्टिकल देखें।
निष्कर्ष (Conclusion)
इस आर्टिकल में Cairo स्मार्ट कॉन्ट्रैक्ट टेस्टिंग के लिए कुछ प्राथमिक cheatcodes को कवर किया गया है: caller_address, block_timestamp, store, load, और #[should_panic] के साथ रिवर्ट टेस्टिंग, और safe dispatcher का उपयोग करना। ये फंक्शन्स कॉलर प्रतिरूपण (caller impersonation), टाइमस्टैम्प हेरफेर (timestamp manipulation), डायरेक्ट स्टोरेज एक्सेस और त्रुटि सत्यापन (error verification) क्षमताएँ प्रदान करते हैं।
Solidity Foundry के टेस्टिंग फ्रेमवर्क के भीतर cheatcodes के समान, Starknet Foundry cheatcodes Cairo के आर्किटेक्चर के लिए अनुकूलित सिंटैक्स के साथ तुलनीय कार्यक्षमता प्रदान करते हैं। मुख्य टेस्टिंग कांसेप्ट्स दोनों इकोसिस्टम्स में एक समान रहते हैं।
अतिरिक्त cheatcodes के लिए, starknet foundry book देखें।
यह आर्टिकल Cairo Programming on Starknet पर ट्यूटोरियल सीरीज़ का हिस्सा है।