ERC-6909 टोकन स्टैंडर्ड, ERC-1155 टोकन स्टैंडर्ड का एक सुव्यवस्थित (streamlined) विकल्प है।
ERC-1155 स्टैंडर्ड ने एक मल्टीपल-टोकन इंटरफ़ेस पेश किया जो एक सिंगल स्मार्ट कॉन्ट्रैक्ट को फंजीबल और नॉन-फंजीबल टोकन (यानी, ERC20 और ERC721) को शामिल करने की अनुमति देता है।
ERC-1155 ने कई चुनौतियों का समाधान किया, जैसे डिप्लॉयमेंट की लागत कम करना, Ethereum ब्लॉकचेन पर रिडंडेंट बाइटकोड को कम करना, और मल्टी-टोकन ट्रेडिंग के लिए टोकन अप्रूवल को सुव्यवस्थित करना।
हालाँकि, इसने हर ट्रांसफर के लिए अनिवार्य कॉलबैक, बैच ट्रांसफर के ज़बरदस्ती शामिल किए जाने और सिंगल-ऑपरेटर अप्रूवल स्कीम के फाइन-ग्रेन्ड कंट्रोल की कमी के कारण कुछ ब्लोट (bloat) और गैस इनएफिशिएंसी (gas inefficiency) पेश की। ERC-6909 कॉन्ट्रैक्ट-लेवल कॉलबैक और बैच ट्रांसफर को हटाकर और ग्रैन्युलर टोकन मैनेजमेंट के लिए सिंगल-ऑपरेटर परमिशन स्कीम को हाइब्रिड (अलाउंस-ऑपरेटर) परमिशन स्कीम से बदलकर इन कमियों को दूर करता है।
नोट:
निम्नलिखित सेक्शन में यह मान लिया गया है कि आप ERC-1155 स्टैंडर्ड और इसके कॉन्सेप्ट्स से परिचित हैं। यदि आप परिचित नहीं हैं, तो कृपया आगे बढ़ने से पहले आर्टिकल को पढ़ें।
ERC-6909 और ERC-1155 स्टैंडर्ड के बीच अंतर
ERC-6909 ट्रांसफर के लिए कॉलबैक की आवश्यकता को हटाता है
ERC-1155 स्पेसिफिकेशन के लिए आवश्यक है कि safeTransferFrom और safeBatchTransferFrom यह जांचें कि रिसीव करने वाला अकाउंट एक कॉन्ट्रैक्ट है। यदि ऐसा है, तो इसे यह जांचने के लिए कि क्या यह ट्रांसफर स्वीकार करता है, प्राप्तकर्ता कॉन्ट्रैक्ट अकाउंट पर ERC1155TokenReceiver इंटरफ़ेस फ़ंक्शन्स (onERC1155Received, onERC1155BatchReceived) को अवश्य कॉल (MUST call) करना चाहिए।
ये कॉलबैक कुछ मामलों में सहायक होते हैं। हालाँकि, ये उन प्राप्तकर्ताओं के लिए अनावश्यक एक्सटर्नल कॉल हैं जो इस व्यवहार से बाहर (opt out) निकलना चाहते हैं। कॉलबैक प्राप्तकर्ता कॉन्ट्रैक्ट अकाउंट्स की गैस कॉस्ट और कोडसाइज को प्रभावित करते हैं, क्योंकि उन्हें कई कॉलबैक (यानी, onERC1155Received, onERC1155BatchReceived के माध्यम से) लागू करने और टोकन प्राप्त करने के लिए मैजिक 4-बाइट वैल्यू वापस करने की आवश्यकता होती है। इसके विपरीत, ERC-6909 इम्प्लीमेंटर्स को अपने कॉलबैक आर्किटेक्चर पर निर्णय लेने की अनुमति है।
ERC-6909 बैच ट्रांसफर लॉजिक को छोड़ देता है
बैच ट्रांसफर, हालांकि कभी-कभी फायदेमंद होते हैं, जानबूझकर ERC-6909 स्टैंडर्ड से हटा दिए गए हैं ताकि डेवलपर्स विशिष्ट एग्जीक्यूशन एन्वायरनमेंट के अनुरूप बैच ट्रांसफर लॉजिक लागू कर सकें। डेवलपर्स जैसा उचित समझें वैसा बैच ट्रांसफर लागू कर सकते हैं और उन्हें केवल स्टैंडर्ड का पालन करने के लिए अतिरिक्त बैच ट्रांसफर फ़ंक्शन जोड़ने की आवश्यकता नहीं है।
नीचे दिखाया गया safeBatchTransferFrom फ़ंक्शन, ERC-1155 स्टैंडर्ड में बैच ट्रांसफर निष्पादित करता है। हालाँकि, इसका जबरन शामिल होना उन एप्लिकेशन के लिए ब्लोट (bloat) जोड़ता है जिन्हें इनकी आवश्यकता नहीं है:
// ERC-1155
function safeBatchTransferFrom(
address _from,
address _to,
uint256[] calldata _ids,
uint256[] calldata _values,
bytes calldata _data
) external;
यहाँ ERC-6909 transferFrom फ़ंक्शन है। हम देख सकते हैं कि बैच फीचर और _data पैरामीटर को समाप्त कर दिया गया है।
// ERC-6909
function transferFrom(
address sender,
address receiver,
uint256 id,
uint256 amount
) public returns (bool) {
if (sender != msg.sender && !isOperator[sender][msg.sender]) {
uint256 senderAllowance = allowance[sender][msg.sender][id];
if (senderAllowance < amount) revert InsufficientPermission();
if (senderAllowance != type(uint256).max) {
allowance[sender][msg.sender][id] = senderAllowance - amount;
}
}
if (balanceOf[sender][id] < amount) revert InsufficientBalance();
balanceOf[sender][id] -= amount;
balanceOf[receiver][id] += amount;
emit Transfer(msg.sender, sender, receiver, id, amount);
return true;
}
ERC-6909 ग्लोबल अप्रूवल और ग्रैन्युलर अलाउंस दोनों का समर्थन करता है
// in ERC-1155 →
function setApprovalForAll(
address _operator,
bool _approved
) external;
ऊपर दिखाया गया setApprovalForAll फ़ंक्शन, ERC-1155 में एक ग्लोबल ऑपरेटर मॉडल है जो एक अकाउंट को उनकी ओर से सभी टोकन आईडी प्रबंधित करने (एक ऑपरेटर के रूप में कार्य करने) के लिए दूसरे अकाउंट को अधिकृत करने की अनुमति देता है। एक बार अधिकृत होने के बाद, ऑपरेटरों के पास अधिकृत करने वाले अकाउंट के स्वामित्व वाली किसी भी टोकन आईडी की कितनी भी राशि को ट्रांसफर करने का अप्रतिबंधित (unrestricted) एक्सेस होता है।
जबकि यह दृष्टिकोण डेलिगेशन को सरल बनाता है, इसमें फाइन-ग्रेन्ड कंट्रोल का अभाव है:
- व्यक्तिगत टोकन आईडी या राशियों के लिए विशिष्ट अनुमतियां देने का कोई तरीका नहीं है।
- यह ऑल-और-नथिंग (all-or-nothing) दृष्टिकोण उन परिदृश्यों के साथ संरेखित नहीं होगा जिनमें नियंत्रित अनुमतियों की आवश्यकता होती है।
ग्रैन्युलर कंट्रोल पेश करने के लिए, ERC-6909 हाइब्रिड ऑपरेटर परमिशन स्कीम में निम्नलिखित शामिल हैं:
- ERC-1155 से ऑपरेटर मॉडल,
- और ERC-20 से प्रेरित अलाउंस मॉडल।
ERC-6909 में ऑपरेटर मॉडल
नीचे दिखाए गए ERC-6909 setOperator फ़ंक्शन में, spender वेरिएबल को एक ऑपरेटर के रूप में सेट किया गया है और उसे अलाउंस प्रतिबंधों के बिना अकाउंट के स्वामित्व वाले सभी टोकन आईडी को ट्रांसफर करने की ग्लोबल अनुमति दी गई है।
function setOperator(address spender, bool approved) public returns (bool) {
isOperator[msg.sender][spender] = approved;
emit OperatorSet(msg.sender, spender, approved);
return true;
}
ERC-6909 में अलाउंस मॉडल
अलाउंस मॉडल एक टोकन-विशिष्ट और अमाउंट-विशिष्ट कंट्रोल सिस्टम पेश करता है जहां एक अकाउंट किसी विशिष्ट टोकन आईडी के लिए एक सीमित अलाउंस सेट कर सकता है।
उदाहरण के लिए, Alice नीचे दिखाए गए ERC-6909 में approve फ़ंक्शन का उपयोग करके, Bob को अन्य टोकन आईडी या असीमित राशियों का एक्सेस दिए बिना Token ID 42 की 100 यूनिट्स ट्रांसफर करने की अनुमति दे सकती है।
function approve(address spender, uint256 id, uint256 amount) public returns (bool) {
allowance[msg.sender][spender][id] = amount;
emit Approval(msg.sender, spender, id, amount);
return true;
}
approve में spender वेरिएबल एक ऐसा अकाउंट है जो टोकन मालिक की ओर से एक विशिष्ट टोकन आईडी की विशिष्ट मात्रा को ट्रांसफर करने के लिए अधिकृत है।
उदाहरण के लिए, एक टोकन मालिक spender को किसी विशेष Token ID की <= 100 यूनिट्स को ट्रांसफर करने की अनुमति दे सकता है। वैकल्पिक रूप से, वे अलाउंस को type(uint256).max पर सेट करके किसी विशिष्ट टोकन आईडी के लिए असीमित (infinite) अप्रूवल भी दे सकते हैं।
ERC-6909 यह निर्दिष्ट नहीं करता है कि type(uint256).max पर सेट किए गए अलाउंस में कटौती की जानी चाहिए या नहीं। इसके बजाय, यह व्यवहार ERC-20 के समान, इम्प्लीमेंटर के विवेक (discretion) पर छोड़ दिया गया है।
कोर डेटा स्ट्रक्चर्स
ERC-6909 इम्प्लीमेंटेशन्स अकाउंट बैलेंस और अप्रूवल की स्थिति (state) को अपडेट करने के लिए तीन मैपिंग का उपयोग करते हैं।
balanceOf: किसी ID का ओनर बैलेंस
balanceOf मैपिंग किसी एड्रेस (owner) द्वारा रखे गए एक विशिष्ट टोकन आईडी के बैलेंस को ट्रैक करता है। मैपिंग में owner => (id => amount) स्ट्रक्चर यह दर्शाता है कि एक सिंगल ओनर कई टोकन रख सकता है और उनके संबंधित आईडी के माध्यम से उनके बैलेंस को ट्रैक कर सकता है।
mapping(address owner => mapping(uint256 id => uint256 amount)) public balanceOf;
allowance: किसी ID का स्पेंडर अलाउंस
अलाउंस मैपिंग यह परिभाषित करती है कि एक स्पेंडर ओनर की ओर से किसी विशिष्ट टोकन (ID) का कितना हिस्सा ट्रांसफर कर सकता है। यह टोकन स्पेंडिंग (खर्च) पर फाइन-ग्रेन्ड कंट्रोल की सुविधा प्रदान करता है।
mapping(address owner => mapping(address spender => mapping(uint256 id => uint256 amount))) public allowance;
उदाहरण के लिए, allowance[0xDEF...][0x123...][5] उन टोकन की मात्रा (ID 5 के साथ) लौटाएगा जिसे ओनर 0xDEF... ने स्पेंडर 0x123... को ट्रांसफर करने की अनुमति दी है।
isOperator: ऑपरेटर अप्रूवल स्टेटस
mapping(address owner => mapping(address operator => bool isOperator)) public isOperator;
यह मैपिंग ट्रैक करती है कि किसी स्पेंडर को किसी एड्रेस के स्वामित्व वाले सभी टोकन के लिए ऑपरेटर के रूप में अप्रूव किया गया है या नहीं। उदाहरण के लिए, isOperator[0x123...][0xABC...] true लौटाता है यदि एड्रेस 0xABC... को एड्रेस 0x123... के स्वामित्व वाले टोकन खर्च करने की अनुमति है; अन्यथा, यह false लौटाता है।
कोर ERC-6909 फ़ंक्शन्स और उनके डेटा पैरामीटर्स
ट्रांसफर फ़ंक्शन्स
यह स्पेसिफिकेशन ERC-721 और ERC-1155 में देखे गए “सेफ ट्रांसफर मैकेनिज्म” का पालन नहीं करता है, क्योंकि नेमिंग कन्वेंशन को भ्रामक माना गया था, यह देखते हुए कि वे आर्बिट्रेरी (arbitrary) कॉन्ट्रैक्ट्स पर एक्सटर्नल कॉल करते हैं। ERC-6909 निम्नलिखित विवरणों के साथ transfer और transferFrom फ़ंक्शन्स का उपयोग करता है।
Transfer:
ERC-6909 transfer फ़ंक्शन ERC-20 transfer के समान ही व्यवहार करता है, सिवाय इसके कि यह एक विशिष्ट टोकन आईडी पर लागू होता है। फ़ंक्शन रिसीवर एड्रेस, टोकन की आईडी और ट्रांसफर की जाने वाली राशि को इनपुट पैरामीटर के रूप में लेता है और balanceOf मैपिंग का उपयोग करके बैलेंस को अपडेट करता है। ERC-20 ट्रांसफर फ़ंक्शन की तरह, यदि ट्रांजेक्शन सफलतापूर्वक निष्पादित हो जाता है तो true वापस करना आवश्यक है।
//ERC-20 interface transfer function
function transfer(address _to, uint256 _value) public returns (bool)
// ERC-6909 transfer function Reference Implementation
// @notice Transfers an amount of an id from the caller to a receiver.
// @param receiver The address of the receiver.
// @param id The id of the token.
// @param amount The amount of the token.
function transfer(address receiver, uint256 id, uint256 amount) public returns (bool) {
if (balanceOf[msg.sender][id] < amount) revert InsufficientBalance(msg.sender, id);
balanceOf[msg.sender][id] -= amount;
balanceOf[receiver][id] += amount;
emit Transfer(msg.sender, msg.sender, receiver, id, amount);
return true;
}
transferFrom:
ERC-6909 का transferFrom फ़ंक्शन टोकन आईडी की आवश्यकता के कारण ERC-20 के फ़ंक्शन से अलग है। इसके अतिरिक्त, यह अलाउंस के अलावा ऑपरेटर अप्रूवल की भी जाँच करता है।
यह फ़ंक्शन पहले if (sender != msg.sender && !isOperator[sender][msg.sender]) की जाँच करता है, यह सुनिश्चित करते हुए कि कॉलर (msg.sender) इनमें से एक है:
- ओनर (
sender), या - एक अप्रूव्ड ऑपरेटर (
isOperator[sender][msg.sender] == true)।
यदि msg.sender ओनर या एक अप्रूव्ड ऑपरेटर नहीं है, तो फ़ंक्शन यह जाँचता है कि क्या कॉलर के पास ट्रांसफर के लिए पर्याप्त अलाउंस (enough allowance) है। यदि अलाउंस मौजूद है लेकिन उसे अनलिमिटेड पर सेट नहीं किया गया है (type(uint256).max), तो ट्रांसफर की गई amount को अलाउंस से काट लिया जाता है।
इसके अलावा, स्टैंडर्ड निर्दिष्ट करता है कि यदि कॉलर एक ऑपरेटर या sender है तो फ़ंक्शन को कॉलर के टोकन id के allowance से amount नहीं घटाना चाहिए (SHOULD NOT subtract)।
// ERC-6909 transferFrom
function transferFrom(address sender, address receiver, uint256 id, uint256 amount) public returns (bool) {
if (sender != msg.sender && !isOperator[sender][msg.sender]) {
uint256 senderAllowance = allowance[sender][msg.sender][id];
if (senderAllowance < amount) revert InsufficientPermission();
if (senderAllowance != type(uint256).max) {
allowance[sender][msg.sender][id] = senderAllowance - amount;
}
}
if (balanceOf[sender][id] < amount) revert InsufficientBalance();
balanceOf[sender][id] -= amount;
balanceOf[receiver][id] += amount;
emit Transfer(msg.sender, sender, receiver, id, amount);
return true;
}
approve:
approve फ़ंक्शन कॉलर (msg.sender) को एक स्पेंडर को टोकन (ID) का विशिष्ट अलाउंस देने की अनुमति देता है। यह नए अलाउंस को दर्शाने के लिए अलाउंस मैपिंग को अपडेट करता है और एक Approval इवेंट एमिट (emit) करता है।
function approve(address spender, uint256 id, uint256 amount) public returns (bool) {
allowance[msg.sender][spender][id] = amount;
emit Approval(msg.sender, spender, id, amount);
return true;
}
setOperator:
setOperator फ़ंक्शन कॉलर (msg.sender) को अप्रूव्ड पैरामीटर को true या false पर सेट करके उनकी ओर से किसी विशिष्ट एड्रेस (spender) के लिए ऑपरेटर परमिशन देने या रद्द करने (revoke) की अनुमति देता है। यह फ़ंक्शन उसी के अनुसार isOperator मैपिंग को अपडेट करता है और बदलाव के बाहरी श्रोताओं (external listeners) को सूचित करने के लिए एक OperatorSet इवेंट एमिट करता है।
function setOperator(address spender, bool approved) public returns (bool) {
isOperator[msg.sender][spender] = approved;
emit OperatorSet(msg.sender, spender, approved);
return true;
}
ERC-6909 में इवेंट्स और लॉगिंग्स
ERC-6909 मल्टी-टोकन कॉन्ट्रैक्ट के भीतर टोकन ट्रांसफर, अप्रूवल और ऑपरेटर अनुमतियों को ट्रैक करने के लिए प्रमुख इवेंट्स को परिभाषित करता है।
1. Transfer इवेंट:
/// @notice The event emitted when a transfer occurs.
event Transfer(address caller, address indexed sender, address indexed receiver, uint256 indexed id, uint256 amount);
ERC-6909 में Transfer इवेंट का उपयोग टोकन मूवमेंट्स को ट्रैक करने के लिए किया जाता है और इसे निम्नलिखित शर्तों के तहत एमिट किया जाना चाहिए:
- इवेंट तब एमिट होता है जब किसी टोकन
idकीamountएक अकाउंट से दूसरे अकाउंट में ट्रांसफर की जाती है। यहsender,receiver,token ID, और ट्रांसफर की गईamountको लॉग करता है। - जब नए टोकन बनाए जाते हैं, तो इवेंट को ज़ीरो एड्रेस (
0x0) के रूप मेंsenderके साथ एमिट किया जाना चाहिए। - जब टोकन नष्ट (destroy) किए जाते हैं, तो टोकन हटाए जाने को इंगित करने के लिए इवेंट को ज़ीरो एड्रेस (
0x0) के रूप में रिसीवर के साथ एमिट किया जाना चाहिए।
2. OperatorSet इवेंट:
/// @notice The event emitted when an operator is set.
event OperatorSet(address indexed owner, address indexed spender, bool approved);
OperatorSet इवेंट तब एमिट किया जाता है जब भी कोई ओनर किसी अन्य एड्रेस के लिए ऑपरेटर परमिशन असाइन करता है या रद्द (revoke) करता है। यह इवेंट ओनर के एड्रेस, स्पेंडर के एड्रेस और अपडेट किए गए अप्रूवल स्टेटस (अनुमति देने के लिए true, रद्द करने के लिए false) को लॉग करता है।
3. Approval इवेंट:
/// @notice The event emitted when an approval occurs.
event Approval(address indexed owner, address indexed spender, uint256 indexed id, uint256 amount);
Approval इवेंट को तब एमिट किया जाना चाहिए जब कोई ओनर अपनी ओर से किसी दिए गए टोकन आईडी की विशिष्ट राशि को ट्रांसफर करने के लिए किसी स्पेंडर के लिए अप्रूवल सेट या अपडेट करता है। यह इवेंट owner, spender, टोकन id, और अप्रूव्ड amount को लॉग करता है।
अब जब हमने ERC-6909 और ERC-1155 के बीच के अंतरों के साथ-साथ ERC-6909 में मुख्य विधियों (methods) और इवेंट्स का पता लगा लिया है, तो आइए स्टैंडर्ड के कुछ वास्तविक जीवन के उपयोगों (real-life uses) की जांच करें।
Uniswap v4 PoolManager ERC-6909 को कैसे लागू करता है।
Uniswap v3 में, फैक्ट्री/पूल मॉडल UniswapV3Factory कॉन्ट्रैक्ट का उपयोग करके प्रत्येक पूल के लिए एक अलग कॉन्ट्रैक्ट डिप्लॉय करके नए टोकन पेयर बनाता है। यह विधि गैस की लागत बढ़ाती है क्योंकि प्रत्येक नए पूल के लिए एक नए कॉन्ट्रैक्ट डिप्लॉयमेंट की आवश्यकता होती है।
इसके विपरीत, Uniswap v4 एक सिंगलटन (singleton) कॉन्ट्रैक्ट (PoolManager.sol) पेश करता है, जो अलग-अलग कॉन्ट्रैक्ट डिप्लॉयमेंट की आवश्यकता के बजाय सभी लिक्विडिटी पूल को अपने आंतरिक स्टेट (internal state) के हिस्से के रूप में प्रबंधित करता है। यह डिज़ाइन पूल निर्माण के लिए गैस की लागत को काफी कम कर देता है।
इसके अलावा, पिछले संस्करणों में कई Uniswap पूल को शामिल करने वाले ट्रांजेक्शन के लिए कई कॉन्ट्रैक्ट्स में टोकन ट्रांसफर और रिडंडेंट स्टेट अपडेट की आवश्यकता होती थी। Uniswap v4 में, पूलों के अंदर और बाहर ERC-20 टोकन ट्रांसफर करने के बजाय, PoolManager कॉन्ट्रैक्ट उपयोगकर्ताओं के ERC-20 टोकन के ERC-6909 रिप्रेजेंटेशन्स को केंद्रीय रूप से होल्ड कर सकता है।
उदाहरण के लिए, यदि कोई उपयोगकर्ता टोकन A के लिए लिक्विडिटी प्रदान करता है, तो वे बाद में अपना हिस्सा निकालने का विकल्प चुन सकते हैं और टोकन A को अपने वॉलेट में ERC-20 ट्रांसफर के रूप में प्राप्त कर सकते हैं। हालाँकि, यदि वे अपने टोकन को प्रोटोकॉल के अंदर छोड़ने का विकल्प चुनते हैं, तो Uniswap v4 PoolManager कॉन्ट्रैक्ट से ERC-20 टोकन बाहर ट्रांसफर करने के बजाय LP को उनके टोकन बैलेंस के ERC-6909 रिप्रेजेंटेशन्स को मिंट (mint) कर सकता है — जिससे क्रॉस-कॉन्ट्रैक्ट कॉल की बचत होती है। ये ERC-6909 बैलेंस उपयोगकर्ताओं को वॉलेट के बीच टोकन ले जाने की आवश्यकता के बिना प्रोटोकॉल के भीतर ट्रेड करने या इंटरैक्ट करने की अनुमति देते हैं।
इसका मतलब है कि जब उपयोगकर्ता बाद में टोकन B के लिए टोकन A का ट्रेड करता है, तो उनके वॉलेट से ERC-20 टोकन ट्रांसफर करने के बजाय, Uniswap बस पूल के भीतर उनके ERC-6909 बैलेंस को अपडेट कर देता है।
नोट: Uniswap V4 में ERC-6909 का उपयोग LP टोकन के रूप में नहीं किया जाता है।
सिंगलटन डेफी आर्किटेक्चर और NFT कलेक्शन्स के लिए ERC-6909 मेटाडेटा संबंधी विचार
यहाँ IERC6909Metadata इंटरफ़ेस है जो परिभाषित करता है कि ERC-6909 स्टैंडर्ड मेटाडेटा, जैसे नाम, प्रतीक (symbols), और डेसीमल्स (decimals) को व्यक्तिगत टोकन के साथ कैसे जोड़ा जा सकता है।
ध्यान दें कि नीचे दिए गए name, symbol, और decimals फ़ंक्शन id के आधार पर बदल सकते हैं, जिससे ERC-6909 के भीतर अलग-अलग टोकन के अलग-अलग नाम, प्रतीक और डेसीमल्स हो सकते हैं।
/// @notice Contains metadata about individual tokens.
interface IERC6909Metadata is IERC6909 {
/// @notice Name of a given token.
/// @param id The id of the token.
/// @return name The name of the token.
function name(uint256 id) external view returns (string memory);
/// @notice Symbol of a given token.
/// @param id The id of the token.
/// @return symbol The symbol of the token.
function symbol(uint256 id) external view returns (string memory);
/// @notice Decimals of a given token.
/// @param id The id of the token.
/// @return decimals The decimals of the token.
function decimals(uint256 id) external view returns (uint8);
}
एक DeFi प्रोटोकॉल के लिए, हमारे पास कई LP टोकन हो सकते हैं, और हम उन्हें इस तरह मानकीकृत (standardize) करना चाह सकते हैं कि उन सभी में डेसीमल्स की संख्या समान हो, जैसे 18। हालाँकि, हम यह चाह सकते हैं कि नाम और प्रतीक पूल द्वारा रखी गई विभिन्न संपत्तियों (assets) को दर्शाएं।
इसके विपरीत, NFTs के लिए, decimals मान हमेशा 1 पर सेट होना चाहिए, क्योंकि NFTs अविभाज्य (indivisible) हैं।
एक विशिष्ट NFT कलेक्शन (उदा., ERC-721) में, सभी टोकन एक संपूर्ण कलेक्शन का प्रतिनिधित्व करने के लिए एक ही नाम और प्रतीक साझा करते हैं (उदा., “PUNK” प्रतीक के साथ “CryptoPunks”)। ERC-6909 हमें ERC-712 कन्वेंशन का पालन करने की अनुमति देता है, जहां एक कलेक्शन के सभी NFT एक ही मेटाडेटा साझा करते हैं।
ERC-6909 नॉन-फंजीबल टोकन का इम्प्लीमेंटेशन उदाहरण।
ERC-6909 स्पेसिफिकेशन स्पष्ट रूप से नॉन-फंजीबल टोकन का समर्थन करने के लिए किसी अनूठे दृष्टिकोण को परिभाषित नहीं करता है। हालाँकि, ERC-1155 स्पेसिफिकेशन में वर्णित ID बिट्स-स्प्लिटिंग तकनीक का उपयोग ERC-6909 में नॉन-फंजीबल टोकन को लागू करने के लिए किया जा सकता है। यह दृष्टिकोण बिट-शिफ्टिंग और एडिशन ऑपरेशन्स का उपयोग करके कलेक्शन ID और आइटम ID को एक सिंगल uint256 टोकन ID में एनकोड (encode) करता है।
function getTokenId(uint256 collectionId, uint256 itemId) public pure returns (uint256) {
return (collectionId << 128) + itemId;
}
नीचे दिया गया ERC6909MultiCollectionNFT कॉन्ट्रैक्ट एक नॉन-फंजीबल टोकन (NFT) इम्प्लीमेंटेशन का एक उदाहरण है जो collectionId और itemId से एक टोकन ID जनरेट करने के लिए getTokenId का उपयोग करता है।
mintNFT फ़ंक्शन यह सुनिश्चित करता है कि एड्रेस की परवाह किए बिना प्रत्येक tokenId को केवल एक बार मिंट किया जा सकता है। यह mintedTokens मैपिंग का उपयोग करके यह ट्रैक करता है कि क्या किसी NFT tokenId को विश्व स्तर पर मिंट किया गया है।
चूंकि mintNFT में amount वेरिएबल 1 पर सेट है, इसलिए फ़ंक्शन में _mint(to, tokenId, amount) कॉल एक tokenid के लिए केवल एक प्रति (copy) मिंट करेगा। किसी भी स्थिति में जहां amount > 1 है, टोकन नॉन-फंजीबल के बजाय फंजीबल बन जाएगा।
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./ERC6909.sol";
contract ERC6909MultiCollectionNFT is ERC6909 {
struct NFT {
string uri;
}
mapping(uint256 => NFT) private _tokens;
mapping(uint256 => string) private _collectionURIs;
mapping(uint256 => bool) public mintedTokens;
event MintedNFT(address indexed to, uint256 indexed collectionId, uint256 indexed itemId, uint256 tokenId, string uri);
// Compute Token ID by concatenating collectionId and itemId
function getTokenId(uint256 collectionId, uint256 itemId) public pure returns (uint256) {
return (collectionId << 128) + itemId;
}
function _mint(address to, uint256 tokenId, uint256 amount) internal {
balanceOf[to][tokenId] += amount;
emit Transfer(msg.sender, address(0), to, tokenId, amount);
}
function mintNFT(address to, uint256 collectionId, uint256 itemId, string memory uri) external {
uint256 amount = 1;
uint256 tokenId = getTokenId(collectionId, itemId);
require(!mintedTokens[tokenId], "ERC6909MultiCollectionNFT: Token already minted");
require(amount == 1, "ERC6909MultiCollectionNFT: Token copies must be 1");
_tokens[tokenId] = NFT(uri);
mintedTokens[tokenId] = true; // Mark as minted
_mint(to, tokenId, amount); // amount is defined as 1.
emit MintedNFT(to, collectionId, itemId, tokenId, uri);
}
function nftBalanceOf(address owner, uint256 tokenId) public view returns (uint256) {
return balanceOf[owner][tokenId];
}
}
याद रखें कि ऊपर mintNFT में _mint कॉल balanceOf मैपिंग को 1 पर अपडेट करता है, क्योंकि मिंट्स पूरी तरह से नॉन-फंजीबल हैं। इसलिए, इस कॉन्ट्रैक्ट में nftBalanceOf फ़ंक्शन से हमेशा 1 लौटाने की उम्मीद की जाती है, यदि owner एड्रेस ने वास्तव में tokenId मिंट किया है।
टोकन ओनरशिप को ट्रांसफर करने के लिए, नीचे दिया गया nftTransfer फ़ंक्शन यह सुनिश्चित करता है कि केवल NFT ओनर ही ट्रांसफर शुरू कर सकता है, इसके लिए यह एकमात्र मौजूदा यूनिट को ट्रांसफर करने की अनुमति देने से पहले उनके बैलेंस को वेरिफाई करता है।
function nftTransfer(address to, uint256 tokenId) external {
require(balanceOf[tokenId][msg.sender] == 1, "ERC6909MultiCollectionNFT: This should be non-fungible.");
require(to != address(0), "ERC6909MultiCollectionNFT: transfer to zero address");
transfer(to, tokenId, 1);
// the amount in this case is equal to 1.
emit Transfer(msg.sender, address(0), to, tokenId, 1);
}
ERC-6909 कंटेंट URI एक्सटेंशन्स और मेटाडेटा URI JSON स्कीमा
ERC-6909 में मेटाडेटा एक्सेस को मानकीकृत करने के लिए, वैकल्पिक IERC6909ContentURI इंटरफ़ेस कॉन्ट्रैक्ट और टोकन स्तरों पर मेटाडेटा पुनर्प्राप्त (retrieve) करने के लिए दो URI फ़ंक्शन्स (contractURI और tokenURI) को परिभाषित करता है। ERC-6909 स्टैंडर्ड यह अनिवार्य नहीं करता है कि टोकन के साथ URI मेटाडेटा जुड़ा हो। हालाँकि, यदि किसी इम्प्लीमेंटेशन में ये URI फ़ंक्शन्स शामिल हैं, तो लौटाए गए URI को उन JSON फ़ाइलों को इंगित (point) करना चाहिए जो ERC-6909 मेटाडेटा URI JSON स्कीमा के अनुरूप हों।
pragma solidity ^0.8.19;
import "./IERC6909.sol";
/// @title ERC6909 Content URI Interface
interface IERC6909ContentURI is IERC6909 {
/// @notice Contract level URI
/// @return uri The contract level URI.
function contractURI() external view returns (string memory);
/// @notice Token level URI
/// @param id The id of the token.
/// @return uri The token level URI.
function tokenURI(uint256 id) external view returns (string memory);
}
जैसा कि ऊपर दिए गए कोड में दिखाया गया है, ERC-6909 IERC6909ContentURI इंटरफ़ेस दो वैकल्पिक URI फ़ंक्शन्स को परिभाषित करता है, अर्थात् contractURI और tokenURI; प्रत्येक अपने संबंधित URI JSON स्कीमा के साथ।
contractURI फ़ंक्शन (कोई तर्क या आर्ग्युमेंट्स नहीं लेता है) कॉन्ट्रैक्ट-लेवल मेटाडेटा की ओर इशारा करते हुए एक सिंगल URI लौटाता है, जबकि tokenURI() प्रत्येक टोकन आईडी के लिए एक विशिष्ट URI लौटाता है।
नीचे एक उदाहरण दिया गया है कि कॉन्ट्रैक्ट URI JSON स्कीमा को कैसे संरचित (structure) किया जाए, जैसा कि ERC-6909 स्टैंडर्ड द्वारा निर्दिष्ट किया गया है।
{
"title": "Contract Metadata",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the contract."
},
"description": {
"type": "string",
"description": "The description of the contract."
},
"image_url": {
"type": "string",
"format": "uri",
"description": "The URL of the image representing the contract."
},
"banner_image_url": {
"type": "string",
"format": "uri",
"description": "The URL of the banner image of the contract."
},
"external_link": {
"type": "string",
"format": "uri",
"description": "The external link of the contract."
},
"editors": {
"type": "array",
"items": {
"type": "string",
"description": "An Ethereum address representing an authorized editor of the contract."
},
"description": "An array of Ethereum addresses representing editors (authorized editors) of the contract."
},
"animation_url": {
"type": "string",
"description": "An animation URL for the contract."
}
},
"required": ["name"]
}
दूसरी ओर, tokenURI फ़ंक्शन एक टोकन का uint256 पैरामीटर id लेता है और टोकन URI लौटाता है। यदि टोकन id मौजूद नहीं है तो फ़ंक्शन रिवर्ट (revert) कर सकता है (MAY revert)। कॉन्ट्रैक्ट के साथ इंटरैक्ट करने वाले क्लाइंट्स को उस टोकन से जुड़े सही मेटाडेटा को एक्सेस करने के लिए URI में {id} की प्रत्येक घटना (occurrence) को वास्तविक टोकन ID से अवश्य बदलना चाहिए (MUST replace)।
नीचे tokenURI फ़ंक्शन का इम्प्लीमेंटेशन है, जो एक स्टेटिक URI टेम्पलेट (static URI template) लौटाता है जो प्लेसहोल्डर प्रारूप का अनुसरण करता है:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "./ERC6909.sol";
import "./interfaces/IERC6909ContentURI.sol";
contract ERC6909ContentURI is ERC6909, IERC6909ContentURI {
/// @notice The contract level URI.
string public contractURI;
/// @notice The URI for each id.
/// @return The URI of the token.
function tokenURI(uint256) public pure override returns (string memory) {
return "<baseuri>/{id}";
}
}
यहाँ एक उदाहरण दिया गया है कि URI JSON स्कीमा को कैसे संरचित किया जाए, जैसा कि ERC-6909 स्टैंडर्ड द्वारा निर्दिष्ट किया गया है।
{
"title": "Asset Metadata",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Identifies the token"
},
"description": {
"type": "string",
"description": "Describes the token"
},
"image": {
"type": "string",
"description": "A URI pointing to an image resource."
},
"animation_url": {
"type": "string",
"description": "An animation URL for the token."
}
},
"required": ["name", "description", "image"]
}
ERC-6909 स्पेसिफिकेशन में अलाउंस और ऑपरेटर एम्बिगुइटी (अस्पष्टता)।
एक ऐसे परिदृश्य पर विचार करें जहां एक अकाउंट (A) किसी अन्य अकाउंट (B) को ऑपरेटर परमिशन देता है और B के लिए एक विशिष्ट मात्रा में टोकन ट्रांसफर करने के लिए एक अलाउंस भी सेट करता है।
यदि B, A की ओर से ट्रांसफर शुरू करता है, तो इम्प्लीमेंटेशन को चेक्स (checks) के सही क्रम को निर्धारित करना चाहिए और यह तय करना चाहिए कि अलाउंस ऑपरेटर परमिशन के साथ कैसे इंटरैक्ट करते हैं।
यह अस्पष्टता (ambiguity) चेक्स के क्रम से संबंधित है। क्या कॉन्ट्रैक्ट को:
- पहले अलाउंस की जांच करनी चाहिए, यदि यह अपर्याप्त है तो रिवर्ट (revert) करना चाहिए, भले ही B के पास ऑपरेटर अनुमतियां हों।
- पहले ऑपरेटर परमिशन की जांच करनी चाहिए, अलाउंस की परवाह किए बिना ट्रांसफर को आगे बढ़ने की अनुमति देनी चाहिए।
नीचे दिए गए allowanceFirst कॉन्ट्रैक्ट में, यदि अकाउंट B के पास ऑपरेटर परमिशन है लेकिन अपर्याप्त अलाउंस है, तो अलाउंस चेक विफल हो जाएगा, जिससे ट्रांजेक्शन रिवर्ट हो जाएगा। यह काउंटरइंट्यूटिव (counterintuitive) हो सकता है क्योंकि ऑपरेटर परमिशन का आमतौर पर अर्थ अप्रतिबंधित एक्सेस होता है, और उपयोगकर्ता ट्रांजेक्शन सफल होने की उम्मीद कर सकते हैं।
इसके विपरीत, operatorFirst कॉन्ट्रैक्ट में, यदि इम्प्लीमेंटेशन पहले ऑपरेटर परमिशन की जांच करता है, तो यह अलाउंस चेक को बायपास कर देगा, और ऑपरेटर के अप्रतिबंधित एक्सेस के आधार पर ट्रांजेक्शन सफल हो जाएगा।
contract operatorFirst {
function transferFrom(address sender, address receiver, uint256 id, uint256 amount) public {
// check if `isOperator` first
if (msg.sender != sender && !isOperator[sender][msg.sender]) {
require(allowance[sender][msg.sender][id] >= amount, "insufficient allowance");
allowance[sender][msg.sender][id] -= amount;
}
// -- snip --
}
}
contract allowanceFirst{
function transferFrom(address sender, address receiver, uint256 id, uint256 amount) public {
// check if allowance is sufficient first
if (msg.sender != sender && allowance[sender][msg.sender][id] < amount) {
require(isOperator[sender][msg.sender], "insufficient allowance");
}
// ERROR: when allowance is insufficient, this panics due to arithmetic underflow, regardless of
// whether the caller has operator permissions.
allowance[sender][msg.sender][id] -= amount;
// -- snip --
}
}
यह स्टैंडर्ड जानबूझकर परमिशन चेक पर निर्णय को अनियंत्रित (unconstrained) छोड़ देता है, जिससे इम्प्लीमेंटर्स को चुनने का लचीलापन (flexibility) मिलता है। जब किसी अकाउंट में ऑपरेटर परमिशन और अपर्याप्त अलाउंस दोनों होते हैं, तो ट्रांसफर का व्यवहार उस क्रम पर निर्भर करता है जिसमें चेक्स किए जाते हैं।
निष्कर्ष
ERC-6909 स्टैंडर्ड ट्रांसफर फ़ंक्शन्स में बैचिंग और अनिवार्य कॉलबैक को हटाकर ERC-1155 की एफिशिएंसी (efficiency) में काफी सुधार करता है। बैचिंग को हटाने से केस-बाय-केस ऑप्टिमाइज़ेशन की अनुमति मिलती है, विशेष रूप से रोलअप (rollups) या गैस-सेंसिटिव (gas-sensitive) वातावरण के लिए।
इसने हाइब्रिड ऑपरेटर परमिशन स्कीम के माध्यम से टोकन अप्रूवल का स्केलेबल कंट्रोल भी पेश किया।
आभार
हम इस आर्टिकल की समीक्षा करने के लिए vectorized, jtriley, और neodaoist (ERC-6909 के सह-लेखकों) को धन्यवाद देना चाहते हैं।