minimal proxy standard हमें clone के निर्माण को parameterize करने की अनुमति देता है, लेकिन इसके लिए एक अतिरिक्त initialization transaction की आवश्यकता होती है। इस कदम को पूरी तरह से बायपास करना और storage का उपयोग करने के बजाय proxy के bytecode में उस वैल्यू को parameterize करना संभव है जिसकी हमें परवाह है।
MetaProxy standard भी स्मार्ट कॉन्ट्रैक्ट clones बनाने के लिए एक minimal bytecode implementation है, जिसमें प्रत्येक clones के लिए एक अतिरिक्त अद्वितीय immutable metadata होता है।
यह metadata कुछ भी हो सकता है, एक स्ट्रिंग से लेकर एक नंबर तक, और इसकी arbitrary length हो सकती है। हालाँकि, इसका इच्छित उपयोग implementation contracts के व्यवहार को parameterize करने के लिए function arguments के रूप में कार्य करना है।
चूँकि इस standard का bytecode ज्ञात है, इसका उपयोग उपयोगकर्ताओं और थर्ड-पार्टी टूल्स, जैसे कि Etherscan द्वारा यह पता लगाने के लिए किया जा सकता है कि एक clone हमेशा जोड़े गए metadata के साथ एक विशेष implementation contract एड्रेस पर रीडायरेक्ट करेगा।
आइए metadata के बिना MetaProxy के bytecode पर एक नज़र डालें।
600b380380600b3d393df3363d3d373d3d3d3d60368038038091363936013d73bebebebebebebebebebebebebebebebebebebebe5af43d3d93803e603457fd5bf3
MetaProxy bytecode की लंबाई 65 बाइट्स है, जिसमें 11 बाइट्स का init code और 54 बाइट्स का runtime code शामिल है।
हालाँकि MetaProxy कॉन्ट्रैक्ट का bytecode Minimal Proxy standard के समान है, bytecode के कुछ हिस्से अलग हैं, उदाहरण के लिए, bytecode के हरे रंग के हिस्से (नीचे) में कुछ अतिरिक्त opcode कमांड हैं, जिन्हें हम बाद में समझाएंगे।

