کال ڈیٹا (Calldata) کی بہتری کے لیے مختصر ABIs
تعارف
اس مضمون میں، آپ آپٹیمسٹک رول اپس (optimistic rollups)، ان پر ٹرانزیکشنز کی لاگت، اور اس بارے میں جانیں گے کہ کس طرح یہ مختلف لاگت کا ڈھانچہ ہمیں ایتھیریم مین نیٹ (Mainnet) کی نسبت مختلف چیزوں کو بہتر بنانے کا تقاضا کرتا ہے۔ آپ یہ بھی سیکھیں گے کہ اس بہتری کو کیسے نافذ کیا جائے۔
مکمل انکشاف
میں Optimism (opens in a new tab) کا کل وقتی ملازم ہوں، اس لیے اس مضمون میں دی گئی مثالیں Optimism پر چلیں گی۔ تاہم، یہاں بیان کی گئی تکنیک دیگر رول اپس کے لیے بھی اتنی ہی اچھی طرح کام کرنی چاہیے۔
اصطلاحات
جب رول اپس پر بات کی جاتی ہے، تو اصطلاح 'لیئر 1' (L1) مین نیٹ (Mainnet)، یعنی پروڈکشن ایتھیریم نیٹ ورک کے لیے استعمال ہوتی ہے۔ اصطلاح 'لیئر 2' (L2) رول اپ یا کسی دوسرے سسٹم کے لیے استعمال ہوتی ہے جو سیکیورٹی کے لیے L1 پر انحصار کرتا ہے لیکن اپنی زیادہ تر پروسیسنگ آف چین (offchain) کرتا ہے۔
ہم L2 ٹرانزیکشنز کی لاگت کو مزید کیسے کم کر سکتے ہیں؟
آپٹیمسٹک رول اپس کو ہر تاریخی ٹرانزیکشن کا ریکارڈ محفوظ کرنا ہوتا ہے تاکہ کوئی بھی ان کا جائزہ لے سکے اور تصدیق کر سکے کہ موجودہ اسٹیٹ (state) درست ہے۔ ایتھیریم مین نیٹ میں ڈیٹا داخل کرنے کا سب سے سستا طریقہ اسے کال ڈیٹا (calldata) کے طور پر لکھنا ہے۔ یہ حل Optimism (opens in a new tab) اور Arbitrum (opens in a new tab) دونوں نے منتخب کیا تھا۔
L2 ٹرانزیکشنز کی لاگت
L2 ٹرانزیکشنز کی لاگت دو اجزاء پر مشتمل ہوتی ہے:
- L2 پروسیسنگ، جو عام طور پر انتہائی سستی ہوتی ہے
- L1 اسٹوریج، جو مین نیٹ گیس کی لاگت سے منسلک ہوتی ہے
جب میں یہ لکھ رہا ہوں، Optimism پر L2 گیس کی قیمت 0.001 Gwei ہے۔ دوسری طرف، L1 گیس کی قیمت تقریباً 40 gwei ہے۔ آپ موجودہ قیمتیں یہاں دیکھ سکتے ہیں (opens in a new tab)۔
کال ڈیٹا کے ایک بائٹ کی قیمت یا تو 4 گیس (اگر یہ صفر ہو) یا 16 گیس (اگر یہ کوئی اور قدر ہو) ہوتی ہے۔ EVM پر سب سے مہنگے آپریشنز میں سے ایک اسٹوریج میں لکھنا ہے۔ L2 پر اسٹوریج میں 32 بائٹ کا لفظ لکھنے کی زیادہ سے زیادہ لاگت 22100 گیس ہے۔ فی الحال، یہ 22.1 gwei ہے۔ لہذا اگر ہم کال ڈیٹا کا ایک بھی صفر بائٹ بچا سکیں، تو ہم اسٹوریج میں تقریباً 200 بائٹس لکھنے کے قابل ہو جائیں گے اور پھر بھی فائدے میں رہیں گے۔
ABI
ٹرانزیکشنز کی اکثریت ایک بیرونی ملکیت والے اکاؤنٹ (externally-owned account) سے کانٹریکٹ تک رسائی حاصل کرتی ہے۔ زیادہ تر کانٹریکٹس Solidity میں لکھے جاتے ہیں اور اپنے ڈیٹا فیلڈ کی تشریح ایپلیکیشن بائنری انٹرفیس (ABI) (opens in a new tab) کے مطابق کرتے ہیں۔
تاہم، ABI کو L1 کے لیے ڈیزائن کیا گیا تھا، جہاں کال ڈیٹا کے ایک بائٹ کی قیمت تقریباً چار ریاضیاتی آپریشنز کے برابر ہوتی ہے، نہ کہ L2 کے لیے جہاں کال ڈیٹا کے ایک بائٹ کی قیمت ایک ہزار سے زیادہ ریاضیاتی آپریشنز سے زیادہ ہوتی ہے۔ کال ڈیٹا کو اس طرح تقسیم کیا گیا ہے:
| سیکشن | لمبائی | بائٹس | ضائع شدہ بائٹس | ضائع شدہ گیس | ضروری بائٹس | ضروری گیس |
|---|---|---|---|---|---|---|
| فنکشن سلیکٹر | 4 | 0-3 | 3 | 48 | 1 | 16 |
| صفر (Zeroes) | 12 | 4-15 | 12 | 48 | 0 | 0 |
| منزل کا پتہ | 20 | 16-35 | 0 | 0 | 20 | 320 |
| رقم | 32 | 36-67 | 17 | 64 | 15 | 240 |
| کل | 68 | 160 | 576 |
وضاحت:
- فنکشن سلیکٹر: کانٹریکٹ میں 256 سے کم فنکشنز ہیں، اس لیے ہم انہیں ایک بائٹ سے ممتاز کر سکتے ہیں۔ یہ بائٹس عام طور پر غیر صفر ہوتے ہیں اور اس لیے ان کی قیمت سولہ گیس ہوتی ہے (opens in a new tab)۔
- صفر (Zeroes): یہ بائٹس ہمیشہ صفر ہوتے ہیں کیونکہ بیس بائٹ کے پتے کو رکھنے کے لیے بتیس بائٹ کے لفظ کی ضرورت نہیں ہوتی۔ صفر رکھنے والے بائٹس کی قیمت چار گیس ہوتی ہے (یلو پیپر دیکھیں (opens in a new tab)، ضمیمہ G، صفحہ 27،
Gtxdatazeroکی قدر)۔ - رقم: اگر ہم فرض کریں کہ اس کانٹریکٹ میں
decimalsاٹھارہ ہیں (عام قدر) اور ٹوکنز کی زیادہ سے زیادہ رقم جو ہم منتقل کریں گے وہ 1018 ہوگی، تو ہمیں زیادہ سے زیادہ رقم 1036 ملتی ہے۔ 25615 > 1036، اس لیے پندرہ بائٹس کافی ہیں۔
L1 پر 160 گیس کا ضیاع عام طور پر نہ ہونے کے برابر ہے۔ ایک ٹرانزیکشن کی لاگت کم از کم 21,000 گیس (opens in a new tab) ہوتی ہے، اس لیے اضافی 0.8% سے کوئی فرق نہیں پڑتا۔ تاہم، L2 پر، چیزیں مختلف ہیں۔ ٹرانزیکشن کی تقریباً پوری لاگت اسے L1 پر لکھنے کی ہوتی ہے۔ ٹرانزیکشن کال ڈیٹا کے علاوہ، ٹرانزیکشن ہیڈر (منزل کا پتہ، دستخط، وغیرہ) کے 109 بائٹس ہوتے ہیں۔ اس لیے کل لاگت 109*16+576+160=2480 ہے، اور ہم اس کا تقریباً 6.5% ضائع کر رہے ہیں۔
جب آپ کا منزل پر کنٹرول نہ ہو تو لاگت کو کم کرنا
یہ فرض کرتے ہوئے کہ آپ کا منزل کے کانٹریکٹ پر کنٹرول نہیں ہے، آپ پھر بھی اس جیسے (opens in a new tab) حل کا استعمال کر سکتے ہیں۔ آئیے متعلقہ فائلوں کا جائزہ لیتے ہیں۔
Token.sol
یہ منزل کا کانٹریکٹ ہے (opens in a new tab)۔ یہ ایک معیاری ERC-20 کانٹریکٹ ہے، جس میں ایک اضافی خصوصیت ہے۔ یہ faucet فنکشن کسی بھی صارف کو استعمال کے لیے کچھ ٹوکن حاصل کرنے دیتا ہے۔ یہ ایک پروڈکشن ERC-20 کانٹریکٹ کو بیکار بنا دے گا، لیکن جب کوئی ERC-20 صرف ٹیسٹنگ کی سہولت کے لیے موجود ہو تو یہ زندگی کو آسان بنا دیتا ہے۔
1 /* *2 * @dev کالر کو کھیلنے کے لیے 1000 ٹوکنز دیتا ہے */3 function faucet() external {4 _mint(msg.sender, 1000);5 } // function faucetCalldataInterpreter.sol
یہ وہ کانٹریکٹ ہے جسے ٹرانزیکشنز کو مختصر کال ڈیٹا کے ساتھ کال کرنا چاہیے (opens in a new tab)۔ آئیے اس کا لائن بہ لائن جائزہ لیتے ہیں۔
1// SPDX-License-Identifier: Unlicense2pragma solidity ^0.8.0;345import { OrisUselessToken } from "./Token.sol";ہمیں ٹوکن فنکشن کی ضرورت ہے تاکہ معلوم ہو سکے کہ اسے کیسے کال کرنا ہے۔
1contract CalldataInterpreter {23 OrisUselessToken public immutable token;اس ٹوکن کا پتہ جس کے لیے ہم ایک پراکسی ہیں۔
12 /* *3 * @dev ٹوکن کا ایڈریس متعین کریں4 * @param tokenAddr_ ERC-20 کنٹریکٹ کا ایڈریس */5 constructor(6 address tokenAddr_7 ) {8 token = OrisUselessToken(tokenAddr_);9 } // constructorسب دکھائیںٹوکن کا پتہ واحد پیرامیٹر ہے جسے ہمیں بتانے کی ضرورت ہے۔
1 function calldataVal(uint startByte, uint length)2 private pure returns (uint) {کال ڈیٹا سے ایک قدر پڑھیں۔
1 uint _retVal;23 require(length < 0x21,4 "calldataVal length limit is 32 bytes");56 require(length + startByte <= msg.data.length,7 "calldataVal trying to read beyond calldatasize");ہم میموری میں ایک 32 بائٹ (256 بٹ) کا لفظ لوڈ کرنے جا رہے ہیں اور ان بائٹس کو ہٹا دیں گے جو ہمارے مطلوبہ فیلڈ کا حصہ نہیں ہیں۔ یہ الگورتھم 32 بائٹس سے لمبی قدروں کے لیے کام نہیں کرتا، اور یقیناً ہم کال ڈیٹا کے اختتام سے آگے نہیں پڑھ سکتے۔ L1 پر گیس بچانے کے لیے ان ٹیسٹس کو چھوڑنا ضروری ہو سکتا ہے، لیکن L2 پر گیس انتہائی سستی ہے، جو ہمیں کسی بھی قسم کے سینیٹی چیکس (sanity checks) کو فعال کرنے کی اجازت دیتی ہے جو ہم سوچ سکتے ہیں۔
1 assembly {2 _retVal := calldataload(startByte)3 }ہم fallback() کی کال سے ڈیٹا کاپی کر سکتے تھے (نیچے دیکھیں)، لیکن EVM کی اسمبلی زبان Yul (opens in a new tab) کا استعمال کرنا زیادہ آسان ہے۔
یہاں ہم اسٹیک میں startByte سے startByte+31 تک بائٹس پڑھنے کے لیے CALLDATALOAD اوپ کوڈ (opcode) (opens in a new tab) کا استعمال کرتے ہیں۔ عام طور پر، Yul میں اوپ کوڈ کا نحو (syntax) <opcode name>(<first stack value, if any>,<second stack value, if any>...) ہوتا ہے۔
12 _retVal = _retVal >> (256-length*8);صرف سب سے اہم length بائٹس فیلڈ کا حصہ ہیں، اس لیے ہم دیگر قدروں سے چھٹکارا پانے کے لیے رائٹ شفٹ (right-shift) (opens in a new tab) کرتے ہیں۔ اس کا ایک اضافی فائدہ یہ ہے کہ قدر فیلڈ کے دائیں طرف منتقل ہو جاتی ہے، اس لیے یہ قدر بذات خود ہوتی ہے نہ کہ قدر ضرب 256something۔
12 return _retVal;3 }456 fallback() external {جب کسی Solidity کانٹریکٹ کی کال کسی بھی فنکشن کے دستخطوں (signatures) سے میل نہیں کھاتی، تو یہ fallback() فنکشن (opens in a new tab) کو کال کرتی ہے (یہ فرض کرتے ہوئے کہ وہاں ایک موجود ہے)۔ CalldataInterpreter کے معاملے میں، کوئی بھی کال یہاں آتی ہے کیونکہ کوئی اور external یا public فنکشنز نہیں ہیں۔
1 uint _func;23 _func = calldataVal(0, 1);کال ڈیٹا کا پہلا بائٹ پڑھیں، جو ہمیں فنکشن بتاتا ہے۔ یہاں کسی فنکشن کے دستیاب نہ ہونے کی دو وجوہات ہو سکتی ہیں:
- وہ فنکشنز جو
pureیاviewہیں وہ اسٹیٹ (state) کو تبدیل نہیں کرتے اور ان پر گیس خرچ نہیں ہوتی (جب آف چین کال کیا جائے)۔ ان کی گیس کی لاگت کو کم کرنے کی کوشش کرنے کا کوئی مطلب نہیں ہے۔ - وہ فنکشنز جو
msg.sender(opens in a new tab) پر انحصار کرتے ہیں۔msg.senderکی قدر کال کرنے والے کی بجائےCalldataInterpreterکا پتہ ہوگی۔
بدقسمتی سے، ERC-20 کی خصوصیات کو دیکھتے ہوئے (opens in a new tab)، اس سے صرف ایک فنکشن، transfer بچتا ہے۔ اس سے ہمارے پاس صرف دو فنکشنز بچتے ہیں: transfer (کیونکہ ہم transferFrom کو کال کر سکتے ہیں) اور faucet (کیونکہ ہم ٹوکنز واپس اس شخص کو منتقل کر سکتے ہیں جس نے ہمیں کال کیا تھا)۔
12 // ٹوکن کے اسٹیٹ تبدیل کرنے والے میتھڈز کو کال کریں استعمال کرتے ہوئے3 // کال ڈیٹا سے معلومات45 // faucet6 if (_func == 1) {faucet() کی ایک کال، جس میں پیرامیٹرز نہیں ہوتے۔
1 token.faucet();2 token.transfer(msg.sender,3 token.balanceOf(address(this)));4 }token.faucet() کو کال کرنے کے بعد ہمیں ٹوکن ملتے ہیں۔ تاہم، پراکسی کانٹریکٹ کے طور پر، ہمیں ٹوکنز کی ضرورت نہیں ہے۔ EOA (بیرونی ملکیت والا اکاؤنٹ) یا جس کانٹریکٹ نے ہمیں کال کیا اسے ضرورت ہے۔ اس لیے ہم اپنے تمام ٹوکنز اس شخص کو منتقل کر دیتے ہیں جس نے ہمیں کال کیا تھا۔
1 // ٹرانسفر (فرض کریں کہ ہمارے پاس اس کے لیے الاؤنس موجود ہے)2 if (_func == 2) {ٹوکنز منتقل کرنے کے لیے دو پیرامیٹرز کی ضرورت ہوتی ہے: منزل کا پتہ اور رقم۔
1 token.transferFrom(2 msg.sender,ہم کال کرنے والوں کو صرف ان کے اپنے ٹوکنز منتقل کرنے کی اجازت دیتے ہیں
1 address(uint160(calldataVal(1, 20))),منزل کا پتہ بائٹ #1 سے شروع ہوتا ہے (بائٹ #0 فنکشن ہے)۔ ایک پتے کے طور پر، یہ 20 بائٹس لمبا ہوتا ہے۔
1 calldataVal(21, 2)اس مخصوص کانٹریکٹ کے لیے ہم فرض کرتے ہیں کہ ٹوکنز کی زیادہ سے زیادہ تعداد جو کوئی بھی منتقل کرنا چاہے گا وہ دو بائٹس میں فٹ ہو جاتی ہے (65536 سے کم)۔
1 );2 }مجموعی طور پر، ایک منتقلی میں کال ڈیٹا کے 35 بائٹس لگتے ہیں:
| سیکشن | لمبائی | بائٹس |
|---|---|---|
| فنکشن سلیکٹر | 1 | 0 |
| منزل کا پتہ | 32 | 1-32 |
| رقم | 2 | 33-34 |
1 } // fallback23} // contract CalldataInterpretertest.js
یہ جاوا اسکرپٹ یونٹ ٹیسٹ (opens in a new tab) ہمیں دکھاتا ہے کہ اس طریقہ کار کو کیسے استعمال کیا جائے (اور یہ کیسے تصدیق کی جائے کہ یہ صحیح طریقے سے کام کرتا ہے)۔ میں یہ فرض کرنے جا رہا ہوں کہ آپ chai (opens in a new tab) اور ethers (opens in a new tab) کو سمجھتے ہیں اور صرف ان حصوں کی وضاحت کروں گا جو خاص طور پر کانٹریکٹ پر لاگو ہوتے ہیں۔
1const { expect } = require("chai");23describe("CalldataInterpreter", function () {4 it("Should let us use tokens", async function () {5 const Token = await ethers.getContractFactory("OrisUselessToken")6 const token = await Token.deploy()7 await token.deployed()8 console.log("Token addr:", token.address)910 const Cdi = await ethers.getContractFactory("CalldataInterpreter")11 const cdi = await Cdi.deploy(token.address)12 await cdi.deployed()13 console.log("CalldataInterpreter addr:", cdi.address)1415 const signer = await ethers.getSigner()سب دکھائیںہم دونوں کانٹریکٹس کو ڈیپلائے کر کے شروعات کرتے ہیں۔
1 // کھیلنے کے لیے ٹوکنز حاصل کریں2 const faucetTx = {ہم ٹرانزیکشنز بنانے کے لیے وہ ہائی لیول فنکشنز استعمال نہیں کر سکتے جو ہم عام طور پر استعمال کرتے ہیں (جیسے token.faucet())، کیونکہ ہم ABI کی پیروی نہیں کرتے۔ اس کے بجائے، ہمیں ٹرانزیکشن خود بنانی ہوگی اور پھر اسے بھیجنا ہوگا۔
1 to: cdi.address,2 data: "0x01"ٹرانزیکشن کے لیے ہمیں دو پیرامیٹرز فراہم کرنے کی ضرورت ہے:
to، منزل کا پتہ۔ یہ کال ڈیٹا انٹرپریٹر کانٹریکٹ ہے۔data، بھیجنے کے لیے کال ڈیٹا۔ faucet کال کی صورت میں، ڈیٹا ایک واحد بائٹ،0x01ہوتا ہے۔
12 }3 await (await signer.sendTransaction(faucetTx)).wait()ہم سائنر (signer) کے sendTransaction طریقہ کار (opens in a new tab) کو کال کرتے ہیں کیونکہ ہم نے پہلے ہی منزل (faucetTx.to) بتا دی ہے اور ہمیں ٹرانزیکشن پر دستخط کرنے کی ضرورت ہے۔
1// چیک کریں کہ faucet ٹوکنز درست طریقے سے فراہم کرتا ہے2expect(await token.balanceOf(signer.address)).to.equal(1000)یہاں ہم بیلنس کی تصدیق کرتے ہیں۔ view فنکشنز پر گیس بچانے کی کوئی ضرورت نہیں ہے، اس لیے ہم انہیں بس عام طریقے سے چلاتے ہیں۔
1// CDI کو الاؤنس دیں (اپروولز کو پراکسی نہیں کیا جا سکتا)2const approveTX = await token.approve(cdi.address, 10000)3await approveTX.wait()4expect(await token.allowance(signer.address, cdi.address)).to.equal(10000)کال ڈیٹا انٹرپریٹر کو الاؤنس (allowance) دیں تاکہ وہ منتقلی کر سکے۔
1// ٹوکنز ٹرانسفر کریں2const destAddr = "0xf5a6ead936fb47f342bb63e676479bddf26ebe1d"3const transferTx = {4 to: cdi.address,5 data: "0x02" + destAddr.slice(2, 42) + "0100",6}ایک ٹرانسفر ٹرانزیکشن بنائیں۔ پہلا بائٹ "0x02" ہے، اس کے بعد منزل کا پتہ، اور آخر میں رقم (0x0100، جو اعشاریہ میں 256 ہے)۔
1 await (await signer.sendTransaction(transferTx)).wait()23 // چیک کریں کہ ہمارے پاس 256 ٹوکنز کم ہیں4 expect (await token.balanceOf(signer.address)).to.equal(1000-256)56 // اور یہ کہ ہماری منزل کو وہ مل گئے ہیں7 expect (await token.balanceOf(destAddr)).to.equal(256)8 }) // it9}) // describeسب دکھائیںجب آپ کا منزل کے کانٹریکٹ پر کنٹرول ہو تو لاگت کو کم کرنا
اگر آپ کا منزل کے کانٹریکٹ پر کنٹرول ہے تو آپ ایسے فنکشنز بنا سکتے ہیں جو msg.sender چیکس کو نظرانداز کر دیں کیونکہ وہ کال ڈیٹا انٹرپریٹر پر بھروسہ کرتے ہیں۔ آپ اس کی ایک مثال یہاں دیکھ سکتے ہیں کہ یہ کیسے کام کرتا ہے، control-contract برانچ میں (opens in a new tab)۔
اگر کانٹریکٹ صرف بیرونی ٹرانزیکشنز کا جواب دے رہا ہوتا، تو ہم صرف ایک کانٹریکٹ رکھ کر کام چلا سکتے تھے۔ تاہم، اس سے کمپوزایبلٹی (composability) ٹوٹ جائے گی۔ یہ بہت بہتر ہے کہ ایک ایسا کانٹریکٹ ہو جو عام ERC-20 کالز کا جواب دے، اور دوسرا کانٹریکٹ جو مختصر کال ڈیٹا والی ٹرانزیکشنز کا جواب دے۔
Token.sol
اس مثال میں ہم Token.sol میں ترمیم کر سکتے ہیں۔ اس سے ہمیں کئی ایسے فنکشنز رکھنے کی اجازت ملتی ہے جنہیں صرف پراکسی کال کر سکتی ہے۔ یہاں نئے حصے ہیں:
1 // وہ واحد ایڈریس جسے CalldataInterpreter کا ایڈریس متعین کرنے کی اجازت ہے2 address owner;34 // CalldataInterpreter کا ایڈریس5 address proxy = address(0);ERC-20 کانٹریکٹ کو مجاز پراکسی کی شناخت جاننے کی ضرورت ہوتی ہے۔ تاہم، ہم اس متغیر کو کنسٹرکٹر (constructor) میں سیٹ نہیں کر سکتے، کیونکہ ہم ابھی تک اس کی قدر نہیں جانتے۔ یہ کانٹریکٹ پہلے انسٹینشی ایٹ (instantiate) کیا جاتا ہے کیونکہ پراکسی اپنے کنسٹرکٹر میں ٹوکن کے پتے کی توقع کرتی ہے۔
1 /* *2 * @dev ERC20 کنسٹرکٹر کو کال کرتا ہے۔ */3 constructor(4 ) ERC20("Oris useless token-2", "OUT-2") {5 owner = msg.sender;6 }بنانے والے کا پتہ (جسے owner کہا جاتا ہے) یہاں محفوظ کیا جاتا ہے کیونکہ یہی واحد پتہ ہے جسے پراکسی سیٹ کرنے کی اجازت ہے۔
1 /* *2 * @dev پراکسی (CalldataInterpreter) کے لیے ایڈریس سیٹ کریں۔3 * اسے مالک کی طرف سے صرف ایک بار کال کیا جا سکتا ہے */4 function setProxy(address _proxy) external {5 require(msg.sender == owner, "Can only be called by owner");6 require(proxy == address(0), "Proxy is already set");78 proxy = _proxy;9 } // function setProxyسب دکھائیںپراکسی کو مراعات یافتہ رسائی حاصل ہے، کیونکہ یہ سیکیورٹی چیکس کو نظرانداز کر سکتی ہے۔ اس بات کو یقینی بنانے کے لیے کہ ہم پراکسی پر بھروسہ کر سکتے ہیں، ہم صرف owner کو اس فنکشن کو کال کرنے دیتے ہیں، اور وہ بھی صرف ایک بار۔ ایک بار جب proxy کی کوئی حقیقی قدر (صفر نہیں) ہو جاتی ہے، تو وہ قدر تبدیل نہیں ہو سکتی، اس لیے اگر مالک دھوکہ دینے کا فیصلہ بھی کر لے، یا اس کا نیمونک (mnemonic) ظاہر ہو جائے، تب بھی ہم محفوظ رہتے ہیں۔
1 /* *2 * @dev کچھ فنکشنز کو صرف پراکسی کے ذریعے کال کیا جا سکتا ہے۔ */3 modifier onlyProxy {یہ ایک modifier فنکشن (opens in a new tab) ہے، یہ دوسرے فنکشنز کے کام کرنے کے طریقے کو تبدیل کرتا ہے۔
1 require(msg.sender == proxy);سب سے پہلے، تصدیق کریں کہ ہمیں پراکسی نے کال کیا ہے اور کسی اور نے نہیں۔ اگر نہیں، تو revert کریں۔
1 _;2 }اگر ایسا ہے، تو وہ فنکشن چلائیں جس میں ہم ترمیم کرتے ہیں۔
1 /* وہ فنکشنز جو پراکسی کو دراصل اکاؤنٹس کے لیے پراکسی کرنے کی اجازت دیتے ہیں */23 function transferProxy(address from, address to, uint256 amount)4 public virtual onlyProxy() returns (bool)5 {6 _transfer(from, to, amount);7 return true;8 }910 function approveProxy(address from, address spender, uint256 amount)11 public virtual onlyProxy() returns (bool)12 {13 _approve(from, spender, amount);14 return true;15 }1617 function transferFromProxy(18 address spender,19 address from,20 address to,21 uint256 amount22 ) public virtual onlyProxy() returns (bool)23 {24 _spendAllowance(from, spender, amount);25 _transfer(from, to, amount);26 return true;27 }سب دکھائیںیہ تین ایسے آپریشنز ہیں جن میں عام طور پر پیغام کا براہ راست اس ہستی سے آنا ضروری ہوتا ہے جو ٹوکنز منتقل کر رہی ہو یا الاؤنس منظور کر رہی ہو۔ یہاں ہمارے پاس ان آپریشنز کا ایک پراکسی ورژن ہے جو:
onlyProxy()کے ذریعے تبدیل کیا گیا ہے تاکہ کسی اور کو انہیں کنٹرول کرنے کی اجازت نہ ہو۔- وہ پتہ حاصل کرتا ہے جو عام طور پر ایک اضافی پیرامیٹر کے طور پر
msg.senderہوتا ہے۔
CalldataInterpreter.sol
کال ڈیٹا انٹرپریٹر تقریباً اوپر والے جیسا ہی ہے، سوائے اس کے کہ پراکسیڈ فنکشنز کو ایک msg.sender پیرامیٹر ملتا ہے اور transfer کے لیے کسی الاؤنس کی ضرورت نہیں ہوتی۔
1 // ٹرانسفر (الاؤنس کی ضرورت نہیں)2 if (_func == 2) {3 token.transferProxy(4 msg.sender,5 address(uint160(calldataVal(1, 20))),6 calldataVal(21, 2)7 );8 }910 // approve11 if (_func == 3) {12 token.approveProxy(13 msg.sender,14 address(uint160(calldataVal(1, 20))),15 calldataVal(21, 2)16 );17 }1819 // transferFrom20 if (_func == 4) {21 token.transferFromProxy(22 msg.sender,23 address(uint160(calldataVal( 1, 20))),24 address(uint160(calldataVal(21, 20))),25 calldataVal(41, 2)26 );27 }سب دکھائیںTest.js
پچھلے ٹیسٹنگ کوڈ اور اس کوڈ کے درمیان کچھ تبدیلیاں ہیں۔
1const Cdi = await ethers.getContractFactory("CalldataInterpreter")2const cdi = await Cdi.deploy(token.address)3await cdi.deployed()4await token.setProxy(cdi.address)ہمیں ERC-20 کانٹریکٹ کو بتانا ہوگا کہ کس پراکسی پر بھروسہ کرنا ہے
1console.log("CalldataInterpreter addr:", cdi.address)23// الاؤنسز کی تصدیق کے لیے دو سائنرز کی ضرورت ہے4const signers = await ethers.getSigners()5const signer = signers[0]6const poorSigner = signers[1]approve() اور transferFrom() کو چیک کرنے کے لیے ہمیں ایک دوسرے سائنر کی ضرورت ہے۔ ہم اسے poorSigner کہتے ہیں کیونکہ اسے ہمارے کوئی ٹوکن نہیں ملتے (یقیناً اس کے پاس ETH ہونا ضروری ہے)۔
1// ٹوکنز ٹرانسفر کریں2const destAddr = "0xf5a6ead936fb47f342bb63e676479bddf26ebe1d"3const transferTx = {4 to: cdi.address,5 data: "0x02" + destAddr.slice(2, 42) + "0100",6}7await (await signer.sendTransaction(transferTx)).wait()چونکہ ERC-20 کانٹریکٹ پراکسی (cdi) پر بھروسہ کرتا ہے، اس لیے ہمیں ٹرانسفرز کو ریلے (relay) کرنے کے لیے کسی الاؤنس کی ضرورت نہیں ہے۔
1// approval اور transferFrom2const approveTx = {3 to: cdi.address,4 data: "0x03" + poorSigner.address.slice(2, 42) + "00FF",5}6await (await signer.sendTransaction(approveTx)).wait()78const destAddr2 = "0xE1165C689C0c3e9642cA7606F5287e708d846206"910const transferFromTx = {11 to: cdi.address,12 data: "0x04" + signer.address.slice(2, 42) + destAddr2.slice(2, 42) + "00FF",13}14await (await poorSigner.sendTransaction(transferFromTx)).wait()1516// چیک کریں کہ approve / transferFrom کا کمبو درست طریقے سے کیا گیا تھا17expect(await token.balanceOf(destAddr2)).to.equal(255)سب دکھائیںدو نئے فنکشنز کی جانچ کریں۔ نوٹ کریں کہ transferFromTx کو دو ایڈریس پیرامیٹرز کی ضرورت ہوتی ہے: الاؤنس دینے والا اور وصول کرنے والا۔
نتیجہ
Optimism (opens in a new tab) اور Arbitrum (opens in a new tab) دونوں L1 پر لکھے جانے والے کال ڈیٹا کے سائز کو کم کرنے اور اس طرح ٹرانزیکشنز کی لاگت کو کم کرنے کے طریقے تلاش کر رہے ہیں۔ تاہم، عام حل تلاش کرنے والے انفراسٹرکچر فراہم کنندگان کے طور پر، ہماری صلاحیتیں محدود ہیں۔ ڈیپ (dapp) ڈیولپر کے طور پر، آپ کے پاس ایپلیکیشن سے متعلق مخصوص علم ہوتا ہے، جو آپ کو اپنے کال ڈیٹا کو اس سے کہیں بہتر طریقے سے بہتر بنانے کی اجازت دیتا ہے جتنا ہم کسی عام حل میں کر سکتے ہیں۔ امید ہے کہ یہ مضمون آپ کو اپنی ضروریات کے لیے مثالی حل تلاش کرنے میں مدد دے گا۔
میرے مزید کام کے لیے یہاں دیکھیں (opens in a new tab)۔
صفحہ کی آخری اپ ڈیٹ: 3 مارچ، 2026