ERC20 Snapshot डबल वोटिंग (double voting) की समस्या को हल करता है।
यदि वोटों का वजन इस बात से तय होता है कि किसी के पास कितने टोकन हैं, तो एक दुर्भावनापूर्ण उपयोगकर्ता वोट देने के लिए अपने टोकन का उपयोग कर सकता है, फिर उन टोकन को किसी अन्य एड्रेस पर ट्रांसफर कर सकता है, उससे वोट कर सकता है, और ऐसा ही चलता रहता है। यदि प्रत्येक एड्रेस एक स्मार्ट कॉन्ट्रैक्ट है, तो हैकर एक ही ट्रांजैक्शन में ये सभी वोट डाल सकता है। इससे जुड़ा एक हमला यह है कि बहुत सारे गवर्नेंस टोकन प्राप्त करने के लिए flash loan का उपयोग किया जाए, वोट किया जाए, और फिर फ्लैश लोन को वापस कर दिया जाए।
airdrops क्लेम करने में भी इसी तरह की समस्या मौजूद है। कोई व्यक्ति airdrop क्लेम करने के लिए अपने ERC20 टोकन का उपयोग कर सकता है, फिर अपने टोकन को दूसरे एड्रेस पर ट्रांसफर कर सकता है, और फिर से airdrop क्लेम कर सकता है।
मूल रूप से, ERC20 snapshot एक ऐसा तंत्र (mechanism) प्रदान करता है जो उपयोगकर्ताओं द्वारा उसी ट्रांजैक्शन में टोकन ट्रांसफर करने और टोकन यूटिलिटी का फिर से उपयोग करने से बचाव करता है।
पहली नज़र में, स्नैपशॉट लेना (snapshotting) एक कठिन समस्या लग सकती है। इसका ब्रूट फोर्स (brute force), या साधारण समाधान यह है कि ERC20 की “balances” मैपिंग में प्रत्येक एड्रेस को इटरेट (iterate) किया जाए, और फिर इन्हें दूसरी मैपिंग में कॉपी किया जाए। ERC20 में मूल रूप से मैपिंग को इटरेट करना संभव नहीं है, इसलिए कोडर को enumerable map का उपयोग करना होगा — एक ऐसा मैप जिसमें एक ऐरे (array) होता है जो सभी कीज़ (keys) का ट्रैक रखता है।
जैसा कि कोई कल्पना कर सकता है, यह O(n) ऑपरेशन गैस के मामले में बहुत अधिक खर्चीला होगा।
कंप्यूटर विज्ञान में एक कहावत है कि “कंप्यूटर विज्ञान की हर समस्या को एक और स्तर के इंडायरेक्शन (indirection) के साथ हल किया जा सकता है” और ERC20 snapshots इसे इसी तरह हल करता है।
कुशल लेकिन साधारण समाधान
आइए balances मैपिंग का उदाहरण लें। यहाँ एक बग वाला (buggy) solidity समाधान दिया गया है, लेकिन यह सही दिशा में एक कदम है।
balances[snapshotNumber][user]
इस मामले में, snapshotNumber एक काउंटर है जो शून्य से शुरू होता है और हर बार स्नैपशॉट पूरा होने पर एक से बढ़ जाता है।
हमारे वोटिंग वाले उदाहरण पर वापस लौटते हैं, हम एक विशेष समय पर एक स्नैपशॉट बनाते हैं, सभी को अपना काम करने देते हैं, फिर एक और स्नैपशॉट बनाते हैं। वोटिंग के समय, हम पिछले स्नैपशॉट का उपयोग करते हैं क्योंकि टोकन ट्रांसफर करके वर्तमान स्नैपशॉट को अभी भी बदला जा सकता है।
इस तरह, जिस स्नैपशॉट की हमें परवाह है, वहां snapshotNumber और उनके एड्रेस दोनों को प्रदान करके हम किसी का बैलेंस चेक (query) कर सकते हैं। चूंकि हम वर्तमान स्नैपशॉट को जानते हैं, इसलिए balanceOf केवल सबसे हालिया स्नैपशॉट पर बैलेंस है।
आह, लेकिन यहाँ एक समस्या है! हर बार जब हम स्नैपशॉट लेते हैं, तो हर किसी का बैलेंस शून्य पर सेट हो जाता है! इसे कुछ अकाउंटिंग के साथ हल करना संभव है — बस उस अंतिम स्नैपशॉट को ट्रैक करें जिस पर उपयोगकर्ता ने ट्रांजैक्शन किया था, लेकिन जैसे ही इंजीनियर सभी कॉर्नर केस (corner cases) को कवर करने का प्रयास करता है, यह बहुत जल्दी जटिल हो जाता है।
OpenZeppelin समाधान
OpenZeppelin इसे इस प्रकार पूरा करता है। कोड
प्रत्येक बैलेंस एक struct को स्टोर करता है
struct Snapshots {
uint256[] ids;
uint256[] values;
}
mapping(address => Snapshots) private _accountBalanceSnapshots;
function balanceOfAt(address account, uint256 snapshotId) public view virtual returns (uint256) {
(bool snapshotted, uint256 value) = _valueAt(snapshotId, _accountBalanceSnapshots[account]);
return snapshotted ? value : balanceOf(account);
}
उपयोगकर्ता के बैलेंस के अंदर, हम एक struct स्टोर करते हैं जिसमें ids और values का एक ऐरे (array) होता है। ids का ऐरे एक मोनोटोनिक रूप से बढ़ने वाली (monotonically increasing) स्नैपशॉट आईडी है, और values वह बैलेंस हैं जब वह आईडी सक्रिय स्नैपशॉट थी।
स्नैपशॉट लेना
यहाँ स्नैपशॉट फंक्शन दिया गया है। यह केवल वर्तमान स्नैपशॉट आईडी को बढ़ा देता है।
function _snapshot() internal virtual returns (uint256) {
_currentSnapshotId.increment();
uint256 currentId = _getCurrentSnapshotId();
emit Snapshot(currentId);
return currentId;
}
जब कोई उपयोगकर्ता नए स्नैपशॉट में ट्रांसफर करता है, तो _beforeTokenTransfer हुक (hook) को कॉल किया जाता है, जिसमें निम्नलिखित कोड होता है।
प्राप्तकर्ता और प्रेषक दोनों के लिए _updateAccountSnapshot को कॉल किया जाता है।
// Update balance and/or total supply snapshots before the values are modified. This is implemented
// in the _beforeTokenTransfer hook, which is executed for _mint, _burn, and _transfer operations.
function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override {
super._beforeTokenTransfer(from, to, amount);
if (from == address(0)) {
// mint
_updateAccountSnapshot(to);
_updateTotalSupplySnapshot();
} else if (to == address(0)) {
// burn
_updateAccountSnapshot(from);
_updateTotalSupplySnapshot();
} else {
// transfer
_updateAccountSnapshot(from);
_updateAccountSnapshot(to);
}
}
यह वह _updateAccountSnapshot फंक्शन है जिसे कॉल किया जाता है
function _updateAccountSnapshot(address account) private {
_updateSnapshot(_accountBalanceSnapshots[account], balanceOf(account));
}
जो बदले में _updateSnapshot को कॉल करता है। इसकी परिभाषा नीचे दी गई है
function _updateSnapshot(Snapshots storage snapshots, uint256 currentValue) private {
uint256 currentId = _getCurrentSnapshotId();
if (_lastSnapshotId(snapshots.ids) < currentId) {
snapshots.ids.push(currentId);
snapshots.values.push(currentValue);
}
}
क्योंकि currentId अभी-अभी बढ़ाया गया था, इसलिए if स्टेटमेंट सत्य (true) होगा। snapshots ऐरे के अंदर, वर्तमान बैलेंस जुड़ (append) जाएगा। क्योंकि इसे _beforeTokenTransfer हुक में कॉल किया गया था, यह बैलेंस के बदलने से पहले वाले बैलेंस को दर्शाता है।
इसलिए, एक बार स्नैपशॉट आईडी बढ़ने के बाद, स्नैपशॉट ट्रांजैक्शन के बाद होने वाले कोई भी ट्रांसफर, ट्रांजैक्शन होने से पहले के बैलेंस को ऐरे में स्टोर करेंगे। यह प्रभावी रूप से सभी के वर्तमान बैलेंस को “फ़्रीज़ (freezes)” कर देता है क्योंकि स्नैपशॉट के बाद होने वाले किसी भी ट्रांसफर के कारण “पुराना” मान स्टोर हो जाता है। क्या होगा यदि दो स्नैपशॉट होते हैं, लेकिन कोई एड्रेस उन स्नैपशॉट के दौरान ट्रांजैक्शन नहीं करता है? उस स्थिति में, स्नैपशॉट आईडी सन्निहित (contiguous) नहीं होंगी।
इस कारण से, हम “ids[snapshotId]” करके किसी स्नैपशॉट पर अकाउंट के बैलेंस को एक्सेस नहीं कर सकते हैं। इसके बजाय, उपयोगकर्ता जिस स्नैपशॉट आईडी का अनुरोध कर रहा है उसे खोजने के लिए binary search का उपयोग किया जाता है। यदि आईडी नहीं मिलती है, तो हम पिछले आसन्न (adjacent) स्नैपशॉट वैल्यू का उपयोग करते हैं। उदाहरण के लिए, यदि हम स्नैपशॉट 5 पर किसी उपयोगकर्ता का बैलेंस जानना चाहते हैं, लेकिन उन्होंने स्नैपशॉट 3 और 4 के दौरान टोकन ट्रांसफर नहीं किए, तो हम स्नैपशॉट 2 को देखेंगे।
Total supply को भी इसी तरह ट्रैक किया जाता है
पाठक ध्यान दे सकते हैं कि struct Snapshots में ids और values जैसे ओवरजेनेरिक (overgeneric) वेरिएबल नाम प्रतीत होते हैं। क्या इसे अधिक सटीक होने के लिए बस “balance” नाम नहीं दिया जाना चाहिए?
ERC20 Snapshot उसी रणनीति का उपयोग करके total supply को ट्रैक करता है, इसलिए वेरिएबल के नाम इस तथ्य को दर्शाते हैं कि एक ही struct का उपयोग उपयोगकर्ता के बैलेंस और total supply दोनों को ट्रैक करने के लिए किया जाता है।
केवल mint और burn ही total supply को बदलते हैं, इसलिए जब इन फंक्शन्स को कॉल किया जाता है, तो इन मानों को अपडेट करने से पहले total supply को स्टोर करने वाले struct की जाँच की जाती है ताकि यह देखा जा सके कि क्या स्नैपशॉट बदल गया है।
ध्यान दें कि ऐतिहासिक allowance मानों का स्नैपशॉट नहीं लिया जाता है।
अतिरिक्त गैस लागत
नियमित ट्रांसफर अधिक महंगे होते हैं क्योंकि हम जाँचते हैं कि उपयोगकर्ता के ids में अंतिम आईडी वर्तमान स्नैपशॉट से मेल खाती है या नहीं और यदि ऐसा नहीं है तो एक नई आईडी जोड़ते हैं। ids और values के ऐरे में जोड़ने पर दो अतिरिक्त SSTOREs खर्च होंगे। जब एक नया स्नैपशॉट होता है, तो किसी एड्रेस पर या वहां से पहला ट्रांसफर अधिक महंगा होगा। लेकिन दूसरे ट्रांजैक्शन की लागत लगभग नियमित ERC20 टोकन में ट्रांसफर के समान ही होगी।
हैक होना
यदि कोई फ्लैश लोन (flash loan) लेता है और एक ही ट्रांजैक्शन में एक स्नैपशॉट बनाता है, तो वे कृत्रिम रूप से अपनी वोटिंग पावर (voting power) बढ़ा सकते हैं। यदि टोकन कम ब्याज दर पर उधार लिए जा सकते हैं, और एक हमलावर जानता है कि अगला स्नैपशॉट कब होगा, तो वे ऐसा ही कुछ पूरा करने के लिए स्नैपशॉट से ठीक पहले टोकन उधार ले सकते हैं। हालाँकि, वोटिंग पावर बढ़ाने के लिए फ्लैश लोन एक व्यवहार्य तरीका नहीं होगा, क्योंकि उन्हें एक अलग स्नैपशॉट ट्रांजैक्शन के दौरान उच्च बैलेंस बनाए रखने की आवश्यकता होगी।
वोटों की गिनती
यह बस किसी विशेष स्नैपशॉट पर एक एड्रेस का बैलेंस है जिसे total supply से विभाजित किया गया है।
मूल रूप से 22 फरवरी, 2023 को प्रकाशित