डमी एड्रेस 0xbebebebebebebebebebebebebebebebebebebebe को डिप्लॉयमेंट के बाद implementation contract एड्रेस से बदल दिया जाता है।
लेखक
यह लेख RareSkills Technical Writing Program के हिस्से के रूप में Jesse Raymond (LinkedIn, Twitter) द्वारा सह-लिखा गया था।
MetaProxy standard के साथ एक ERC20 कॉन्ट्रैक्ट बनाना
इस सेक्शन में, हम एक ERC20 कॉन्ट्रैक्ट का MetaProxy clone बनाएंगे। आइए गहराई से जानें कि यह कैसे किया जा सकता है और कल्पना करें कि clone में metadata कैसे जोड़ा जाता है।
ERC20 कॉन्ट्रैक्ट को लागू करने के लिए, हम OpenZeppelin ERC20Upgradeable कॉन्ट्रैक्ट को inherit करेंगे, जिसमें constructor के बजाय ERC20 स्टेट वेरिएबल्स को इनिशियलाइज़ करने के लिए “ERC20_init” फ़ंक्शन का उपयोग किया जाता है, जिसे proxy पैटर्न (जैसे कि हम यहाँ बना रहे हैं) के साथ उपयोग नहीं किया जा सकता है।
ऐसा इसलिए है क्योंकि constructors को कॉन्ट्रैक्ट डिप्लॉयमेंट पर कॉल किया जाता है, और यदि हम इस विधि का पालन करते हैं, तो ERC20 standard के name और symbol जैसे स्टेट वेरिएबल्स ERC20 MetaProxy clone bytecode में इनिशियलाइज़ नहीं होंगे, क्योंकि constructor implementation contract के storage को सेट कर रहा होगा न कि clone के।
हालाँकि, हम initialization फ़ंक्शन का उपयोग नहीं करेंगे क्योंकि हम उन्हें metadata के रूप में जोड़ने के बाद आसानी से ERC20 MetaProxy clone के name, symbol और totalSupply को इसके bytecode से प्राप्त कर सकते हैं।
ERC20 implementation contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
contract ERC20Implementation is ERC20Upgradeable {
// get ERC20 name from the metadata
function name()
public
view
virtual
override
returns (string memory name__)
{
(name__, , ) = getMetadata();
}
// get ERC20 symbol from the metadata
function symbol()
public
view
virtual
override
returns (string memory symbol__)
{
(, symbol__, ) = getMetadata();
}
// get ERC20 total supply from the metadata
function totalSupply()
public
view
virtual
override
returns (uint256 totalSupply_)
{
(, , totalSupply_) = getMetadata();
}
// mint function
function mint(uint amount) public {
_mint(msg.sender, amount * 10 ** 18);
}
/// returns the decoded metadata of this (ERC20 MetaProxy) contract.
function getMetadata()
public
pure
returns (
string memory name__,
string memory symbol__,
uint256 totalSupply__
)
{
bytes memory data;
assembly {
let posOfMetadataSize := sub(calldatasize(), 32)
let size := calldataload(posOfMetadataSize)
let dataPtr := sub(posOfMetadataSize, size)
data := mload(64)
mstore(64, add(data, add(size, 32)))
mstore(data, size)
let memPtr := add(data, 32)
calldatacopy(memPtr, dataPtr, size)
}
//return the decoded metadata
return abi.decode(data, (string, string, uint256));
}
}
Metadata प्राप्त करना
implementation में getMetadata फ़ंक्शन का उपयोग clone का metadata वापस करने (return करने) के लिए किया जाता है। चूँकि MetaProxy हमेशा अपना metadata तब लोड करता है जब भी इसके फ़ंक्शंस को कॉल किया जाता है (यह standard का डिज़ाइन है, जिसे हम इस लेख में बाद में समझाएंगे), कॉल से metadata निकालने और इसे हमारे implementation में एक ट्यूपल (tuple) के रूप में वापस करने के लिए getMetadata फ़ंक्शन का उपयोग किया जाता है।
इसका उपयोग ERC20 name, symbol और totalSupply फ़ंक्शंस में metadata का एक विशिष्ट भाग प्राप्त करने के लिए भी किया जा रहा है, जो name और symbol के लिए एक स्ट्रिंग या totalSupply के लिए एक uint256 हो सकता है।
हमने इस फ़ंक्शन को यहाँ दिए गए उदाहरण implementation से लिया है और इसे ERC20 कॉन्ट्रैक्ट के लिए हमारे उद्देश्यों के अनुकूल संशोधित किया है।
Factory कॉन्ट्रैक्ट
मूल EIP में MetaProxyFactory के implementation का एक लिंक भी है, जिसे हम यहाँ इंपोर्ट (import) और इनहेरिट (inherit) करते हैं।
MetaProxyFactory में नए MetaProxy clones बनाने के लिए कोड होता है।
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./ERC20Implementation.sol";
import "./MetaProxyFactory.sol";
contract ERC20MetaProxyFactory is MetaProxyFactory {
address[] public proxyAddresses;
function createClone(
string memory _name,
string memory _symbol,
uint256 _initialSupply
) public returns (address) {
// Encode the ERC20 constructor arguments
bytes memory metadata = abi.encode(_name, _symbol, _initialSupply);
// Create the proxy
address proxyAddress = _metaProxyFromBytes(
address(new ERC20Implementation()),
metadata
);
proxyAddresses.push(proxyAddress);
return proxyAddress;
}
}
एक clone बनाना - factory कॉन्ट्रैक्ट को समझना
ERC20MetaProxyFactory नए clones बनाने के लिए हमारा factory कॉन्ट्रैक्ट है। नए clones को डिप्लॉय करने के लिए हम _metaProxyFromBytes फ़ंक्शन का उपयोग करते हैं जिसे MetaProxyFactory से inherit किया गया है।
_metaProxyFromBytes फ़ंक्शन दो arguments लेता है, जो हैं; 1. implementation contract का एड्रेस (यही कारण है कि हम पहले एक “ERC20Implementation” कॉन्ट्रैक्ट को डिप्लॉय करने के लिए new कीवर्ड का उपयोग करते हैं)। 2. metadata।
चूँकि स्मार्ट कॉन्ट्रैक्ट्स का bytecode इस कोड में हेक्साडेसिमल (hexadecimal) में दर्शाया गया है, इसलिए metadata को clone के bytecode में जोड़े जाने से पहले abi encoded होना चाहिए।
यही कारण है कि हम createClone फ़ंक्शन arguments को एनकोड (encode) करते हैं, और फिर उन्हें metadata के रूप में _metaProxyFromBytes फ़ंक्शन में पास करते हैं जो नया clone बनाता है और एड्रेस वापस (return) करता है।
यह _metaProxyFromBytes फ़ंक्शन का फ़ंक्शन सिग्नेचर है।
function _metaProxyFromBytes (address targetContract, bytes memory metadata) internal returns (address) {
// code that deploys new clones here
}
Clone को डिप्लॉय करना
यहाँ एक Hardhat स्क्रिप्ट है जो कॉन्ट्रैक्ट्स को डिप्लॉय करती है और Sepolia नेटवर्क पर डिप्लॉय किए गए एक clone के साथ इंटरैक्ट करती है:
const hre = require("hardhat");
async function main() {
const ERC20ProxyFactory = await hre.ethers.getContractFactory(
"ERC20MetaProxyFactory"
);
const erc20ProxyFactory = await ERC20ProxyFactory.deploy();
// deploy the erc20 proxy factory contract
await erc20ProxyFactory.deployed();
console.log(
`ERC20 proxy factory contract deployed to ${erc20ProxyFactory.address}`
);
// create clone
const tx1 = await erc20ProxyFactory.createClone(
"Meta Token V1",
"MTV1",
"150000000000000000000000" //150,000 initial supply * 10^18 decimals
);
await tx1.wait();
const proxyCloneAddress = await erc20ProxyFactory.proxyAddresses(0);
console.log("Proxy clone deployed to", proxyCloneAddress);
// load the clone
const proxyClone = await hre.ethers.getContractAt(
"ERC20Implementation",
proxyCloneAddress
);
// retrieve the metadata
const metadata = await proxyClone.getMetadata();
console.log("metadata for clone: ", metadata);
//retrieve the "name" string from the metadata
const name = await proxyClone.name();
console.log("ERC20 name of clone from metadata: ", name);
const tx2 = await proxyClone.mint(150_000);
tx2.wait();
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
इस स्क्रिप्ट को चलाने के बाद, कंसोल (console) ने निम्नलिखित आउटपुट प्रदर्शित किया:
ERC20 proxy factory contract deployed to 0xd45f2c555ba30aCb89EB0a3fff6a4416f8cC06e2
Proxy clone deployed to 0x5170672424194899F52B29E60e85C1632F0C732e
metadata for clone: [
'MetaProxy Token',
'MPRXT',
BigNumber { value: "150000000000000000000000" },
name__: 'MetaProxy Token',
symbol__: 'MPRXT',
totalSupply__: BigNumber { value: "150000000000000000000000" }
]
ERC20 name of clone from metadata: MetaProxy Token
हमने अपने कॉन्ट्रैक्ट्स को Sepolia नेटवर्क पर भी डिप्लॉय किया है और यहाँ 3 कॉन्ट्रैक्ट्स का विवरण दिया गया है।
ध्यान दें कि ERC20 MetaProxy कॉन्ट्रैक्ट के लिए “read” और “write as proxy” विकल्प हैं, इसका अर्थ है कि Etherscan यह पहचानता है कि proxy कॉन्ट्रैक्ट केवल एक और स्मार्ट कॉन्ट्रैक्ट नहीं है, बल्कि एक proxy कॉन्ट्रैक्ट है।
सुविधा के लिए, हमारा कोड डिप्लॉय किए गए clones की एक सूची रखता है ताकि हम उन्हें अपने Hardhat वातावरण में आसानी से एक्सेस कर सकें, लेकिन यह वैकल्पिक (optional) है।
Reverts को हैंडल करना
जैसा कि परिचय में बताया गया है, यदि कोई त्रुटि (error) तब होती है जब proxy clone किसी कॉल को implementation contract पर रीडायरेक्ट करता है, तो revert पेलोड clone को वापस कर दिया जाता है और उपयोगकर्ताओं को प्रदर्शित किया जाता है।
आइए इसका परीक्षण करें और देखें कि क्या यह अपेक्षित रूप से काम करता है।
हमारे पिछले ERC20 कॉन्ट्रैक्ट उदाहरण में, हम बिना किसी allowance के “transferFrom” फ़ंक्शन को कॉल करने का प्रयास करेंगे ताकि यह देखा जा सके कि क्या transaction सफल होता है या त्रुटि हमें वापस कर दी जाती है।
हम इसे इस Hardhat स्क्रिप्ट के साथ करते हैं।
try {
await proxyClone.transferFrom(
proxyCloneAddress,
erc20ProxyFactory.address,2000000000);
} catch (error) {
console.error(error);
}
और बूम! हमें एक त्रुटि (error) मिली।
Error: VM Exception while processing transaction: reverted with reason
string 'ERC20: insufficient allowance'
इसका अर्थ है कि revert और reverts के कारण को clone में अच्छी तरह से वापस भेज दिया जाता है!
डिप्लॉय किए गए ERC20 clone के bytecode को समझना
याद रखें कि हमने पहले कहा था कि clone का metadata clone के अंत में जोड़ा जाता है। इस सेक्शन में, हम MetaProxy clone के डिप्लॉय किए गए bytecode को समझाएंगे।
ध्यान दें कि सभी clones का bytecode MetaProxy standard के minimal bytecode का पालन करता है, सिवाय इसके कि प्रत्येक clone के bytecode के अंत में उसका अपना metadata होता है।
आइए ERC20 clone के bytecode पर एक नज़र डालें।
0x363d3d373d3d3d3d60368038038091363936013d731bf70065f6b4e424b7b642b3a76a5e01f208e3fc5af43d3d93803e603457fd5bf3000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000174876e800000000000000000000000000000000000000000000000000000000000000000a50726f7879546f6b656e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000650546f6b656e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0
यहाँ बहुत सारे शून्य (zeros) हैं! हमारा bytecode 310 बाइट्स लंबा है।
आइए इसे आगे समझते हैं।
<=== the runtime bytecode of the MetaProxy standard ===>
0x363d3d373d3d3d3d60368038038091363936013d731bf70065f6b4e424b7b642b3a76a5e01f208e3fc5af43d3d93803e603457fd5bf3
<===>
<=== the abi encoded metadata ===>
000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000174876e800000000000000000000000000000000000000000000000000000000000000000a50726f7879546f6b656e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000650546f6b656e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0
<===>
एनकोडेड metadata में मेमोरी में वे offsets शामिल होते हैं जहाँ एनकोडेड वैल्यूज़ संग्रहीत (stored) होती हैं, एनकोडेड स्ट्रिंग वैल्यूज़ की लंबाई, वैल्यूज़, और अगली उपलब्ध मेमोरी (free memory pointer) शामिल होती है। यहाँ metadata का विस्तृत विवरण दिया गया है।
ABI स्पेसिफिकेशन (specification) का पालन करते हुए, 32-बाइट शब्दों के पहले तीन सेट या तो उस डेटा के वैल्यूज़ का प्रतिनिधित्व करते हैं जिसे हम एनकोड कर रहे हैं, या उनके स्थान (location) के पॉइंटर का यदि वे डायनामिक (dynamic) प्रकार के हैं। हमारे पास दो स्ट्रिंग्स और एक अनसाइन्ड इंटीजर (unsigned integer) है, जो क्रमशः नाम, प्रतीक (symbol) और कुल आपूर्ति (total supply) का प्रतिनिधित्व करते हैं। चूँकि नाम और प्रतीक डायनामिक हैं, उनके स्लॉट में पॉइंटर्स होते हैं, जबकि कुल आपूर्ति बस अपने स्लॉट में संग्रहीत होती है।
// memory[0x00 - 0x20] 0000000000000000000000000000000000000000000000000000000000000060 // memory offset for name string
// memory[0x20 - 0x40] 00000000000000000000000000000000000000000000000000000000000000a0 // memory offset for symbol string
// memory[0x40 - 0x60] 000000000000000000000000000000000000000000000000000000174876e800 // the encoded total supply (uint256)
// memory[0x60 - 0x80] 000000000000000000000000000000000000000000000000000000000000000a // the length of the name string (0x0a == 10)
// memory[0x80 - 0xa0] 50726f7879546f6b656e00000000000000000000000000000000000000000000 // the encoded name string
// memory[0xa0 - 0xc0] 0000000000000000000000000000000000000000000000000000000000000006 // the length of the symbol string (6)
// memory[0xc0 - 0xe0] 50546f6b656e0000000000000000000000000000000000000000000000000000 // the encoded symbol string
// memory[0xe0 ] 00000000000000000000000000000000000000000000000000000000000000e0 // the length of the metadata (0xe0 == 224)
जैसा कि पहले बताया गया है, runtime code 54 बाइट्स का होता है। यदि हम ERC20 clone के bytecode को दो भागों में विभाजित करते हैं, और runtime code के पहले 54 bytecode को निकाल देते हैं, तो हमारे पास abi एनकोडेड metadata बचता है, जो 224 बाइट्स है, और कोड के अंत में जोड़ी गई metadata की लंबाई बचती है, जो 32 बाइट्स है।
standard के अनुसार
…MetaProxy bytecode के बाद कुछ भी arbitrary metadata हो सकता है और bytecode के अंतिम 32 बाइट्स (एक वर्ड) को बाइट्स में metadata की लंबाई को इंगित करना चाहिए।
हमारे मामले में, metadata 224 बाइट्स लंबा है, और इसकी लंबाई अंतिम 32 बाइट्स (0x000…000e0) में संग्रहीत की जाती है।
metadata की लंबाई को अंत में संग्रहीत करना अजीब लग सकता है, क्योंकि ABI एन्कोडिंग (encoding) आमतौर पर डेटा शुरू होने से पहले लंबाई को संग्रहीत करती है, लेकिन इस मामले में, यह implementation contract के लिए पहले के निम्नलिखित कोड के साथ अतिरिक्त metadata को पार्स (parse) करना आसान बनाता है।
let posOfMetadataSize := sub(calldatasize(), 32)
यदि हम metadata को यहाँ डिकोड (decode) करते हैं, तो हमें clone का initialization डेटा प्राप्त होता है।

आइए bytecode के निमोनिक्स (mnemonics) को समझते हैं।
<=== start of the runtime bytecode ===>
// Note that RETURNDATASIZE is used in some parts of the bytecode to push zero to the stack.
// This is because RETURNDATASIZE (2 gas) costs less gas than a PUSH1 0 (3 gas).
// copy transaction calldata
[00] CALLDATASIZE
[01] RETURNDATASIZE
[02] RETURNDATASIZE
[03] CALLDATACOPY
// prepare the stack for a delegate call
[04] RETURNDATASIZE
[05] RETURNDATASIZE
[06] RETURNDATASIZE
[07] RETURNDATASIZE
[08] PUSH1 36 // 0x36 == 54, this is the length of the runtime code
[0a] DUP1
[0b] CODESIZE // get the length of the clone's bytecode + the metadata, which is 310 bytes
[0c] SUB // subtract the runtime code from the bytecode, to get the metadata (the remaining 256 bytes). this is used in the delegatecall
[0d] DUP1
[0e] SWAP2
[0f] CALLDATASIZE
[10] CODECOPY // copy the metadata to memory and forward it to the implementation contract during the delegatecall.
[11] CALLDATASIZE
[12] ADD
[13] RETURNDATASIZE
// push the address of the implementation contract to the stack and perform the delegatecall
[14] PUSH20 1bf70065f6b4e424b7b642b3a76a5e01f208e3fc
[29] GAS
[2a] DELEGATECALL
// copy the return data (the result of the call) to memory and set up the stack for a conditional jump
[2b] RETURNDATASIZE
[2c] RETURNDATASIZE
[2d] SWAP4
[2e] DUP1
[2f] RETURNDATACOPY
[30] PUSH1 34
//jump to line 34 and return the result of the call if it was successful, else revert on line 33
[32] JUMPI
[33] REVERT
[34] JUMPDEST
[35] RETURN
<<=== the metadata starts from here ===>>
यह clone का bytecode कैसे काम करता है, इसका एक हाई-लेवल विवरण है। संक्षेप में, यह एक transaction में इसे भेजे गए calldata को कॉपी करता है और उस calldata के साथ implementation contract को एक delegatecall करता है, जबकि delegatecall में metadata को आगे बढ़ाता है।
ध्यान दें कि चूँकि metadata को भी सभी कॉल्स में आगे बढ़ाया जाता है, इसलिए इसे कुछ ऐसे फ़ंक्शंस के साथ भी भेजा जाएगा जिन्हें इसकी आवश्यकता नहीं है, जैसे कि ERC20 balanceOf() फ़ंक्शन, जिसका एनकोडेड metadata से कोई संबंध नहीं है।
निष्कर्ष
EIP-3448 MetaProxy standard को EIP-1167 Minimal Proxy Standard के विस्तार के रूप में देखा जा सकता है, जो प्रत्येक clone के runtime bytecode से immutable metadata को जोड़ने की अनुमति देता है।
MetaProxy standard उपयोगकर्ताओं को storage का उपयोग करने के बजाय clone के bytecode में उस वैल्यू को parameterize करने की अनुमति देता है जिसकी वे परवाह करते हैं, जिससे गैस की लागत (gas costs) कम हो जाती है।
इसके अलावा, थर्ड-पार्टी टूल्स जैसे कि Etherscan standard के ज्ञात bytecode का उपयोग यह निर्धारित करने के लिए कर सकते हैं कि एक clone हमेशा एक विशिष्ट तरीके से एक विशिष्ट implementation contract एड्रेस पर रीडायरेक्ट करेगा और clone से जुड़े metadata को क्वेरी (query) करेगा।
और जानें
यह सामग्री हमारे एडवांस्ड Solidity Bootcamp का हिस्सा है। हमारी सभी पेशकशों के लिए हमारा blockchain bootcamp देखें।
मूल रूप से 3 मार्च, 2023 को प्रकाशित