اپنے گیس کے بغیر صارفین کو ٹوکن رکھنے اور کنٹریکٹس کال کرنے کی اجازت دینا
تعارف
ایک پچھلے مضمون میں EIP-712 دستخطوں کا استعمال کرتے ہوئے آپ کی اپنی ایپلیکیشن تک گیس کے بغیر رسائی کے استعمال پر تبادلہ خیال کیا گیا تھا، لیکن یہ آپ کے اپنے سمارٹ کنٹریکٹس تک محدود ہے۔ اکاؤنٹ کی تجرید کا استعمال کرتے ہوئے، ہم ایسے سمارٹ کنٹریکٹ والیٹس بنا سکتے ہیں جو دو قسم کی ٹرانزیکشنز کو قبول کرتے ہیں اور انہیں مطلوبہ منزل تک پہنچاتے ہیں:
- کسی مخصوص EOA کے ذریعے بھیجی گئی ٹرانزیکشنز (جن کے لیے اس EOA کے پاس ETH ہونا ضروری ہے)
- کہیں سے بھی بھیجی گئی ٹرانزیکشنز، لیکن اسی EOA کے ذریعے دستخط شدہ۔
اس طرح، ہم کسی اکاؤنٹ کو اثاثے (ٹوکنز وغیرہ) رکھنے اور وہ تمام افعال انجام دینے کے لیے گیس کے بغیر طریقہ فراہم کر سکتے ہیں جو گیس والا EOA کر سکتا ہے۔
ہم صرف درخواست کو آگے کیوں نہیں بھیج سکتے؟
ERC-20 اور متعلقہ معیارات میں، اکاؤنٹ کا مالک msg.sender (opens in a new tab) ہوتا ہے، وہ پتہ جس نے ٹوکن کنٹریکٹ کو کال کیا، جو ضروری نہیں کہ ٹرانزیکشن شروع کرنے والا، tx.origin (opens in a new tab) ہو۔ یہ سیکیورٹی وجوہات (opens in a new tab) کی بنا پر ضروری ہے۔ اس کا مطلب یہ ہے کہ اگر ہم ٹوکن کی منتقلی کی درخواستوں کو آگے بھیجتے ہیں، تو وہ صارف کے زیر کنٹرول پتے کے بجائے ریلے کرنے والے کے پتے سے ٹوکن منتقل کرنے کی کوشش کریں گے۔
ایک حل موجود ہے جو آپ کو EIP-7702 (opens in a new tab) کے ذریعے EOA پتہ استعمال کرنے کی اجازت دیتا ہے، لیکن اس کے لیے ممکنہ طور پر خطرناک تفویض پر دستخط کرنے کی ضرورت ہوتی ہے، لہذا آپ اسے صرف ایسے سمارٹ کنٹریکٹ کو تفویض کرنے کے لیے استعمال کر سکتے ہیں جسے والیٹ فراہم کنندہ منظور کرتا ہو۔ اس ٹیوٹوریل کے لیے میں صارف کے پراکسی کے طور پر سمارٹ کنٹریکٹ بنانے کے بہت آسان طریقے کو ترجیح دیتا ہوں۔
اسے عملی شکل میں دیکھنا
-
یقینی بنائیں کہ آپ کے پاس Node (opens in a new tab) اور Foundry (opens in a new tab) دونوں موجود ہیں۔
-
ایپلیکیشن کو کلون کریں اور ضروری سافٹ ویئر انسٹال کریں۔
git clone https://github.com/qbzzt/260315-gasless-tokens.git cd 260315-gasless-tokens forge build cd server npm install -
SEPOLIA_PRIVATE_KEYکو ایسے والیٹ پر سیٹ کرنے کے لیے.envمیں ترمیم کریں جس کے پاس Sepolia پر ETH ہو۔ اگر آپ کو Sepolia ETH کی ضرورت ہے، تو اسے حاصل کرنے کے لیے فوسٹ کا استعمال کریں۔ مثالی طور پر، یہ نجی کلید اس کلید سے مختلف ہونی چاہیے جو آپ کے براؤزر والیٹ میں ہے۔ -
سرور شروع کریں۔
npm run dev -
URL
http://localhost:5173(opens in a new tab) پر ایپلیکیشن براؤز کریں۔ -
والیٹ سے جڑنے کے لیے Connect with Injected پر کلک کریں۔ والیٹ میں منظور کریں، اور اگر ضروری ہو تو Sepolia میں تبدیلی کو منظور کریں۔
-
نیچے سکرول کریں اور Deploy UserProxy (slow process) پر کلک کریں۔
-
آپ دیکھ سکتے ہیں کہ صارف پراکسی کب تعینات ہوتی ہے کیونکہ UserProxy access کے آگے ایک پتہ ہوتا ہے۔ اگر آپ نے 24 سیکنڈ (2 بلاکس) انتظار کیا اور یہ اب بھی نہیں ہوا ہے، تو تبدیلیوں کا پتہ لگانے میں کوئی مسئلہ ہو سکتا ہے۔
اگر ایسا ہے، تو Sepolia ایکسپلورر (opens in a new tab) پر جائیں اور وہ تعیناتی ٹرانزیکشن ہیش درج کریں جو آپ کو
npm run devپر سرور آؤٹ پٹ میں نظر آتا ہے۔ اس کا پتہ دیکھنے کے لیے بنائے گئے کنٹریکٹ پر کلک کریں، پھر اسے کاپی کریں۔ پتے کو Or enter existing proxy address فیلڈ میں پیسٹ کریں، پھر Set proxy address پر کلک کریں۔ -
ٹوکن حاصل کرنے کے لیے ERC-20 کنٹریکٹ کے
faucet(opens in a new tab) فنکشن کو کال جمع کرانے کے لیے Request more tokens for proxy پر کلک کریں۔ والیٹ میں دستخط کی تصدیق کریں۔ یقیناً، ٹوکن صارف کے پتے پر نہیں بلکہ پراکسی کے پتے پر پہنچتے ہیں۔ -
نیچے سکرول کریں اور Last transaction: کے نیچے دیے گئے لنک پر کلک کریں۔ یہ آپ کو
faucetٹرانزیکشن دکھانے کے لیے براؤزر کھول دے گا۔ -
amount to transfer میں، ایک اور ایک ہزار کے درمیان کوئی عدد درج کریں۔ ٹوکنز کو اپنے پتے پر منتقل کرنے کے لیے Transfer پر کلک کریں۔ درخواست کے لیے Confirm پر کلک کرنے سے پہلے، دیکھیں کہ جس ڈیٹا پر دستخط کیے جا رہے ہیں وہ غیر واضح ہے۔ صارفین کو یہ سمجھنے میں مشکل پیش آئے گی کہ وہ کس چیز پر دستخط کر رہے ہیں۔ یاد رکھیں کہ ہم اس پر نیچے تبادلہ خیال کریں گے۔
-
ٹرانزیکشن کی تصدیق ہونے کے بعد، your balance اور proxy balance دونوں میں تبدیلی دیکھنے کا انتظار کریں۔ نوٹ کریں کہ اس میں بھی کچھ وقت لگے گا، کیونکہ Sepolia کا بلاک کا وقت 12 سیکنڈ ہے۔
یہ کیسے کام کرتا ہے
گیس کے بغیر تجربے کے لیے، ہمیں صارف کے لیے ایک یوزر انٹرفیس، یوزر انٹرفیس سے پیغامات کو چین تک پہنچانے کے لیے ایک سرور، اور انہیں وصول کرنے اور ان کی تصدیق کرنے کے لیے ایک سمارٹ کنٹریکٹ کی ضرورت ہوتی ہے۔
والیٹ سمارٹ کنٹریکٹ
یہ سمارٹ کنٹریکٹ (opens in a new tab) ہے۔ اس کا مقصد وہ سب کچھ کرنا ہے جو اصل مالک درخواست کرتا ہے، قطع نظر اس کے کہ اس کی درخواست کرنے کے لیے کون سا چینل استعمال کیا گیا ہے، اور باقی سب کچھ نظر انداز کرنا ہے۔ ایسا کرنے کے لیے، اس کے فنکشنز کال کرنے کے لیے ایک ہدف کا پتہ اور اسے کال کرنے کے لیے استعمال ہونے والا ڈیٹا وصول کرتے ہیں۔
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract UserProxy {
address immutable OWNER;
uint public nonce = 0;
مالک کی شناخت اور پیغامات کو دہرائے جانے سے روکنے کے لیے ایک نانس (opens in a new tab)۔ چونکہ نانس ایک public متغیر ہے، اس لیے Solidity کمپائلر ایک ویو فنکشن، nonce() (opens in a new tab) بھی بناتا ہے، جو آف چین کوڈ کو اس کی قدر پڑھنے کی اجازت دیتا ہے۔
bytes32 private constant SIGNED_ACCESS_TYPEHASH =
keccak256("SignedAccess(address target,bytes data,uint256 nonce)");
bytes32 private constant SIGNED_ACCESS_PAYABLE_TYPEHASH =
keccak256("SignedAccessPayable(address target,bytes data,uint256 nonce,uint256 value)");
bytes32 immutable DOMAIN_SEPARATOR;
EIP-712 دستخطوں (opens in a new tab) کی تصدیق کے لیے درکار معلومات۔
constructor(address owner_) {
OWNER = owner_;
ایک UserProxy ایک ہی مالک کے پتے سے منسلک ہوتا ہے۔ یہ ضروری ہے کیونکہ یہ اثاثوں (ERC-20 ٹوکنز، NFTs وغیرہ) کا مالک ہو سکتا ہے۔ ہم مختلف مالکان کے اثاثوں کو آپس میں ملانا نہیں چاہتے۔
DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
),
keccak256(bytes("UserProxy")),
keccak256(bytes("1")),
block.chainid,
address(this)
)
);
}
ڈومین الگ کرنے والا (opens in a new tab)۔ اس کا حساب کمپائل کے وقت نہیں لگایا جا سکتا، کیونکہ یہ چین ID اور کنٹریکٹ کے پتے پر منحصر ہے۔ یہ کسی UserProxy کے لیے دوسرے کے لیے تیار کردہ پیغام سے بے وقوف بننا ناممکن بنا دیتا ہے۔
event CallResult(address target, bytes returnData);
کال کے نتائج کو لاگ کریں۔
function directAccess(address target, bytes calldata data)
external returns (bytes memory) {
اس فنکشن کو مالک براہ راست کال کر سکتا ہے۔ اگر کوئی ریلے دستیاب نہیں ہیں، تو مالک اب بھی بلاک چین پر براہ راست اثاثوں تک رسائی حاصل کر سکتا ہے (اگر صارف کے پاس ETH ہے)۔
require(msg.sender == OWNER, "Only owner can call");
(bool success, bytes memory returnData) = target.call(data);
require(success, "Call failed");
emit CallResult(target, returnData);
return returnData;
}
اگر ہمیں مالک کی طرف سے براہ راست کال کیا جاتا ہے، تو فراہم کردہ کال ڈیٹا کے ساتھ ہدف کو کال کریں۔
function signedAccess(
address target,
bytes calldata data,
uint8 v,
bytes32 r,
bytes32 s)
یہ UserProxy کا بنیادی فنکشن ہے۔ یہ target اور data کے ساتھ ساتھ ایک دستخط بھی حاصل کرتا ہے۔
external returns (bytes memory) {
// EIP-712 ڈائجسٹ کا حساب لگائیں
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(
abi.encode(
SIGNED_ACCESS_TYPEHASH,
target,
keccak256(data),
nonce
)
)
)
);
ڈائجسٹ میں نانس بھی شامل ہوتا ہے، لیکن ہمیں اسے ٹرانزیکشن سے وصول کرنے کی ضرورت نہیں ہے؛ ہم پہلے ہی صحیح قدر جانتے ہیں۔ غلط نانس والے دستخط کو مسترد کر دیا جائے گا۔
// دستخط کنندہ کو بازیافت کریں
address signer = ecrecover(digest, v, r, s);
require(signer == OWNER, "Signature invalid or not by owner");
اگر دستخط غلط ہے، تو ecrecover عام طور پر ایک مختلف پتہ واپس کرے گا، اور اسے قبول نہیں کیا جائے گا۔
(bool success, bytes memory returnData) = target.call(data);
require(success, "Call failed");
اس کنٹریکٹ کو کال کریں جسے صارف نے ہمیں کال کرنے کو کہا تھا، اور اگر کامیاب نہ ہو تو ریورٹ کریں۔
emit CallResult(target, returnData);
nonce++; // ری پلے کو روکنے کے لیے نانس میں اضافہ کریں
return returnData;
}
اگر کامیاب ہو جائے، تو ایک لاگ ایونٹ خارج کریں اور نانس میں اضافہ کریں۔
function directAccessPayable(address target, uint value, bytes calldata data)
external payable returns (bytes memory) {
.
.
.
}
function signedAccessPayable(
.
.
.
}
}
یہ تقریباً ایک جیسی اقسام ہیں جو آپ کو کنٹریکٹ سے ETH منتقل کرنے کی بھی اجازت دیتی ہیں۔
ریلے کرنے والا
ریلے کرنے والا ایک سرور جزو ہے۔ یہ JavaScript میں لکھا گیا ہے؛ آپ سورس کوڈ یہاں (opens in a new tab) دیکھ سکتے ہیں۔
import express from "express";
import { createServer as createViteServer } from "vite";
import { createWalletClient, createPublicClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { sepolia } from 'viem/chains'
import dotenv from 'dotenv'
وہ لائبریریاں جن کی ہمیں ضرورت ہے۔ یہ ایک Express (opens in a new tab) سرور ہے، جو یوزر انٹرفیس کوڈ پیش کرنے کے لیے Vite (opens in a new tab) کا استعمال کرتا ہے۔ ہم بلاک چین کے ساتھ بات چیت کرنے کے لیے Viem (opens in a new tab) کا استعمال کرتے ہیں، اور ٹرانزیکشن بھیجنے والے پتے کے لیے نجی کلید پڑھنے کے لیے dotenv (opens in a new tab) کا استعمال کرتے ہیں۔
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
const UserProxy = require('../contracts/out/UserProxy.sol/UserProxy.json')
یہ مرتب شدہ UserProxy کو پڑھنے کا ایک آسان طریقہ ہے۔ ہمیں UserProxy کو کال کرنے کے قابل ہونے کے لیے ABI کی ضرورت ہے، اور اسے صارف کے لیے تعینات کرنے کے قابل ہونے کے لیے مرتب شدہ کوڈ کی ضرورت ہے۔
dotenv.config()
const sepoliaAccount = privateKeyToAccount(process.env.SEPOLIA_PRIVATE_KEY)
console.log("Using account:", sepoliaAccount.address)
.env فائل پڑھیں، پتہ نکالیں، اور اسے کنسول پر پرنٹ کریں۔
const sepoliaClient = createWalletClient({
account: sepoliaAccount,
chain: sepolia,
transport: http("https://rpc.sentio.xyz/sepolia"),
})
const publicClient = createPublicClient({
chain: sepolia,
transport: http(),
})
Viem کلائنٹس جو بلاک چین سے بات کرتے ہیں۔
const start = async () => {
const app = express()
ایک Express سرور چلائیں۔
app.use(express.json())
Express کو درخواست کا باڈی پڑھنے کو کہیں، اور اگر یہ JSON ہے تو اسے پارس کریں۔
app.post("/server/deploy", async (req, res) => {
یہ وہ کوڈ ہے جو پراکسی کو تعینات کرنے کی درخواستوں کو سنبھالتا ہے۔ نوٹ کریں کہ ہم یہاں ڈینائل آف سروس (opens in a new tab) حملوں کا شکار ہو سکتے ہیں کیونکہ ایک حملہ آور ہمیں پراکسی تعینات کرنے کی درخواستوں کے ساتھ اس وقت تک سپیم کر سکتا ہے جب تک کہ ہمارا ETH ختم نہ ہو جائے۔ پروڈکشن سسٹم پر، ہم شاید یہ تقاضا کریں گے کہ پراکسی تعینات کرنے کی درخواست پر دستخط کیے جائیں اور دستخط کنندہ ایک موجودہ گاہک ہو۔
try {
const ownerAddress = req.body.ownerAddress
درخواست سے مالک کا پتہ حاصل کریں۔
const txHash = await sepoliaClient.deployContract({
abi: UserProxy.abi,
bytecode: UserProxy.bytecode.object,
args: [ownerAddress],
account: sepoliaAccount,
})
console.log("Deployment transaction hash:", txHash)
const receipt = await publicClient.waitForTransactionReceipt({
hash: txHash,
})
کنٹریکٹ تعینات کریں (opens in a new tab) اور اس کے تعینات ہونے تک انتظار کریں (opens in a new tab)۔
res.json({ contractAddress: receipt.contractAddress })
اگر سب کچھ ٹھیک ہے، تو پراکسی کا پتہ یوزر انٹرفیس کو واپس کریں۔
} catch (err) {
console.error(err)
res.status(500).json({ error: err.message })
}
})
اگر کوئی مسئلہ ہے، تو اس کی اطلاع دیں۔
app.post("/server/message", async (req, res) => {
یہ وہ کوڈ ہے جو UserProxy کنٹریکٹ کے لیے صارف کے پیغامات پر کارروائی کرتا ہے۔ یہ ایک اور نقطہ ہے جو ڈینائل آف سروس حملے کا شکار ہو سکتا ہے۔
try {
const { proxy, target, data, v, r, s } = req.body
const txHash = await sepoliaClient.writeContract({
address: proxy,
abi: UserProxy.abi,
functionName: 'signedAccess',
args: [target, data, v, r, s],
account: sepoliaAccount,
})
درخواست کا ڈیٹا حاصل کریں اور اسے پراکسی پر signedAccess کو کال کرنے کے لیے استعمال کریں۔
console.log("Message transaction hash:", txHash)
res.json({ txHash })
ٹرانزیکشن ہیش واپس رپورٹ کریں۔ یہ UI کو صارف کے لیے ٹرانزیکشن چیک کرنے کے لیے ایک URL دکھانے کی اجازت دیتا ہے۔
} catch (err) {
console.error(err)
res.status(500).json({ error: err.message })
}
})
دوبارہ، اگر کوئی مسئلہ ہے، تو اس کی اطلاع دیں۔
// باقی سب کچھ Vite کو سنبھالنے دیں
const vite = await createViteServer({
server: { middlewareMode: true }
})
app.use(vite.middlewares)
app.listen(5173, () => {
console.log("Dev server running on http://localhost:5173");
})
}
start()
باقی ہر چیز کے لیے، Vite کا استعمال کریں، جو ہمارے لیے یوزر انٹرفیس پیش کرنے کا کام سنبھالتا ہے۔
یوزر انٹرفیس
یہ یوزر انٹرفیس کوڈ ہے (opens in a new tab)۔ زیادہ تر کوڈ تقریباً ویسا ہی ہے جیسا کہ اس مضمون میں دستاویزی شکل میں موجود ہے، سوائے Token.jsx (opens in a new tab) کے۔
Token.jsx (opens in a new tab) کے کچھ حصے اس مضمون میں Greeter.jsx (opens in a new tab) سے ملتے جلتے ہیں۔ یہاں نئے حصے ہیں۔
import {
encodeFunctionData
} from 'viem'
یہ فنکشن (opens in a new tab) ایک EVM فنکشن کال کے لیے کال ڈیٹا بناتا ہے۔ یہ ضروری ہے تاکہ صارف کال ڈیٹا پر دستخط کر سکے۔
import UserProxy from '../../contracts/out/UserProxy.sol/UserProxy.json'
UserProxy، جس کی وضاحت اوپر کی گئی ہے۔
import Erc20 from '../../contracts/out/Faucet.sol/FaucetToken.json'
یہ کنٹریکٹ (opens in a new tab) زیادہ تر ایک عام ERC-20 کنٹریکٹ ہے، جس میں ایک اہم فنکشن، faucet() کا اضافہ کیا گیا ہے۔ یہ فنکشن جانچ کے مقاصد کے لیے ان ٹوکنز کی درخواست کرنے والے کسی بھی شخص کو ٹوکن دیتا ہے۔
const erc20Addrs = {
// Sepolia
11155111: '0x4cBedDEDA88fDd9e116618a5cD71BB0E440C2A78'
}
FaucetToken کا پتہ۔
const Address = ({ address }) => {
if (!address) return null
return (
<a href={`https://eth-sepolia.blockscout.com/address/${address}?tab=read_write_contract`} target="_blank">{address}</a>
)
}
یہ جزو بلاک ایکسپلورر پر کنٹریکٹ کے لنک کے ساتھ ایک پتہ آؤٹ پٹ کرتا ہے۔
const Token = () => {
...
یہ وہ بنیادی جزو ہے جو زیادہ تر کام کرتا ہے۔
const [ balanceAmount, setBalanceAmount ] = useState("Loading...")
صارف کے پتے کا ٹوکن بیلنس۔
const [ proxyAddr, setProxyAddr ] = useState(null)
صارف کی ملکیت والی پراکسی کا پتہ۔
const [ proxyBalanceAmount, setProxyBalanceAmount ] = useState("Loading...")
پراکسی کا ٹوکن بیلنس۔
const [ newProxyAddr, setNewProxyAddr ] = useState("")
یہ فیلڈ اس وقت استعمال ہوتی ہے جب صارف دستی طور پر پراکسی کا پتہ سیٹ کرتا ہے۔ پراکسی کا پتہ دستی طور پر سیٹ کرنے کی صلاحیت ہونے سے صارف ہر بار نئی پراکسی تعینات کرنے (اور پرانی پراکسی کی ملکیت والے تمام ٹوکنز کھونے) کے بجائے موجودہ پراکسی استعمال کر سکتا ہے۔
const [ txHash, setTxHash ] = useState(null)
آخری ٹرانزیکشن کا ہیش، جو ایکسپلورر کا لنک دکھانے کے لیے استعمال ہوتا ہے تاکہ صارف اس ٹرانزیکشن کو چیک کر سکے۔
const [ transferToken, setTransferToken ] = useState("")
const [ transferAmount, setTransferAmount ] = useState("")
const [ transferTo, setTransferTo ] = useState("")
یہ تمام فیلڈز ERC-20 کنٹریکٹ کو ٹوکن کی منتقلی کی کمانڈز بھیجنے کے لیے استعمال ہوتی ہیں۔ یہ FaucetToken ہو سکتا ہے، لیکن ایسا ہونا ضروری نہیں ہے۔ transfer فنکشن ERC-20 معیار کا حصہ ہے۔
const balance = useReadContract({
...
})
const proxyBalance = useReadContract({
...
})
ان دو ٹوکن بیلنسز کو پڑھیں جن میں ہماری دلچسپی ہے، صارف کے پاس کتنا ہے، اور پراکسی کے پاس کتنا ہے۔
const nonce = useReadContract({
address: proxyAddr,
abi: UserProxy.abi,
functionName: 'nonce',
args: [],
})
ری پلے حملوں کو روکنے کے لیے (مثال کے طور پر، کوئی بیچنے والا ایسی ٹرانزیکشن کو ری پلے کرے جو اسے پیسے دیتی ہے)، ہم ایک نانس (opens in a new tab) استعمال کرتے ہیں۔ ہمیں موجودہ قدر جاننے کی ضرورت ہے تاکہ اسے اس ڈیٹا میں شامل کیا جا سکے جس پر ہم دستخط کرتے ہیں۔
useEffect(() => {
if (balance?.status === "success")
setBalanceAmount(balance.data / 10n**18n)
else
setBalanceAmount("Loading...")
}, [balance])
useEffect(() => {
if (proxyBalance?.status === "success")
setProxyBalanceAmount(proxyBalance.data / 10n**18n)
else
setProxyBalanceAmount("Loading...")
}, [proxyBalance])
جب بلاک چین سے پڑھی گئی معلومات تبدیل ہوتی ہیں تو صارف کو دکھائے جانے والے بیلنس کو اپ ڈیٹ کرنے کے لیے useEffect (opens in a new tab) کا استعمال کریں۔
useEffect(() => {
setTransferToken(faucetAddr)
}, [faucetAddr])
useEffect(() => {
setTransferTo(account.address)
}, [account.address])
پہلے سے طے شدہ طور پر FaucetToken ٹوکنز کو صارف کے اپنے اکاؤنٹ میں منتقل کرنا ہے۔ یہاں ہم ان اقدار کو اس وقت سیٹ کرتے ہیں جب ہم انہیں Viem سے وصول کرتے ہیں۔
const proxyAddressChange = (evt) => setNewProxyAddr(evt.target.value)
const transferTokenChange = (evt) => setTransferToken(evt.target.value)
const transferToChange = (evt) => setTransferTo(evt.target.value)
const transferAmountChange = (evt) => setTransferAmount(evt.target.value)
ٹیکسٹ فیلڈز تبدیل ہونے پر ایونٹ ہینڈلرز۔
const deployUserProxy = async () => {
try {
const response = await fetch("/server/deploy", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ownerAddress: account.address })
})
const data = await response.json()
setProxyAddr(data.contractAddress)
} catch (err) {
console.error("Error:", err)
}
}
سرور سے اس صارف کے لیے پراکسی تعینات کرنے کو کہیں۔
const signMessage = async(proxyAddr, target, calldata) => {
آن چین UserProxy کو بھیجنے کے لیے سرور کو بھیجنے سے پہلے ایک پیغام پر دستخط کریں۔ اس کی وضاحت یہاں کی گئی ہے۔ ہمیں ہدف کے پتے (اس ٹوکن کا پتہ جسے ہم کال کر رہے ہیں) اور بھیجے جانے والے کال ڈیٹا دونوں کے ساتھ ایک پیغام پر دستخط کرنے کی ضرورت ہے۔
const domain = {
.
.
.
return {v, r, s}
}
const messageUserProxy = async (proxy, target, data, v, r, s) => {
ایک دستخط شدہ پیغام UserProxy کو بھیجیں، جو دستخط کی تصدیق کرے گا اور پھر اسے target کو بھیج دے گا۔
try {
const response = await fetch("/server/message", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
proxy, target, // دونوں پتے
data, // ہدف کو بھیجنے کے لیے کال ڈیٹا
v, r, s // دستخط
})
})
const serverResponse = await response.json()
setTxHash(serverResponse.txHash)
} catch (err) {
console.error("Error:", err)
}
}
سرور کو ایک درخواست بھیجیں، اور جب آپ کو جواب موصول ہو، تو ٹرانزیکشن ہیش حاصل کریں۔
const faucetSimulation = useSimulateContract({
address: faucetAddr,
abi: Erc20.abi,
functionName: 'faucet',
account: account.address
})
faucet فنکشن کو کال کرنے کی نقل کریں۔ ہم فوسٹ بٹن کو صرف اسی صورت میں فعال کرتے ہیں جب یہ کامیاب ہو۔
const proxyFaucet = async () => {
const calldata = encodeFunctionData({
abi: Erc20.abi,
functionName: 'faucet',
args: [],
})
const {v, r, s} = await signMessage(proxyAddr, calldata)
messageUserProxy(proxyAddr, faucetAddr, calldata, v, r, s)
}
const proxyTransfer = async () => {
const calldata = encodeFunctionData({
abi: Erc20.abi,
functionName: 'transfer',
args: [transferTo, BigInt(transferAmount) * 10n**18n],
})
const {v, r, s} = await signMessage(proxyAddr, transferToken, calldata)
messageUserProxy(proxyAddr, transferToken, calldata, v, r, s)
}
سرور اور UserProxy کے ذریعے کسی فنکشن کو کال کرنے کے لیے، ہم تین مراحل پر عمل کرتے ہیں:
-
encodeFunctionData(opens in a new tab) کا استعمال کرتے ہوئے دستخط کرنے اور بھیجنے کے لیے کال ڈیٹا بنائیں۔ -
پیغام پر دستخط کریں (ہدف کا پتہ، کال ڈیٹا، اور نانس)۔
-
پیغام سرور کو بھیجیں۔
return (
<>
<div align="left">
<h2>Token</h2>
<h4>Token contract address <Address address={faucetAddr} /></h4>
<hr />
<h4>Direct access (as <Address address={account?.address} />)</h4>
Your balance: {balanceAmount}
<br />
<button disabled={!faucetSimulation.data}
onClick={() => writeContract(
faucetSimulation.data.request
)}
>
Request more tokens
</button>
<hr />
جزو کا یہ حصہ آپ کو براؤزر سے براہ راست FaucetToken استعمال کرنے دیتا ہے۔ اس کا بنیادی مقصد ڈیبگنگ میں سہولت فراہم کرنا ہے۔
<h4>UserProxy access <Address address={proxyAddr} /></h4>
<button onClick={deployUserProxy}>
Deploy UserProxy (slow process)
</button>
صارف کو ایک نیا UserProxy تعینات کرنے دیں۔
<br /><br />
<input type="text" placeholder="یا موجودہ پراکسی پتہ درج کریں" value={newProxyAddr} onChange={proxyAddressChange} />
<br /><br />
<button
onClick={() => setProxyAddr(newProxyAddr)}
disabled={newProxyAddr.match(/^0x[a-fA-F0-9]{40}$/) === null}
>
Set proxy address
</button>
صارفین کو صرف اس وقت Set proxy address پر کلک کرنے دیں جب وہ کوئی جائز پتہ درج کریں۔ نوٹ کریں کہ یہ اس بات کو یقینی نہیں بناتا کہ زیر بحث پتہ واقعی ایک UserProxy کنٹریکٹ ہے۔ ایسی جانچ شامل کرنا ممکن ہے، لیکن یہ بہت سست ہوگا (صارف کا بدتر تجربہ) اور سیکیورٹی کو بہتر نہیں بنائے گا (حملہ آور ہمیشہ یوزر انٹرفیس کے لیے اپنا کوڈ استعمال کر سکتے ہیں)۔
<br /><br />
{ proxyAddr && (
باقی حصہ صرف اس صورت میں دکھائیں جب کوئی جائز پراکسی پتہ موجود ہو۔
<>
Proxy balance: {proxyBalanceAmount}
<br />
Proxy nonce: {nonce?.data?.toString() ?? "Loading..."}
صارف کو نانس جاننے کی ضرورت نہیں ہے؛ یہ صرف ڈیبگنگ کے مقاصد کے لیے ہے۔
<br />
<button disabled={!proxyAddr || proxyAddr === "Loading..." || nonce?.status !== 'success'}
onClick={proxyFaucet}
>
Request more tokens for proxy
</button>
ہم پراکسی کے ذریعے faucet() کو کال کی نقل نہیں کر سکتے۔ تاہم، ہم کم از کم یہ یقینی بنا سکتے ہیں کہ ہمارے پاس ایک پراکسی ہے اور پراکسی نے ہمیں ایک نانس کی اطلاع دی ہے۔
<hr />
<h4>Transfer tokens from proxy</h4>
<ul>
<li> Token to transfer: <input type="text" placeholder="Token to transfer" value={transferToken} onChange={transferTokenChange} /> </li>
<li> Recipient address: <input type="text" placeholder="Recipient address" value={transferTo} onChange={transferToChange} /> </li>
<li> Amount to transfer: <input type="number" placeholder="Amount to transfer" value={transferAmount} onChange={transferAmountChange} /> </li>
</ul>
<button disabled={!proxyAddr || proxyAddr === "Loading..." || nonce?.status !== 'success'}
onClick={proxyTransfer}
>
Transfer
</button>
</>
)}
صارف کو ERC-20 منتقلی کی ٹرانزیکشنز جاری کرنے دیں۔
<hr />
{ txHash && (
<>
<h4>Last transaction:</h4>
<a href={`https://eth-sepolia.blockscout.com/tx/${txHash}`} target="_blank">
{txHash}
</a>
</>
)}
اگر کوئی آخری ٹرانزیکشن ہیش ہے، تو ایک لنک دکھائیں تاکہ صارف اسے بلاک ایکسپلورر میں دیکھ سکے۔
</div>
</>
)
}
export {Token}
یہ صرف React بوائلرپلیٹ ہے۔
کمزوریاں
ہمارا سرور ڈینائل آف سروس حملوں کا شکار ہو سکتا ہے۔ اس حملے کی وضاحت سیریز کے پچھلے مضمون میں کی گئی ہے۔
مزید برآں، ہم صارف کے برے رویے کی حوصلہ افزائی کر رہے ہیں۔ ہم صارف سے اس پر دستخط کرنے کو کہتے ہیں:
ہم جانتے ہیں کہ یہ اس ٹوکن، رقم، اور منزل کے پتے کے لیے ایک جائز ERC-20 منتقلی ہے جسے صارف منتقل کرنا چاہتا ہے۔ لیکن زیادہ تر صارفین نہیں جانتے کہ کال ڈیٹا کی تشریح کیسے کی جائے، اور انہیں اندازہ نہیں ہوتا کہ وہ کس چیز پر دستخط کر رہے ہیں۔ یہ دو وجوہات کی بنا پر برا ڈیزائن ہے:
- کچھ صارفین ہمیں استعمال نہیں کریں گے کیونکہ وہ اس ڈیٹا پر بھروسہ نہیں کرتے جس پر ہم انہیں دستخط کرنے کو کہتے ہیں۔
- دوسرے صارفین ہم پر بھروسہ کریں گے اور یہ سیکھیں گے کہ انہیں یہ سمجھے بغیر کال ڈیٹا پر دستخط کر دینے چاہئیں کہ یہ کیا ہے۔ اس کا مطلب یہ ہے کہ اگر کوئی حملہ آور انہیں اپنی ویب سائٹ پر بھیجنے میں کامیاب ہو جاتا ہے، تو وہ ان سے ایسی ٹرانزیکشن پر دستخط کروا سکتا ہے جو اسے صارف کی ملکیت والے تمام USDC (یا DAI، یا کوئی اور ERC-20) دے دے۔
اس کا حل یہ ہے کہ عام طور پر استعمال ہونے والے فنکشنز، جیسے منتقلی کے لیے UserProxy میں الگ الگ فنکشنز ہوں۔ پھر صارفین کسی ایسی چیز پر دستخط کر سکتے ہیں جسے وہ سمجھتے ہوں۔
نوٹ: اگرچہ صارفین اپنی مرضی کا کوئی بھی والیٹ استعمال کر سکتے ہیں، لیکن یہ انتہائی تجویز کیا جاتا ہے کہ EIP-712 استعمال کرنے والی ایپلیکیشنز انہیں ایسا والیٹ استعمال کرنے کی ترغیب دیں جو دستخط کا مکمل ڈیٹا دکھاتا ہو (opens in a new tab)۔ کچھ والیٹس پتے کو کاٹ دیتے ہیں، جو غیر محفوظ ہے۔ ایک حملہ آور ایسا پتہ بنا سکتا ہے جس کے شروع اور آخر کے حروف ایک جیسے ہوں، لیکن درمیان میں مختلف ہوں۔
نتیجہ
مذکورہ بالا کمزوریوں کے علاوہ، اس ٹیوٹوریل کے حل میں کئی خامیاں ہیں جنہیں دور کرنے میں ایتھیریم ہماری مدد کر سکتا ہے۔
- سنسرشپ کے خلاف مزاحمت۔ فی الحال، صارفین آپ کا سرور، کسی اور کا قائم کردہ مسابقتی سرور استعمال کر سکتے ہیں، یا براہ راست ایتھیریم سے جڑ سکتے ہیں، جس پر گیس کی لاگت آتی ہے۔ ERC-4337 (opens in a new tab) کا استعمال صارفین کو اپنی ٹرانزیکشن سرورز کے ایک بڑے پول کو پیش کرنے دیتا ہے، جس سے ان کی ٹرانزیکشنز کے سنسر ہونے کا امکان کم ہو جاتا ہے۔
- EOA کی ملکیت والے اثاثے۔ جیسا کہ اوپر بتایا گیا ہے، EIP-7702 (opens in a new tab) کو EOA پتے کی ملکیت والے اثاثوں کا انتظام کرنے کے لیے استعمال کیا جا سکتا ہے۔ اس کی اپنی مشکلات ہیں، لیکن بعض اوقات یہ ضروری ہوتا ہے۔
مجھے امید ہے کہ مستقبل قریب میں ان خصوصیات کو شامل کرنے کے بارے میں ٹیوٹوریلز شائع کروں گا۔


