کال ڈیٹا کی بہتری کے لیے مختصر ABIs
تعارف
اس مضمون میں، آپ آپٹیمسٹک رول اپس، ان پر ٹرانزیکشنز کی لاگت، اور اس بارے میں جانیں گے کہ لاگت کا یہ مختلف ڈھانچہ ہمیں ایتھیریم مین نیٹ کی نسبت مختلف چیزوں کو بہتر بنانے کا تقاضا کیسے کرتا ہے۔ آپ یہ بھی سیکھیں گے کہ اس بہتری کو کیسے نافذ کیا جائے۔
مکمل انکشاف
میں آپٹیمزم (opens in a new tab) کا کل وقتی ملازم ہوں، اس لیے اس مضمون میں دی گئی مثالیں آپٹیمزم پر چلیں گی۔ تاہم، یہاں بیان کی گئی تکنیک دیگر رول اپس کے لیے بھی اتنی ہی اچھی طرح کام کرنی چاہیے۔
اصطلاحات
رول اپس پر بات کرتے وقت، اصطلاح 'لیئر ۱ (l1)' مین نیٹ، یعنی پروڈکشن ایتھیریم نیٹ ورک کے لیے استعمال ہوتی ہے۔ اصطلاح 'لیئر ۲ (l2)' رول اپ یا کسی دوسرے ایسے سسٹم کے لیے استعمال ہوتی ہے جو سیکیورٹی کے لیے L1 پر انحصار کرتا ہے لیکن اپنی زیادہ تر پروسیسنگ آف چین کرتا ہے۔
ہم L2 ٹرانزیکشنز کی لاگت کو مزید کیسے کم کر سکتے ہیں؟
آپٹیمسٹک رول اپس کو ہر تاریخی ٹرانزیکشن کا ریکارڈ محفوظ کرنا ہوتا ہے تاکہ کوئی بھی ان کا جائزہ لے سکے اور تصدیق کر سکے کہ موجودہ حالت درست ہے۔ ایتھیریم مین نیٹ میں ڈیٹا داخل کرنے کا سب سے سستا طریقہ اسے کال ڈیٹا کے طور پر لکھنا ہے۔ یہ حل آپٹیمزم (opens in a new tab) اور آربٹرم (opens in a new tab) دونوں نے منتخب کیا تھا۔
L2 ٹرانزیکشنز کی لاگت
L2 ٹرانزیکشنز کی لاگت دو اجزاء پر مشتمل ہوتی ہے:
- L2 پروسیسنگ، جو عام طور پر انتہائی سستی ہوتی ہے
- L1 اسٹوریج، جو مین نیٹ گیس کی لاگت سے منسلک ہے
جب میں یہ لکھ رہا ہوں، آپٹیمزم پر L2 گیس کی لاگت 0.001 Gwei ہے۔ دوسری طرف، L1 گیس کی لاگت تقریباً 40 Gwei ہے۔ آپ موجودہ قیمتیں یہاں دیکھ سکتے ہیں (opens in a new tab)۔
کال ڈیٹا کے ایک بائٹ کی لاگت یا تو 4 گیس (اگر یہ صفر ہے) یا 16 گیس (اگر یہ کوئی اور قدر ہے) ہوتی ہے۔ EVM پر سب سے مہنگے آپریشنز میں سے ایک اسٹوریج میں لکھنا ہے۔ L2 پر اسٹوریج میں 32-byte کا لفظ لکھنے کی زیادہ سے زیادہ لاگت 22,100 گیس ہے۔ فی الحال، یہ 22.1 Gwei ہے۔ لہذا اگر ہم کال ڈیٹا کا ایک بھی صفر بائٹ بچا سکیں، تو ہم اسٹوریج میں تقریباً 200 bytes لکھنے کے قابل ہو جائیں گے اور پھر بھی فائدے میں رہیں گے۔
ABI
ٹرانزیکشنز کی اکثریت ایک بیرونی ملکیت والے اکاؤنٹ سے کنٹریکٹ تک رسائی حاصل کرتی ہے۔ زیادہ تر کنٹریکٹس Solidity میں لکھے جاتے ہیں اور ایپلیکیشن بائنری انٹرفیس (ABI) (opens in a new tab) کے مطابق اپنے ڈیٹا فیلڈ کی تشریح کرتے ہیں۔
تاہم، ABI کو L1 کے لیے ڈیزائن کیا گیا تھا، جہاں کال ڈیٹا کے ایک بائٹ کی لاگت تقریباً چار ریاضیاتی آپریشنز کے برابر ہوتی ہے، نہ کہ L2 کے لیے جہاں کال ڈیٹا کے ایک بائٹ کی لاگت ایک ہزار سے زیادہ ریاضیاتی آپریشنز کے برابر ہوتی ہے۔ کال ڈیٹا کو اس طرح تقسیم کیا گیا ہے:
| سیکشن | لمبائی | بائٹس | ضائع شدہ بائٹس | ضائع شدہ گیس | ضروری بائٹس | ضروری گیس |
|---|---|---|---|---|---|---|
| فنکشن سلیکٹر | 4 | 0-3 | 3 | 48 | 1 | 16 |
| صفر | 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)۔
- صفر: یہ بائٹس ہمیشہ صفر ہوتے ہیں کیونکہ بیس بائٹ کے پتے کو رکھنے کے لیے بتیس بائٹ کے لفظ کی ضرورت نہیں ہوتی۔
صفر رکھنے والے بائٹس کی لاگت چار گیس ہوتی ہے (یلو پیپر دیکھیں (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 bytes ہوتے ہیں۔
لہذا کل لاگت 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 صرف ٹیسٹنگ کی سہولت کے لیے موجود ہو تو یہ زندگی کو آسان بنا دیتا ہے۔
/**
* @dev کالر کو کھیلنے کے لیے 1000 ٹوکن دیتا ہے
*/
function faucet() external {
_mint(msg.sender, 1000);
} // function faucet
CalldataInterpreter.sol
یہ وہ کنٹریکٹ ہے جسے ٹرانزیکشنز کو مختصر کال ڈیٹا کے ساتھ کال کرنا چاہیے (opens in a new tab)۔ آئیے اس کا لائن بہ لائن جائزہ لیتے ہیں۔
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
import { OrisUselessToken } from "./Token.sol";
ہمیں ٹوکن فنکشن کی ضرورت ہے تاکہ معلوم ہو سکے کہ اسے کیسے کال کرنا ہے۔
کنٹریکٹ CalldataInterpreter {
OrisUselessToken public immutable token;
اس ٹوکن کا پتہ جس کے لیے ہم پراکسی ہیں۔
/**
* @dev ٹوکن کا پتہ متعین کریں
* @param tokenAddr_ ERC-20 کنٹریکٹ کا پتہ
*/
کنسٹرکٹر(
address tokenAddr_
) {
token = OrisUselessToken(tokenAddr_);
} // constructor
ٹوکن کا پتہ واحد پیرامیٹر ہے جسے ہمیں بتانے کی ضرورت ہے۔
function calldataVal(uint startByte, uint length)
private pure returns (uint) {
کال ڈیٹا سے ایک قدر پڑھیں۔
uint _retVal;
require(length < 0x21,
"calldataVal length limit is 32 bytes");
require(length + startByte <= msg.data.length,
"calldataVal trying to read beyond calldatasize");
ہم میموری میں ایک 32-byte (256-bit) کا لفظ لوڈ کرنے جا رہے ہیں اور ان بائٹس کو ہٹا دیں گے جو ہمارے مطلوبہ فیلڈ کا حصہ نہیں ہیں۔ یہ الگورتھم 32 bytes سے لمبی قدروں کے لیے کام نہیں کرتا، اور یقیناً ہم کال ڈیٹا کے اختتام سے آگے نہیں پڑھ سکتے۔ L1 پر گیس بچانے کے لیے ان ٹیسٹس کو چھوڑنا ضروری ہو سکتا ہے، لیکن L2 پر گیس انتہائی سستی ہے، جو ہمیں ہر قسم کے ضروری چیکس شامل کرنے کے قابل بناتی ہے۔
assembly {
_retVal := calldataload(startByte)
}
ہم fallback() کی کال سے ڈیٹا کاپی کر سکتے تھے (نیچے دیکھیں)، لیکن EVM کی اسمبلی زبان، Yul (opens in a new tab) کا استعمال کرنا زیادہ آسان ہے۔
یہاں ہم اسٹیک میں بائٹس startByte سے startByte+31 تک پڑھنے کے لیے CALLDATALOAD آپ کوڈ (opens in a new tab) استعمال کرتے ہیں۔
عام طور پر، Yul میں آپ کوڈ کا سنٹیکس <opcode name>(<first stack value, if any>,<second stack value, if any>...) ہوتا ہے۔
_retVal = _retVal >> (256-length*8);
صرف سب سے اہم length بائٹس فیلڈ کا حصہ ہیں، اس لیے ہم دیگر قدروں سے چھٹکارا پانے کے لیے رائٹ شفٹ (opens in a new tab) کرتے ہیں۔
اس کا ایک اضافی فائدہ یہ ہے کہ قدر فیلڈ کے دائیں طرف منتقل ہو جاتی ہے، اس لیے یہ قدر بذات خود ہوتی ہے نہ کہ قدر ضرب 256something۔
return _retVal;
}
fallback() external {
جب کسی Solidity کنٹریکٹ کی کال کسی بھی فنکشن کے دستخطوں سے میل نہیں کھاتی، تو یہ fallback() فنکشن (opens in a new tab) کو کال کرتی ہے (یہ فرض کرتے ہوئے کہ وہاں ایک موجود ہے)۔
CalldataInterpreter کے معاملے میں، کوئی بھی کال یہاں آتی ہے کیونکہ کوئی اور external یا public فنکشنز نہیں ہیں۔
uint _func;
_func = calldataVal(0, 1);
کال ڈیٹا کا پہلا بائٹ پڑھیں، جو ہمیں فنکشن بتاتا ہے۔ یہاں کسی فنکشن کے دستیاب نہ ہونے کی دو وجوہات ہیں:
- وہ فنکشنز جو
pureیاviewہیں، حالت کو تبدیل نہیں کرتے اور ان پر گیس خرچ نہیں ہوتی (جب آف چین کال کیا جائے)۔ ان کی گیس کی لاگت کو کم کرنے کی کوشش کرنے کا کوئی فائدہ نہیں ہے۔ - وہ فنکشنز جو
msg.sender(opens in a new tab) پر انحصار کرتے ہیں۔msg.senderکی قدرCalldataInterpreterکا پتہ ہوگی، نہ کہ کال کرنے والے کا۔
بدقسمتی سے، ERC-20 کی خصوصیات کو دیکھتے ہوئے (opens in a new tab)، اس سے صرف ایک فنکشن، transfer بچتا ہے۔
اس سے ہمارے پاس صرف دو فنکشنز بچتے ہیں: transfer (کیونکہ ہم transferFrom کو کال کر سکتے ہیں) اور faucet (کیونکہ ہم ٹوکنز واپس اسی کو منتقل کر سکتے ہیں جس نے ہمیں کال کیا تھا)۔
// ٹوکن کے حالت تبدیل کرنے والے میتھڈز کو استعمال کرتے ہوئے کال کریں
// کال ڈیٹا سے معلومات
// faucet
if (_func == 1) {
faucet() کو ایک کال، جس میں پیرامیٹرز نہیں ہیں۔
token.faucet();
token.transfer(msg.sender,
token.balanceOf(address(this)));
}
جب ہم token.faucet() کو کال کرتے ہیں تو ہمیں ٹوکنز ملتے ہیں۔ تاہم، پراکسی کنٹریکٹ کے طور پر، ہمیں ٹوکنز کی ضرورت نہیں ہے۔
EOA (بیرونی ملکیت والا اکاؤنٹ) یا کنٹریکٹ جس نے ہمیں کال کیا ہے، اسے ضرورت ہوتی ہے۔
اس لیے ہم اپنے تمام ٹوکنز اسی کو منتقل کر دیتے ہیں جس نے ہمیں کال کیا تھا۔
// منتقلی (فرض کریں کہ ہمارے پاس اس کے لیے الاؤنس ہے)
if (_func == 2) {
ٹوکنز منتقل کرنے کے لیے دو پیرامیٹرز کی ضرورت ہوتی ہے: منزل کا پتہ اور رقم۔
token.transferFrom(
msg.sender,
ہم کال کرنے والوں کو صرف وہی ٹوکنز منتقل کرنے کی اجازت دیتے ہیں جن کے وہ مالک ہیں۔
address(uint160(calldataVal(1, 20))),
منزل کا پتہ بائٹ #1 سے شروع ہوتا ہے (بائٹ #0 فنکشن ہے)۔ ایک پتے کے طور پر، یہ 20-bytes لمبا ہوتا ہے۔
calldataVal(21, 2)
اس مخصوص کنٹریکٹ کے لیے ہم فرض کرتے ہیں کہ ٹوکنز کی زیادہ سے زیادہ تعداد جسے کوئی بھی منتقل کرنا چاہے گا، وہ دو بائٹس میں سما سکتی ہے (65536 سے کم)۔
);
}
مجموعی طور پر، ایک منتقلی میں کال ڈیٹا کے 35 bytes لگتے ہیں:
| سیکشن | لمبائی | بائٹس |
|---|---|---|
| فنکشن سلیکٹر | 1 | 0 |
| منزل کا پتہ | 32 | 1-32 |
| رقم | 2 | 33-34 |
} // fallback
} // contract CalldataInterpreter
test.js
یہ JavaScript یونٹ ٹیسٹ (opens in a new tab) ہمیں دکھاتا ہے کہ اس طریقہ کار کو کیسے استعمال کیا جائے (اور یہ کیسے تصدیق کی جائے کہ یہ صحیح طریقے سے کام کرتا ہے)۔ میں یہ فرض کرنے جا رہا ہوں کہ آپ chai (opens in a new tab) اور ethers (opens in a new tab) کو سمجھتے ہیں اور صرف ان حصوں کی وضاحت کروں گا جو خاص طور پر کنٹریکٹ پر لاگو ہوتے ہیں۔
const { expect } = require("chai");
describe("CalldataInterpreter", function () {
it("Should let us use tokens", async function () {
const Token = await ethers.getContractFactory("OrisUselessToken")
const token = await Token.deploy()
await token.deployed()
console.log("Token addr:", token.address)
const Cdi = await ethers.getContractFactory("CalldataInterpreter")
const cdi = await Cdi.deploy(token.address)
await cdi.deployed()
console.log("CalldataInterpreter addr:", cdi.address)
const signer = await ethers.getSigner()
ہم دونوں کنٹریکٹس کو ڈیپلائے کر کے شروعات کرتے ہیں۔
// کھیلنے کے لیے ٹوکن حاصل کریں
const faucetTx = {
ہم ٹرانزیکشنز بنانے کے لیے وہ اعلیٰ سطحی فنکشنز استعمال نہیں کر سکتے جو ہم عام طور پر استعمال کرتے ہیں (جیسے token.faucet())، کیونکہ ہم ABI کی پیروی نہیں کرتے۔
اس کے بجائے، ہمیں ٹرانزیکشن خود بنانی ہوگی اور پھر اسے بھیجنا ہوگا۔
to: cdi.address,
data: "0x01"
ٹرانزیکشن کے لیے ہمیں دو پیرامیٹرز فراہم کرنے کی ضرورت ہے:
to، منزل کا پتہ۔ یہ کال ڈیٹا انٹرپریٹر کنٹریکٹ ہے۔data، بھیجنے کے لیے کال ڈیٹا۔ فوسٹ کال کی صورت میں، ڈیٹا ایک ہی بائٹ،0x01ہوتا ہے۔
}
await (await signer.sendTransaction(faucetTx)).wait()
ہم سائنر کے sendTransaction طریقہ کار (opens in a new tab) کو کال کرتے ہیں کیونکہ ہم نے پہلے ہی منزل (faucetTx.to) بتا دی ہے اور ہمیں ٹرانزیکشن پر دستخط کرنے کی ضرورت ہے۔
// چیک کریں کہ faucet ٹوکن درست طریقے سے فراہم کرتا ہے
expect(await token.balanceOf(signer.address)).to.equal(1000)
یہاں ہم بیلنس کی تصدیق کرتے ہیں۔
view فنکشنز پر گیس بچانے کی کوئی ضرورت نہیں ہے، اس لیے ہم انہیں بس عام طریقے سے چلاتے ہیں۔
// CDI کو الاؤنس دیں (منظوریوں کو پراکسی نہیں کیا جا سکتا)
const approveTX = await token.approve(cdi.address, 10000)
await approveTX.wait()
expect(await token.allowance(signer.address, cdi.address)).to.equal(10000)
کال ڈیٹا انٹرپریٹر کو منتقلی کرنے کے قابل ہونے کے لیے ایک الاؤنس دیں۔
// ٹوکن کی منتقلی
const destAddr = "0xf5a6ead936fb47f342bb63e676479bddf26ebe1d"
const transferTx = {
to: cdi.address,
data: "0x02" + destAddr.slice(2, 42) + "0100",
}
منتقلی کی ٹرانزیکشن بنائیں۔ پہلا بائٹ "0x02" ہے، اس کے بعد منزل کا پتہ، اور آخر میں رقم (0x0100، جو اعشاریہ میں 256 ہے)۔
await (await signer.sendTransaction(transferTx)).wait()
// چیک کریں کہ ہمارے پاس 256 ٹوکن کم ہیں
expect (await token.balanceOf(signer.address)).to.equal(1000-256)
// اور یہ کہ ہماری منزل کو وہ مل گئے ہیں
expect (await token.balanceOf(destAddr)).to.equal(256)
}) // it
}) // describe
جب منزل کے کنٹریکٹ پر آپ کا کنٹرول ہو تو لاگت کو کم کرنا
اگر منزل کے کنٹریکٹ پر آپ کا کنٹرول ہے تو آپ ایسے فنکشنز بنا سکتے ہیں جو msg.sender چیکس کو نظر انداز کر دیں کیونکہ وہ کال ڈیٹا انٹرپریٹر پر بھروسہ کرتے ہیں۔
آپ یہاں control-contract برانچ میں اس کی ایک مثال دیکھ سکتے ہیں کہ یہ کیسے کام کرتا ہے (opens in a new tab)۔
اگر کنٹریکٹ صرف بیرونی ٹرانزیکشنز کا جواب دے رہا ہوتا، تو ہم صرف ایک کنٹریکٹ رکھ کر کام چلا سکتے تھے۔ تاہم، اس سے ترکیب پذیری ٹوٹ جائے گی۔ یہ بہت بہتر ہے کہ ایک ایسا کنٹریکٹ ہو جو عام ERC-20 کالز کا جواب دے، اور دوسرا کنٹریکٹ جو مختصر کال ڈیٹا والی ٹرانزیکشنز کا جواب دے۔
Token.sol
اس مثال میں ہم Token.sol میں ترمیم کر سکتے ہیں۔
یہ ہمیں ایسے کئی فنکشنز رکھنے کی سہولت دیتا ہے جنہیں صرف پراکسی ہی کال کر سکتی ہے۔
یہاں نئے حصے ہیں:
// واحد پتہ جسے CalldataInterpreter کا پتہ متعین کرنے کی اجازت ہے
address owner;
// CalldataInterpreter کا پتہ
address proxy = address(0);
ERC-20 کنٹریکٹ کو مجاز پراکسی کی شناخت جاننے کی ضرورت ہوتی ہے۔ تاہم، ہم اس متغیر کو کنسٹرکٹر میں سیٹ نہیں کر سکتے، کیونکہ ہم ابھی تک اس کی قدر نہیں جانتے۔ یہ کنٹریکٹ پہلے شروع کیا جاتا ہے کیونکہ پراکسی اپنے کنسٹرکٹر میں ٹوکن کے پتے کی توقع کرتی ہے۔
/**
* @dev ERC20 کنسٹرکٹر کو کال کرتا ہے۔
*/
constructor(
) ERC20("Oris useless token-2", "OUT-2") {
owner = msg.sender;
}
بنانے والے کا پتہ (جسے owner کہا جاتا ہے) یہاں محفوظ کیا جاتا ہے کیونکہ یہی واحد پتہ ہے جسے پراکسی سیٹ کرنے کی اجازت ہے۔
/**
* @dev پراکسی (CalldataInterpreter) کے لیے پتہ سیٹ کریں۔
* مالک کی طرف سے صرف ایک بار کال کیا جا سکتا ہے
*/
function setProxy(address _proxy) external {
require(msg.sender == owner, "Can only be called by owner");
require(proxy == address(0), "Proxy is already set");
proxy = _proxy;
} // function setProxy
پراکسی کو مراعات یافتہ رسائی حاصل ہے، کیونکہ یہ سیکیورٹی چیکس کو نظر انداز کر سکتی ہے۔
یہ یقینی بنانے کے لیے کہ ہم پراکسی پر بھروسہ کر سکتے ہیں، ہم صرف owner کو اس فنکشن کو کال کرنے دیتے ہیں، اور وہ بھی صرف ایک بار۔
ایک بار جب proxy کی کوئی حقیقی قدر (صفر نہیں) ہو جاتی ہے، تو وہ قدر تبدیل نہیں ہو سکتی، اس لیے اگر مالک دھوکہ دینے کا فیصلہ بھی کر لے، یا اس کا یادداشت کا جملہ (mnemonic) ظاہر ہو جائے، تب بھی ہم محفوظ رہتے ہیں۔
/**
* @dev کچھ فنکشنز کو صرف پراکسی کے ذریعے کال کیا جا سکتا ہے۔
*/
modifier onlyProxy {
یہ ایک modifier فنکشن (opens in a new tab) ہے، یہ دوسرے فنکشنز کے کام کرنے کے طریقے کو تبدیل کرتا ہے۔
require(msg.sender == proxy);
سب سے پہلے، تصدیق کریں کہ ہمیں پراکسی نے کال کیا ہے اور کسی اور نے نہیں۔
اگر نہیں، تو revert۔
_;
}
اگر ایسا ہے، تو وہ فنکشن چلائیں جس میں ہم ترمیم کرتے ہیں۔
/* وہ فنکشنز جو پراکسی کو دراصل اکاؤنٹس کے لیے پراکسی کرنے کی اجازت دیتے ہیں */
function transferProxy(address from, address to, uint256 amount)
public virtual onlyProxy() returns (bool)
{
_transfer(from, to, amount);
return true;
}
function approveProxy(address from, address spender, uint256 amount)
public virtual onlyProxy() returns (bool)
{
_approve(from, spender, amount);
return true;
}
function transferFromProxy(
address spender,
address from,
address to,
uint256 amount
) public virtual onlyProxy() returns (bool)
{
_spendAllowance(from, spender, amount);
_transfer(from, to, amount);
return true;
}
یہ تین ایسے آپریشنز ہیں جن میں عام طور پر یہ تقاضا ہوتا ہے کہ پیغام براہ راست اس ہستی کی طرف سے آئے جو ٹوکنز منتقل کر رہی ہو یا الاؤنس منظور کر رہی ہو۔ یہاں ہمارے پاس ان آپریشنز کا ایک پراکسی ورژن ہے جو:
onlyProxy()کے ذریعے تبدیل کیا گیا ہے تاکہ کسی اور کو انہیں کنٹرول کرنے کی اجازت نہ ہو۔- وہ پتہ حاصل کرتا ہے جو عام طور پر ایک اضافی پیرامیٹر کے طور پر
msg.senderہوتا ہے۔
CalldataInterpreter.sol
کال ڈیٹا انٹرپریٹر تقریباً اوپر والے جیسا ہی ہے، سوائے اس کے کہ پراکسی کیے گئے فنکشنز کو ایک msg.sender پیرامیٹر ملتا ہے اور transfer کے لیے کسی الاؤنس کی ضرورت نہیں ہوتی۔
// منتقلی (الاؤنس کی ضرورت نہیں)
if (_func == 2) {
token.transferProxy(
msg.sender,
address(uint160(calldataVal(1, 20))),
calldataVal(21, 2)
);
}
// approve
if (_func == 3) {
token.approveProxy(
msg.sender,
address(uint160(calldataVal(1, 20))),
calldataVal(21, 2)
);
}
// transferFrom
if (_func == 4) {
token.transferFromProxy(
msg.sender,
address(uint160(calldataVal( 1, 20))),
address(uint160(calldataVal(21, 20))),
calldataVal(41, 2)
);
}
Test.js
پچھلے ٹیسٹنگ کوڈ اور اس کوڈ کے درمیان کچھ تبدیلیاں ہیں۔
const Cdi = await ethers.getContractFactory("CalldataInterpreter")
const cdi = await Cdi.deploy(token.address)
await cdi.deployed()
await token.setProxy(cdi.address)
ہمیں ERC-20 کنٹریکٹ کو بتانا ہوگا کہ کس پراکسی پر بھروسہ کرنا ہے۔
console.log("CalldataInterpreter addr:", cdi.address)
// الاؤنسز کی تصدیق کے لیے دو دستخط کنندگان کی ضرورت ہے
const signers = await ethers.getSigners()
const signer = signers[0]
const poorSigner = signers[1]
approve() اور transferFrom() کو چیک کرنے کے لیے ہمیں ایک دوسرے سائنر کی ضرورت ہے۔
ہم اسے poorSigner کہتے ہیں کیونکہ اسے ہمارے کوئی ٹوکنز نہیں ملتے (یقیناً اس کے پاس ETH ہونا ضروری ہے)۔
// ٹوکن کی منتقلی
const destAddr = "0xf5a6ead936fb47f342bb63e676479bddf26ebe1d"
const transferTx = {
to: cdi.address,
data: "0x02" + destAddr.slice(2, 42) + "0100",
}
await (await signer.sendTransaction(transferTx)).wait()
چونکہ ERC-20 کنٹریکٹ پراکسی (cdi) پر بھروسہ کرتا ہے، اس لیے ہمیں منتقلی کو آگے بھیجنے کے لیے کسی الاؤنس کی ضرورت نہیں ہے۔
// منظوری اور transferFrom
const approveTx = {
to: cdi.address,
data: "0x03" + poorSigner.address.slice(2, 42) + "00FF",
}
await (await signer.sendTransaction(approveTx)).wait()
const destAddr2 = "0xE1165C689C0c3e9642cA7606F5287e708d846206"
const transferFromTx = {
to: cdi.address,
data: "0x04" + signer.address.slice(2, 42) + destAddr2.slice(2, 42) + "00FF",
}
await (await poorSigner.sendTransaction(transferFromTx)).wait()
// چیک کریں کہ approve / transferFrom کومبو درست طریقے سے کیا گیا تھا
expect(await token.balanceOf(destAddr2)).to.equal(255)
دو نئے فنکشنز کی جانچ کریں۔
نوٹ کریں کہ transferFromTx کو دو پتے کے پیرامیٹرز کی ضرورت ہوتی ہے: الاؤنس دینے والا اور وصول کرنے والا۔
نتیجہ
آپٹیمزم (opens in a new tab) اور آربٹرم (opens in a new tab) دونوں L1 پر لکھے گئے کال ڈیٹا کے سائز کو کم کرنے اور اس طرح ٹرانزیکشنز کی لاگت کو کم کرنے کے طریقے تلاش کر رہے ہیں۔ تاہم، عام حل تلاش کرنے والے انفراسٹرکچر فراہم کنندگان کے طور پر، ہماری صلاحیتیں محدود ہیں۔ غیر مرکزی ایپلی کیشن (dapp) کے ڈویلپر کے طور پر، آپ کے پاس ایپلی کیشن سے متعلق مخصوص علم ہوتا ہے، جو آپ کو اپنے کال ڈیٹا کو اس سے کہیں بہتر طریقے سے بہتر بنانے کی سہولت دیتا ہے جتنا ہم کسی عام حل میں کر سکتے ہیں۔ امید ہے کہ یہ مضمون آپ کو اپنی ضروریات کے لیے مثالی حل تلاش کرنے میں مدد کرے گا۔