एक “rebase token” (या कभी-कभी “rebasing token”) एक ऐसा ERC-20 token होता है, जिसमें कुल सप्लाई और टोकन होल्डर्स का बैलेंस बिना किसी transfer, minting या burning के बदल सकता है।
DeFi प्रोटोकॉल अक्सर डिपॉजिटर को देय एसेट की मात्रा (प्रोटोकॉल द्वारा कमाए गए मुनाफे सहित) को ट्रैक करने के लिए rebasing tokens का उपयोग करते हैं। उदाहरण के लिए, यदि प्रोटोकॉल पर किसी डिपॉजिटर का 10 ETH (मुनाफे सहित) बकाया है, तो rebasing token के लिए उस डिपॉजिटर का ERC-20 बैलेंस 10e18 होगा। यदि उनके डिपॉजिट की वैल्यू बढ़कर 11 ETH हो जाती है, तो उनका बैलेंस “rebase” होकर 11e18 हो जाएगा।
यह लेख बताता है कि rebasing token को कैसे कोड किया जाए, और उस कोड के पीछे का लॉजिक क्या है।
हम उन संभावित सुरक्षा समस्याओं (security issues) को भी कवर करेंगे जो rebasing token बनाते समय उत्पन्न हो सकती हैं।
Rebase token ट्रांजेक्शन के उदाहरण
इस उदाहरण पर विचार करें जो rebasing tokens को समझाता है:
- Alice एक पूल में 100 ETH डिपॉजिट करती है। पूल उसे 100 rbLP (rebasing LP token) मिंट करके देता है।
- Alice इन 100 rbLP को बर्न करके अपने 100 ETH वापस प्राप्त कर सकती है। उसके rbLP का बैलेंस उस ETH की मात्रा को दर्शाता है जिसे वह पूल से रिडीम कर सकती है।
- लेकिन मान लीजिए कि पूल लेंडिंग फीस आदि से 10% का मुनाफा कमाता है। तो उसका 100 rbLP बैलेंस अपने आप “rebase” होकर 110 rbLP हो जाएगा।
- यानी, अगर हम उसके डिपॉजिट करने के तुरंत बाद
rbLP.balanceOf(alice)को कॉल करते हैं, तो यह 100 (18 डेसीमल के साथ) रिटर्न करेगा। - मुनाफा होने के बाद,
rbLP.balanceOf(alice)110 रिटर्न करेगा।
- यानी, अगर हम उसके डिपॉजिट करने के तुरंत बाद
- अब पूल के मुनाफा कमाने के बाद, Bob 100 ETH पूल में डिपॉजिट करता है। पूल उसे 100 rbLP मिंट करके देगा। हालाँकि, Alice के पास 110 rbLP हैं क्योंकि वह पूल के 10% मुनाफा कमाने से पहले ही लिक्विडिटी सप्लाई कर रही थी।
एक rebase token की कुल सप्लाई को पूल द्वारा होल्ड किए गए ETH की कुल मात्रा के बराबर रखने का प्रयास किया जाता है। असल में, राउंडिंग एरर (rounding errors) के कारण rebase tokens की कुल सप्लाई की तुलना में ETH थोड़ा अधिक हो सकता है — हम इसके बारे में बाद में चर्चा करेंगे।
किसी यूज़र के पास मौजूद rbLP token का बैलेंस वह ETH की मात्रा है जिसे वे पूल से रिडीम कर सकते हैं। इसलिए, किसी यूज़र के बैलेंस को rebasing token की कुल सप्लाई (या दूसरे शब्दों में, पूल द्वारा होल्ड किए गए कुल ETH) में उनके “हिस्से” (share) के रूप में समझा जा सकता है।
हम इस लेख के बाकी हिस्से में डिपॉजिट की गई एसेट को ETH कहेंगे, लेकिन यह कोई अन्य ERC-20 token भी हो सकता है।
Rebasing token डिज़ाइन करना
हम एक rebasing ERC-20 token बनाएंगे। ERC-20 token कॉन्ट्रैक्ट की कुल सप्लाई टोकन द्वारा होल्ड किए गए Ether की मात्रा है (जिसका मतलब है कि हमारे rebasing token में 18 डेसीमल हैं)। हम कभी-कभी “टोकन” को “पूल” के रूप में भी संदर्भित करेंगे। आप इसे एक ऐसे पूल के रूप में सोच सकते हैं जो ERC-20 स्टैण्डर्ड (लेकिन rebasing के साथ) को लागू करता है ताकि यह ट्रैक किया जा सके कि किसे कितना ETH देना है।
यह डिज़ाइन काफी हद तक Lido stETH token से प्रेरित है।
balanceOf()
एक पारंपरिक ERC-20 में, यूज़र का बैलेंस mapping(address => uint256) में एड्रेस से जुड़ा एक साधारण नंबर होता है।
एक rebasing ERC-20 token में, मैपिंग में मौजूद वैल्यू पूल में यूज़र के आंशिक स्वामित्व (fractional ownership) को दर्शाती है। मैपिंग को “शेयर्स” होल्ड करने वाले के रूप में सोचना सबसे अच्छा है।
mapping(address => uint256) internal _shareBalance;
कुल सप्लाई के हिस्से (fraction) को _shareBalance[user] / _totalShares के रूप में कैलकुलेट किया जा सकता है*, जहाँ* _totalShares सभी यूज़र्स के शेयर्स का योग (sum) है।
मान लीजिए कि Alice के पास पूल के 70% ETH का स्वामित्व है, और Bob के पास 30% ETH का स्वामित्व है। शेयर्स का एक वैध वितरण इस प्रकार हो सकता है:
- Alice: 70 शेयर्स
- Bob: 30 शेयर्स
लेकिन हम केवल शेयर्स को स्वामित्व के अनुपात (ratio) के रूप में महत्व देते हैं। इसी परिदृश्य में निम्नलिखित शेयर वितरण भी मान्य होगा:
- Alice: 35 शेयर्स
- Bob: 15 शेयर्स
Rebase token का balanceOf उस ETH की मात्रा को दर्शाता है जिसे यूज़र रिडीम कर सकता है। निश्चित संख्या में शेयर्स से जितना ETH रिडीम किया जा सकता है, वह इस प्रकार है:
दूसरे उदाहरण का उपयोग करते हुए, जहाँ Alice के पास 35 शेयर्स हैं और Bob के पास 15 शेयर्स हैं, हम देख सकते हैं कि Alice पूल में मौजूद ETH का 70% रिडीम कर सकती है:
इस फॉर्मूले को Solidity में बदलने पर हमें मिलता है:
function balanceOf(address account) public view returns (uint256) {
if (_totalShares == 0) {
return 0;
}
return _shareBalance[account] * address(this).balance / _totalShares;
}
वेरिएबल _totalShares को mint() और burn() के दौरान अपडेट किया जाता है, जब पूल में ETH जोड़ा या निकाला जाता है।
mint()
mint() के दौरान, डिपॉजिटर पूल में Ether जोड़ता है और ऐसे शेयर्स की मात्रा को मिंट करता है जो बकाया शेयर्स के कुल प्रतिशत में उनके स्वामित्व प्रतिशत (अस्तित्व में मौजूद सभी शेयर्स का योग, या सभी यूज़र्स के लिए _shareBalance[user] का योग) को दर्शाता है।
यदि वे पहले minter हैं, तो मिंट किए गए शेयर्स की मात्रा सीधे तौर पर msg.value होगी। अन्यथा, उन्हें यह अनुपात बनाए रखना होगा:
इसे इस तरह से सोचा जा सकता है कि “एक शेयर से जितना बैलेंस रिडीम किया जा सकता है, वह मिंट होने से नहीं बदलता है।”
sharesToCreate को सॉल्व करने के लिए, आइए वेरिएबल्स को इस प्रकार छोटा करें:
जहाँ:
- पिछले शेयर्स हैं
- पिछला बैलेंस है
- बनाए जाने वाले (create) शेयर्स हैं, और
- ,
msg.valueहै।
हम निम्नलिखित बीजगणित (algebra) के साथ निकाल सकते हैं:
इसलिए, sharesToCreate = sharesPrevious * msg.value / balancePrevious। हालाँकि, balancePrevious ऐसी चीज़ नहीं है जिसे हम स्टोर करते हैं, लेकिन इसे address(this).balance - msg.value के रूप में कैलकुलेट किया जा सकता है। इस प्रकार, mint() के लिए हमारा कोड निम्न प्रकार है (यह कोड अभी पूरी तरह से सुरक्षित नहीं है!):
function mint(address to) external payable {
require(to != address(0), ERC20InvalidReceiver(to));
uint256 sharesToCreate;
if (_totalShares == 0) {
sharesToCreate = msg.value;
} else {
sharesToCreate = msg.value * _totalShares / (address(this).balance - msg.value);
}
_totalShares += sharesToCreate;
_shareBalance[to] += sharesToCreate;
uint256 balance = sharesToCreate * address(this).balance / _totalShares;
emit Transfer(address(0), to, balance);
}
ध्यान दें कि address(this).balance और totalShares समान प्रतिशत (percentage amount) से बढ़ते हैं। इसलिए, यह अनुपात:
मिंट के दौरान अधिकतर अपरिवर्तित रहता है। ऐसा इसलिए है क्योंकि sharesToCreate की गणना में डिवीज़न (division) शामिल है, और minter के लिए बनाए गए शेयर्स की संख्या आवश्यकता से थोड़ी कम हो सकती है, जिसका मतलब है कि उनका प्रतिशत स्वामित्व (percentage ownership) थोड़ा कम दर्शाया गया है। इसका मतलब है कि अन्य यूज़र्स के प्रतिशत स्वामित्व में थोड़ी वृद्धि हो सकती है।
हालाँकि, यदि कोई सीधे कॉन्ट्रैक्ट में ETH ट्रांसफर करता है, या कॉन्ट्रैक्ट ETH में मुनाफा कमाता है (अर्थात, किसी के मिंटिंग द्वारा नहीं), तो बैलेंस बढ़ जाएगा, लेकिन _totalShares नहीं बढ़ेगा। इससे ऊपर दिए गए फॉर्मूले में अनुपात की वैल्यू बढ़ जाएगी, जिससे बैलेंस ऊपर की ओर rebase हो जाएगा।
ध्यान रखें कि एक हमलावर (attacker) rebasing token को मिंट करने के लिए flashloan का उपयोग करके अस्थायी रूप से अपना बैलेंस बढ़ा सकता है। इसलिए, किसी भी महत्वपूर्ण बिज़नेस लॉजिक को आँख बंद करके balanceOf() या totalSupply() पर निर्भर नहीं होना चाहिए।
यह भी ध्यान देने योग्य है कि फॉर्मूले में:
sharesToCreate = msg.value * _totalShares / (address(this).balance - msg.value);
यह संभव है कि sharesToCreate राउंड डाउन होकर शून्य (zero) हो जाए यदि:
- पूल में काफी ज़्यादा ETH बैलेंस हो
- _totalShares अपेक्षाकृत कम हो (यानी प्रोटोकॉल ने बहुत मुनाफा कमाया हो)
msg.valueबहुत छोटा हो
क्योंकि यह संभव है कि शेयर्स राउंड डाउन होकर शून्य हो जाएँ, इसलिए मौजूदा इम्प्लीमेंटेशन small deposit attack के प्रति संवेदनशील है।
विशेष रूप से:
- हमलावर 1 wei मिंट करता है
- हमलावर पीड़ित (victim) के 1 ether मिंट करने को फ्रंटरन (frontrun) करते हुए पूल में 100 ether डोनेट करता है।
चरण 2 में, sharesToCreate की गणना इस प्रकार की जाएगी:
यह राउंड डाउन होकर शून्य हो जाएगा क्योंकि हर (denominator) अंश (numerator) से बड़ा है। अब पीड़ित ने 1 ether डिपॉजिट किया है, लेकिन उसे कुछ भी मिंट नहीं हुआ। हमलावर सभी शेयर्स का मालिक है, और इस प्रकार उसने पीड़ित के डिपॉजिट पर नियंत्रण कर लिया है।
इसलिए, हमारे rebase token में किसी प्रकार का स्लिपेज प्रोटेक्शन (slippage protection) लागू होना चाहिए।
हम एक “minimumShares” पैरामीटर बना सकते हैं, लेकिन यह यूज़र के लिए शेयर्स के एब्स्ट्रैक्शन (abstraction) को लीक करता है। दूसरे शब्दों में, इंटीग्रेटर्स (integrators) को अब “शेयर्स” को “बैलेंस” से अलग एक वैल्यू के रूप में सोचना होगा।
एक वैकल्पिक सुरक्षा उपाय जिसमें शेयर्स की जानकारी की आवश्यकता नहीं होती है, वह यह जाँचना है कि sharesToCreate / _totalShares का अनुपात msg.value / address(this).balance के करीब हो। यदि sharesToCreate बहुत अधिक राउंड डाउन हो जाता है, तो sharesToCreate / _totalShares का अनुपात कुल बैलेंस के सापेक्ष डिपॉजिट किए गए ether के अनुपात से बहुत कम होगा।
चूँकि sharesToCreate थोड़ा राउंड डाउन होता है, हम यह जाँचते हैं कि:
sharesToCreate / _totalShares >= slippage * msg.value / address(this).balance
जहाँ स्लिपेज एक ऐसी वैल्यू है जैसे 0.999, यदि इच्छित स्लिपेज 0.1% हो। ज़ाहिर है, हम 0.999 को Solidity में व्यक्त नहीं कर सकते, इसलिए हम इसके बजाय आधार अंक (basis points) का उपयोग कर सकते हैं (1 basis point 0.01% होता है, 10,000 basis points 100% होता है)। इससे हमें निम्नलिखित फॉर्मूला मिलता है:
// slippageBp is basis points, so 9900 means we tolerate a 1% slippage
sharesToCreate / totalShares >= slippageBp / 10_000 * msg.value / address(this).balance
उन भिन्नों (fractions) को हटाने के लिए जो राउंड डाउन होकर शून्य हो जाएंगे, हम असमानता (inequality) के दोनों पक्षों को _totalShares * 10_000 * address(this).balance से गुणा करते हैं। इससे हमें प्राप्त होता है:
sharesToCreate * 10_000 * address(this).balance >= slippageBp * msg.value * _totalShares
इस प्रकार, हमारे mint फ़ंक्शन को इस प्रकार अपडेट किया जा सकता है:
function mint(address to, uint256 slippageBp) external payable {
require(to != address(0), ERC20InvalidReceiver(to));
uint256 sharesToCreate;
if (_totalShares == 0) {
sharesToCreate = msg.value;
} else {
sharesToCreate = msg.value * _totalShares / (address(this).balance - msg.value);
}
_totalShares += sharesToCreate;
_shareBalance[to] += sharesToCreate;
require(sharesToCreate * 10_000 * address(this).balance >= slippageBp * msg.value * _totalShares, "slippage");
emit Transfer(address(0), to, msg.value);
}
_totalShares और _shareBalance[to] को राइट (write) करने के तुरंत बाद स्टोरेज से रीड (read) न करके गैस ऑप्टिमाइज़ेशन की गुंजाइश है, लेकिन सरलता के लिए हम यह ऑप्टिमाइज़ेशन नहीं दिखा रहे हैं।
totalSupply()
जैसा कि पहले बताया गया है, rebasing token की कुल सप्लाई पूल द्वारा होल्ड किया गया ETH है:
function totalSupply() public view returns (uint256) {
return address(this).balance;
}
टोकन अमाउंट (बैलेंस) को शेयर्स में बदलना
इस स्तर पर, एक हेल्पर फ़ंक्शन amountToShares को पेश करना उपयोगी है। यह balanceOf() द्वारा उपयोग किए जाने वाले फॉर्मूले का व्युत्क्रम फ़ंक्शन (inverse function) है। मान लीजिए कि कोई यूज़र अपना पूरा बैलेंस बर्न (या ट्रांसफर) करना चाहता है। यह कितने शेयर्स के बराबर होगा?
इसकी गणना करने के लिए, हम _shareBalance[user] के लिए balanceOf समीकरण को हल करते हैं:
दोनों पक्षों को से गुणा करने और से भाग देने पर हमें मिलता है:
इस प्रकार, बैलेंस को शेयर्स में बदलने के लिए, हम निम्नलिखित फ़ंक्शन का उपयोग करते हैं:
function _amountToShares(uint256 amount) internal view returns (uint256) {
if (address(this).balance == 0) {
return 0;
}
return amount * _totalShares / address(this).balance;
}
अब, जब कोई यूज़र अंतर्निहित (underlying) टोकन (हमारे मामले में, ETH) का “अमाउंट” निर्दिष्ट करता है जिसे वे बर्न करना चाहते हैं, तो हम उसे सीधे शेयर्स में बदल सकते हैं।
burn()
burn के लिए आर्गुमेंट वह बैलेंस है जिसे वे बर्न करना चाहते हैं, न कि शेयर्स। burn के दौरान, हम बर्न किए गए अमाउंट को शेयर्स की मात्रा में बदलते हैं, फिर उसे यूज़र के शेयर्स और totalShares से घटाते हैं।
function burn(address from, uint256 amount) external {
_spendAllowanceOrBlock(from, msg.sender, amount);
uint256 shares = _amountToShares(amount);
require(shares > 0, "zero shares");
_shareBalance[from] -= shares;
_totalShares -= shares;
(bool ok,) = from.call{value: amount}("");
require(ok, ERC20InvalidReceiver(from));
emit Transfer(from, address(0), amount);
}
फिर से, एक rebasing token का बैलेंस वह ETH की मात्रा है जिसे वे निकाल सकते हैं। इसलिए, amount पैरामीटर ठीक वही ETH की मात्रा है जो burn() के अंत में उन्हें ट्रांसफर की जाती है।
बैलेंस घटाने से पहले यूज़र के बैलेंस की जाँच करना आवश्यक नहीं है, क्योंकि Solidity (वर्ज़न 0.8.0 या उसके बाद का) अंडरफ्लो (underflow) होने पर रिवर्ट (revert) कर देता है।
हम बाद के अनुभाग में _spendAllowanceOrBlock() फ़ंक्शन पर फिर से विचार करेंगे।
require(shares > 0, "zero shares"); का उद्देश्य कॉन्ट्रैक्ट से Ether को बाहर ट्रांसफर होने से रोकना है यदि shares राउंड डाउन होकर 0 हो जाता है। याद करें कि _amountToShares की गणना amount * _totalShares / address(this).balance; के रूप में की जाती है। यदि amount * _totalShares, address(this).balance से कम है तो shares 0 पर राउंड हो जाता है। न तो _shareBalance[from] -= shares; और न ही _totalShares -= shares; अंडरफ्लो के कारण रिवर्ट होगा, इसलिए कॉलर (caller) उस require स्टेटमेंट के बिना कॉन्ट्रैक्ट से amount निकाल सकता है।
transfer() और transferFrom()
transfer और transferFrom दोनों burn के समान हैं, सिवाय इसके कि शेयर्स को नष्ट करने और ETH भेजने के बजाय, शेयर्स किसी अन्य अकाउंट में क्रेडिट किए जाते हैं और कोई ETH ट्रांसफर नहीं किया जाता है:
function transferFrom(address from, address to, uint256 amount) public returns (bool) {
require(to != address(0), ERC20InvalidReceiver(to));
_spendAllowanceOrBlock(from, msg.sender, amount);
uint256 shareTransfer = _amountToShares(amount);
_shareBalance[from] -= shareTransfer;
_shareBalance[to] += shareTransfer;
emit Transfer(from, to, amount);
return true;
}
क्योंकि amountToShares में amount * _totalShares / address(this).balance की गणना होती है, इसलिए ट्रांसफर किए गए शेयर्स की मात्रा राउंड डाउन हो सकती है।
चूँकि डिवीज़न राउंड डाउन करता है, इसका मतलब है कि to द्वारा प्राप्त बैलेंस amount से बहुत थोड़ा कम हो सकता है।
आमतौर पर rebasing tokens के लिए यह ध्यान रखने वाली एक समस्या है — Lido डॉक्यूमेंटेशन देखें कि stETH इसे कैसे हैंडल करता है।
burn में राउंडिंग इशू
इसके परिणामस्वरूप, यह संभव है कि कोई यूज़र अपना पूरा बैलेंस बर्न कर दे लेकिन उसके पास एक छोटा बैलेंस बच जाए क्योंकि बर्न करने के लिए कैलकुलेट किए गए शेयर्स की संख्या उनके वास्तविक शेयर्स की संख्या से राउंड डाउन हो गई। इस प्रकार, हमें यह नहीं मानना चाहिए कि पूरा बैलेंस बर्न होने पर शेयर बैलेंस शून्य हो जाता है।
Allowance और Approve
Rebasing tokens के लिए allowance और approve को लागू करने का कोई “सही” तरीका नहीं है, क्योंकि rebasing ERC-20 tokens का कोई ऐसा मानक (standard) नहीं है जो यह तय करता हो कि उन्हें कैसा व्यवहार करना चाहिए।
हालाँकि, अधिकांश rebasing tokens एक allowance मैकेनिज्म का उपयोग करते हैं जो सामान्य ERC-20 token के समान होता है — लेकिन allowance rebase नहीं होता है।
इस मैकेनिज्म की कमी यह है कि यदि Alice अपने पूरे बैलेंस के लिए Bob को approve करती है, लेकिन Bob के Alice से ट्रांसफर करने से पहले कोई rebase होता है, तो Bob उसका पूरा बैलेंस नहीं निकाल सकता है।
Compound Finance द्वारा उपयोग किया जाने वाला rebasing token केवल “सब कुछ या कुछ नहीं (all or nothing)” अप्रूवल की अनुमति देकर इस समस्या को हल करता है। approve को कॉल करते समय अमाउंट केवल 0 या type(uint256).max हो सकता है — लेकिन यह उन प्रोटोकॉल के साथ इंटीग्रेशन को तोड़ सकता है जो उस राशि के लिए allowance निर्दिष्ट करते हैं जिसे वे ट्रांसफर करना चाहते हैं। दूसरी ओर, AAVE और stETH (Lido) rebasing token के लिए allowance और approval एक सामान्य ERC-20 की तरह व्यवहार करते हैं और rebasing के लिए खुद को ठीक नहीं करते हैं।
इसलिए, हमारे टोकन का approval लॉजिक OpenZeppelin के बहुत समान है। हम _spendAllowanceOrBlock() फ़ंक्शन लागू करते हैं जो, जैसा कि नाम से पता चलता है, spender के allowance को खर्च करता है और यदि allowance पर्याप्त नहीं है तो रिवर्ट करता है। हमारे इम्प्लीमेंटेशन में, यदि msg.sender == spender है तो हम allowance खर्च नहीं करते हैं और यदि allowance type(uint256).max है तो हम allowance नहीं घटाते हैं।
हम इसे अगले अनुभाग में पूर्ण इम्प्लीमेंटेशन के रूप में दिखाते हैं।
एक पूर्ण Rebasing ERC-20
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol";
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
contract RebasingERC20 is IERC20Errors, IERC20 {
uint256 internal _totalShares;
mapping(address => uint256) public _shareBalance;
mapping(address owner => mapping(address spender => uint256 allowance)) public allowance;
receive() external payable {}
function mint(address to, uint256 slippageBp) external payable {
require(to != address(0), ERC20InvalidReceiver(to));
require(msg.value > 0);
uint256 sharesToCreate;
if (_totalShares == 0) {
sharesToCreate = msg.value;
} else {
uint256 prevBalance = address(this).balance - msg.value;
sharesToCreate = msg.value * _totalShares / prevBalance;
require(sharesToCreate > 0);
}
_totalShares += sharesToCreate;
_shareBalance[to] += sharesToCreate;
require(sharesToCreate * 10_000 * address(this).balance >= slippageBp * msg.value * _totalShares, "slippage");
uint256 balance = sharesToCreate * address(this).balance / _totalShares;
emit Transfer(address(0), to, balance);
}
function burn(address from, uint256 amount) external {
_spendAllowanceOrBlock(from, msg.sender, amount);
uint256 shares = _amountToShares(amount);
require(shares > 0);
_shareBalance[from] -= shares;
_totalShares -= shares;
(bool ok,) = from.call{value: amount}("");
require(ok, ERC20InvalidReceiver(from));
emit Transfer(from, address(0), amount);
}
function _amountToShares(uint256 amount) public view returns (uint256) {
if (address(this).balance == 0) {
return 0;
}
return amount * _totalShares / address(this).balance;
}
function totalSupply() public view returns (uint256) {
return address(this).balance;
}
function balanceOf(address account) public view returns (uint256) {
if (_totalShares == 0) {
return 0;
}
return _shareBalance[account] * address(this).balance / _totalShares;
}
function transfer(address to, uint256 amount) external returns (bool) {
transferFrom(msg.sender, to, amount);
return true;
}
function transferFrom(address from, address to, uint256 amount) public returns (bool) {
require(to != address(0), ERC20InvalidReceiver(to));
_spendAllowanceOrBlock(from, msg.sender, amount);
uint256 shareTransfer = _amountToShares(amount);
_shareBalance[from] -= shareTransfer;
_shareBalance[to] += shareTransfer;
emit Transfer(from, to, amount);
return true;
}
function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function _spendAllowanceOrBlock(address owner, address spender, uint256 amount) internal {
if (owner != msg.sender && allowance[owner][spender] != type(uint256).max) {
uint256 currentAllowance = allowance[owner][spender];
require(currentAllowance >= amount, ERC20InsufficientAllowance(spender, currentAllowance, amount));
allowance[owner][spender] = currentAllowance - amount;
}
}
}
ध्यान दें कि ऊपर दिए गए कोड में name(), symbol(), और decimals() को लागू नहीं किया गया है।
कुछ अंतिम बातें
यदि किसी के द्वारा rebasing token मिंट किए जाने के बाद कोई कॉन्ट्रैक्ट में ETH ट्रांसफर करता है, तो minter का बैलेंस ऊपर की ओर rebase हो जाएगा।
हालाँकि, यदि किसी के मिंट करने से पहले कोई कॉन्ट्रैक्ट में ETH ट्रांसफर करता है, तो पहले minter को उस ETH पर नियंत्रण मिल जाएगा, और उनका बैलेंस पूल में मौजूद सभी ETH के बराबर होगा क्योंकि वे सभी बकाया शेयर्स के मालिक हैं।
कुछ प्रोटोकॉल गैस एफिशिएंसी के लिए ERC-20 tokens के बैलेंस को कैश (cache) करते हैं — लेकिन यदि टोकन rebase होता है तो यह उनके लॉजिक को तोड़ सकता है।
कई प्रोटोकॉल हमारे ऊपर दिए गए उदाहरण की तरह अपने आप rebase नहीं होते हैं। इसके बजाय, वे दैनिक रूप से या किसी अन्य आवधिक अंतराल (periodic interval) पर rebase होते हैं। यह आवश्यक हो सकता है यदि एसेट कॉन्ट्रैक्ट में होल्ड नहीं किया गया है (उदाहरण के लिए, इसे Ethereum वैलिडेटर्स में स्टेक किया गया है) या यदि एसेट्स की वैल्यू ओरेकल्स (oracles) पर निर्भर करती है।
किसी rebasing token के साथ इंटरैक्ट करते समय, ट्रांसफर में निर्दिष्ट amount शेयर्स की राउंडिंग के कारण बैलेंस में हुए बदलाव के बराबर नहीं भी हो सकता है। इसलिए, जमा की गई सही राशि (true amount deposited) का पता लगाने के लिए निम्नलिखित लॉजिक का उपयोग करना बेहतर है:
uint256 balanceBefore = token.balanceOf(address(this));
token.transferFrom(sender, address(this), amount);
uint256 trueTransferAmount = token.balanceOf(address(this)) - balanceBefore;
Ampleforth और OlympusDao का sOHM token दो अन्य उल्लेखनीय rebase tokens हैं। Ampleforth अपने rebase token की वैल्यू को किसी अन्य एसेट के साथ डायनामिक रूप से पेग (peg) करने के लिए rebase tokens का उपयोग करता है। टोकन की वैल्यू बढ़ाने के लिए, यह नीचे की ओर rebase होता है (जिससे यह अधिक दुर्लभ हो जाता है), और जब इसे टोकन की वैल्यू कम करने की आवश्यकता होती है, तो यह ऊपर की ओर rebase होता है जिससे मुद्रास्फीति (inflation) होती है।
हम इस लेख के पिछले वर्ज़न में सुझाये गए बदलावों के लिए Pashov Audit Group के MerlinBoii और deadrosesxyz को धन्यवाद देना चाहते हैं। हम लेख के अंत में दिए गए संदर्भ कॉन्ट्रैक्ट (reference contract) का ऑडिट करने और पिछले इम्प्लीमेंटेशन में एक गंभीर भेद्यता (vulnerability) की पहचान करने के लिए ChainLight को धन्यवाद देना चाहते हैं (ऑडिट रिपोर्ट)।