diff --git a/public/content/translations/ar/developers/tutorials/gasless/index.md b/public/content/translations/ar/developers/tutorials/gasless/index.md new file mode 100644 index 00000000000..cec4ab64e12 --- /dev/null +++ b/public/content/translations/ar/developers/tutorials/gasless/index.md @@ -0,0 +1,362 @@ +--- +title: "رعاية رسوم الغاز: كيفية تغطية تكاليف المعاملات لمستخدميك" +description: "من السهل إنشاء مفتاح خاص وعنوان؛ كل ما في الأمر هو تشغيل البرنامج المناسب. لكن هناك العديد من الأماكن في العالم حيث يكون الحصول على ETH لإرسال المعاملات أصعب بكثير. في هذا البرنامج التعليمي، ستتعلم كيفية تغطية تكاليف الغاز على السلسلة لتنفيذ بيانات منظمة خارج السلسلة وموقعة من المستخدم في عقدك الذكي. تجعل المستخدم يوقع على بنية تحتوي على معلومات المعاملة، والتي يقوم كودك خارج السلسلة بتقديمه إلى البلوكتشين كمعاملة." +author: Ori Pomerantz +tags: [ "بدون-غاز", "الصلابة", "eip-712", "معاملات-وصفية" ] +skill: intermediate +lang: ar +published: 2026-02-27 +--- + +## مقدمة {#مقدمة} + +إذا أردنا أن تخدم Ethereum [مليار شخص إضافي](https://blog.ethereum.org/category/next-billion)، فنحن بحاجة إلى إزالة العوائق وجعلها سهلة الاستخدام قدر الإمكان. أحد مصادر هذه العوائق هو الحاجة إلى ETH لدفع رسوم الغاز. + +إذا كان لديك تطبيق لامركزي يدر عليك المال من المستخدمين، فقد يكون من المنطقي السماح للمستخدمين بتقديم المعاملات من خلال الخادم الخاص بك ودفع رسوم المعاملات بنفسك. نظرًا لأن المستخدمين لا يزالون يوقعون على [رسالة تفويض EIP-712](https://eips.ethereum.org/EIPS/eip-712) في محافظهم، فإنهم يحتفظون بضمانات سلامة Ethereum. يعتمد التوفر على الخادم الذي ينقل المعاملات، لذلك فهو محدود أكثر. ومع ذلك، يمكنك إعداد الأمور بحيث يمكن للمستخدمين أيضًا الوصول إلى العقد الذكي مباشرة (إذا حصلوا على ETH)، والسماح للآخرين بإعداد خوادمهم الخاصة إذا كانوا يريدون رعاية المعاملات. + +لا تعمل التقنية في هذا البرنامج التعليمي إلا عندما تتحكم في العقد الذكي. هناك تقنيات أخرى، بما في ذلك [تجريد الحساب](https://eips.ethereum.org/EIPS/eip-4337) الذي يتيح لك رعاية المعاملات إلى عقود ذكية أخرى، والتي آمل أن أغطيها في برنامج تعليمي مستقبلي. + +ملاحظة: هذا _ليس_ كودًا على مستوى الإنتاج. إنه عرضة لهجمات كبيرة ويفتقر إلى ميزات رئيسية. تعرف على المزيد في [قسم الثغرات الأمنية في هذا الدليل](#vulnerabilities). + +### المتطلبات الأساسية {#prerequisites} + +لفهم هذا البرنامج التعليمي، يجب أن تكون على دراية بما يلي: + +- لغة برمجة Solidity +- JavaScript +- React وWAGMI. إذا لم تكن على دراية بأدوات واجهة المستخدم هذه، [فلدينا برنامج تعليمي لذلك](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +## التطبيق النموذجي {#sample-app} + +التطبيق النموذجي هنا هو نسخة مختلفة من عقد `Greeter` الخاص بـ Hardhat. يمكنك رؤيته [على GitHub](https://github.com/qbzzt/260301-gasless). تم بالفعل نشر العقد الذكي على [Sepolia](https://sepolia.dev/)، على العنوان [`0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA`](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA). + +لرؤيته أثناء العمل، اتبع هذه الخطوات. + +1. استنسخ المستودع وقم بتثبيت البرامج اللازمة. + + ```sh + git clone https://github.com/qbzzt/260301-gasless.git + cd 260301-gasless/server + npm install + ``` + +2. حرر `.env` لتعيين `PRIVATE_KEY` إلى محفظة بها ETH على Sepolia. إذا كنت بحاجة إلى ETH من Sepolia، [فاستخدم صنبورًا](/developers/docs/networks/#sepolia). من الناحية المثالية، يجب أن يكون هذا المفتاح الخاص مختلفًا عن المفتاح الموجود في محفظة متصفحك. + +3. ابدأ الخادم. + + ```sh + npm run dev + ``` + +4. تصفح التطبيق على عنوان URL [`http://localhost:5173`](http://localhost:5173). + +5. انقر فوق **الاتصال بالمحفظة المُضافة** للاتصال بمحفظة. وافق في المحفظة، ووافق على التغيير إلى Sepolia إذا لزم الأمر. + +6. اكتب تحية جديدة وانقر على **تحديث التحية عبر الراعي**. + +7. وقّع على الرسالة. + +8. انتظر حوالي 12 ثانية (وقت الكتلة على Sepolia). أثناء الانتظار، يمكنك إلقاء نظرة على عنوان URL في وحدة تحكم الخادم لرؤية المعاملة. + +9. سترى أن التحية قد تغيرت، وأن قيمة العنوان الأخير الذي تم التحديث بواسطته هي الآن عنوان محفظة متصفحك. + +لفهم كيفية عمل ذلك، نحتاج إلى إلقاء نظرة على كيفية إنشاء الرسالة في واجهة المستخدم، وكيفية ترحيلها بواسطة الخادم، وكيفية معالجتها بواسطة العقد الذكي. + +### واجهة المستخدم {#ui-changes} + +تعتمد واجهة المستخدم على [WAGMI](https://wagmi.sh/)؛ يمكنك القراءة عنها [في هذا البرنامج التعليمي](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +إليك كيفية توقيع الرسالة: + +```js +const signGreeting = useCallback( +``` + +يتيح لنا خطاف React [`useCallback`](https://react.dev/reference/react/useCallback) تحسين الأداء عن طريق إعادة استخدام نفس الوظيفة عند إعادة رسم المكون. + +```js + async (greeting) => { + if (!account) throw new Error("المحفظة غير متصلة") +``` + +إذا لم يكن هناك حساب، فقم برفع خطأ. لا ينبغي أن يحدث هذا أبدًا لأن زر واجهة المستخدم الذي يبدأ العملية التي تستدعي `signGreeting` يتم تعطيله في هذه الحالة. ومع ذلك، قد يقوم المبرمجون المستقبليون بإزالة هذا الإجراء الوقائي، لذا فمن الجيد التحقق من هذا الشرط هنا أيضًا. + +```js + const domain = { + name: "Greeter", + version: "1", + chainId, + verifyingContract: contractAddr, + } +``` + +معلمات لـ [فاصل النطاق](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator). هذه القيمة ثابتة، لذلك في تنفيذ مُحسَّن بشكل أفضل، قد نحسبها مرة واحدة بدلاً من إعادة حسابها في كل مرة يتم فيها استدعاء الدالة. + +- `name` هو اسم يمكن للمستخدم قراءته، مثل اسم التطبيق اللامركزي الذي نُصدر له التوقيعات. +- `version` هو الإصدار. الإصدارات المختلفة غير متوافقة. +- `chainId` هو السلسلة التي نستخدمها، كما هو مقدم [بواسطة WAGMI](https://wagmi.sh/react/api/hooks/useChainId). +- `verifyingContract` هو عنوان العقد الذي سيتحقق من هذا التوقيع. لا نريد أن يتم تطبيق نفس التوقيع على عقود متعددة، في حال وجود العديد من عقود `Greeter` ونريد أن يكون لها تحيات مختلفة. + +```js + + const types = { + GreetingRequest: [ + { name: "greeting", type: "string" }, + ], + } +``` + +نوع البيانات الذي نوقعه. هنا، لدينا معلمة واحدة، `greeting`، لكن الأنظمة الواقعية عادة ما تحتوي على المزيد. + +```js + const message = { greeting } +``` + +الرسالة الفعلية التي نريد توقيعها وإرسالها. `greeting` هو اسم الحقل واسم المتغير الذي يملؤه. + +```js + const signature = await signTypedDataAsync({ + domain, + types, + primaryType: "GreetingRequest", + message, + }) +``` + +احصل فعليًا على التوقيع. هذه الدالة غير متزامنة لأن المستخدمين يستغرقون وقتًا طويلاً (من منظور الكمبيوتر) لتوقيع البيانات. + +```js + const r = `0x${signature.slice(2, 66)}` + const s = `0x${signature.slice(66, 130)}` + const v = parseInt(signature.slice(130, 132), 16) + + return { + req: { greeting }, + v, + r, + s, + } + }, +``` + +ترجع الدالة قيمة سداسية عشرية واحدة. هنا نقسمها إلى حقول. + +```js + [account, chainId, contractAddr, signTypedDataAsync], +) +``` + +إذا تغير أي من هذه المتغيرات، فأنشئ مثيلاً جديدًا للدالة. يمكن للمستخدم تغيير معلمتي `account` و `chainId` في المحفظة. `contractAddr` هي دالة لمعرف السلسلة. لا ينبغي أن تتغير `signTypedDataAsync`، لكننا نستوردها من [خطاف](https://wagmi.sh/react/api/hooks/useSignTypedData)، لذا لا يمكننا التأكد، ومن الأفضل إضافتها هنا. + +الآن بعد أن تم توقيع التحية الجديدة، نحتاج إلى إرسالها إلى الخادم. + +```js + const sponsoredGreeting = async () => { + try { +``` + +تأخذ هذه الدالة توقيعًا وترسله إلى الخادم. + +```js + const signedMessage = await signGreeting(newGreeting) + const response = await fetch("/server/sponsor", { +``` + +أرسل إلى المسار `/server/sponsor` في الخادم الذي أتينا منه. + +```js + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signedMessage), + }) +``` + +استخدم `POST` لإرسال المعلومات بترميز JSON. + +```js + const data = await response.json() + console.log("استجابة الخادم:", data) + } catch (err) { + console.error("خطأ:", err) + } + } +``` + +إخراج الاستجابة. في نظام الإنتاج، سنعرض أيضًا الاستجابة للمستخدم. + +### الخادم {#server} + +أحب استخدام [Vite](https://vite.dev/) كواجهتي الأمامية. يخدم تلقائيًا مكتبات React ويحدّث المتصفح عند تغيير كود الواجهة الأمامية. ومع ذلك، لا يتضمن Vite أدوات الواجهة الخلفية. + +الحل موجود في [`index.js`](https://github.com/qbzzt/260301-gasless/blob/main/server/index.js). + +```js + app.post("/server/sponsor", async (req, res) => { + ... + }) + + // دع Vite يتعامل مع كل شيء آخر + const vite = await createViteServer({ + server: { middlewareMode: true } + }) + + app.use(vite.middlewares) +``` + +أولاً، نسجل معالجًا للطلبات التي نتعامل معها بأنفسنا (`POST` إلى `/server/sponsor`). ثم نقوم بإنشاء واستخدام خادم Vite للتعامل مع جميع عناوين URL الأخرى. + +```js + app.post("/server/sponsor", async (req, res) => { + try { + const signed = req.body + + const txHash = await sepoliaClient.writeContract({ + address: greeterAddr, + abi: greeterABI, + functionName: 'sponsoredSetGreeting', + args: [signed.req, signed.v, signed.r, signed.s], + }) + } ... + }) +``` + +هذه مجرد مكالمة بلوكتشين قياسية لـ [viem](https://viem.sh/). + +### العقد الذكي {#smart-contract} + +أخيرًا، يحتاج [`Greeter.sol`](https://github.com/qbzzt/260301-gasless/blob/main/contracts/src/Greeter.sol) إلى التحقق من التوقيع. + +```solidity + constructor(string memory _greeting) { + greeting = _greeting; + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("Greeter")), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } +``` + +ينشئ المُنشئ [فاصل النطاق](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator)، على غرار كود واجهة المستخدم أعلاه. تنفيذ البلوكتشين أكثر تكلفة بكثير، لذلك نحسبه مرة واحدة فقط. + +```solidity + struct GreetingRequest { + string greeting; + } +``` + +هذه هي البنية التي يتم توقيعها. هنا لدينا حقل واحد فقط. + +```solidity + bytes32 private constant GREETING_TYPEHASH = + keccak256("GreetingRequest(string greeting)"); +``` + +هذا هو [معرف البنية](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct). يتم حسابه في كل مرة في واجهة المستخدم. + +```solidity + function sponsoredSetGreeting( + GreetingRequest calldata req, + uint8 v, + bytes32 r, + bytes32 s + ) external { +``` + +تتلقى هذه الدالة طلبًا موقعًا وتحدّث التحية. + +```solidity + // حساب خلاصة EIP-712 + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + GREETING_TYPEHASH, + keccak256(bytes(req.greeting)) + ) + ) + ) + ); +``` + +أنشئ الخلاصة وفقًا لـ [EIP 712](https://eips.ethereum.org/EIPS/eip-712). + +```solidity + // استعادة الموقّع + address signer = ecrecover(digest, v, r, s); + require(signer != address(0), "توقيع غير صالح"); +``` + +استخدم [`ecrecover`](https://www.evm.codes/precompiled?fork=osaka#0x01) للحصول على عنوان الموقّع. لاحظ أن التوقيع السيئ لا يزال من الممكن أن ينتج عنه عنوان صالح، ولكنه عشوائي. + +```solidity + // تطبيق التحية كما لو أن الموقّع استدعاها + greeting = req.greeting; + emit SetGreeting(signer, req.greeting); + } +``` + +تحديث التحية. + +## الثغرات الأمنية {#vulnerabilities} + +هذا _ليس_ كودًا على مستوى الإنتاج. إنه عرضة لهجمات كبيرة ويفتقر إلى ميزات رئيسية. إليك بعضها، بالإضافة إلى كيفية حلها. + +لرؤية بعض هذه الهجمات، انقر فوق الأزرار الموجودة أسفل عنوان _الهجمات_ وشاهد ما يحدث. بالنسبة لزر **توقيع غير صالح**، تحقق من وحدة تحكم الخادم لرؤية استجابة المعاملة. + +### رفض الخدمة على الخادم {#dos-on-server} + +أسهل هجوم هو هجوم [رفض الخدمة](https://en.wikipedia.org/wiki/Denial-of-service_attack) على الخادم. يتلقى الخادم طلبات من أي مكان على الإنترنت وبناءً على تلك الطلبات يرسل معاملات. لا يوجد شيء على الإطلاق يمنع المهاجم من إصدار مجموعة من التوقيعات، صالحة أو غير صالحة. سيؤدي كل منها إلى معاملة. في النهاية، سينفد ETH من الخادم لدفع ثمن الغاز. + +أحد الحلول لهذه المشكلة هو تحديد المعدل بمعاملة واحدة لكل كتلة. إذا كان الغرض هو إظهار التحيات [للحسابات المملوكة خارجيًا](/developers/docs/accounts/#key-differences)، فلا يهم ما هي التحية في منتصف الكتلة على أي حال. + +حل آخر هو تتبع العناوين والسماح فقط بالتوقيعات من العملاء الصالحين. + +### توقيعات تحية خاطئة {#wrong-greeting-sigs} + +عندما تنقر على **توقيع لتحية خاطئة**، فإنك ترسل توقيعًا صالحًا لعنوان محدد (`0xaA92c5d426430D4769c9E878C1333BDe3d689b3e`) وتحية (`Hello`). لكنه يرسلها مع تحية مختلفة. هذا يربك `ecrecover`، الذي يغير التحية ولكن لديه عنوان خاطئ. + +لحل هذه المشكلة، أضف العنوان إلى [البنية الموقعة](https://github.com/qbzzt/260301-gasless/blob/main/server/src/Greeter.jsx#L122-L124). بهذه الطريقة، لن يتطابق العنوان العشوائي لـ `ecrecover` مع العنوان الموجود في التوقيع، وسيرفض العقد الذكي الرسالة. + +### هجمات إعادة الإرسال {#replay-attack} + +عندما تنقر على **هجوم إعادة الإرسال**، فإنك ترسل نفس توقيع "أنا 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e، وأرغب في أن تكون التحية `Hello`"، ولكن مع التحية الصحيحة. نتيجة لذلك، يعتقد العقد الذكي أن العنوان (الذي ليس لك) قام بتغيير التحية مرة أخرى إلى `Hello`. المعلومات للقيام بذلك متاحة للجمهور في [معلومات المعاملة](https://eth-sepolia.blockscout.com/tx/0xa66afe4bbf886f59533e677a798c802ceab1ac0f9db6e83a4d4b59a45cf7c1b1). + +إذا كانت هذه مشكلة، فإن أحد الحلول هو إضافة [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). احصل على [تعيين](https://docs.soliditylang.org/en/latest/types.html#mapping-types) بين العناوين والأرقام، وأضف حقل nonce إلى التوقيع. إذا تطابق حقل nonce مع التعيين للعنوان، فاقبل التوقيع وقم بزيادة التعيين للمرة القادمة. إذا لم يكن كذلك، فارفض المعاملة. + +حل آخر هو إضافة ختم زمني إلى البيانات الموقعة وقبول التوقيع على أنه صالح فقط لبضع ثوان بعد ذلك الختم الزمني. هذا أبسط وأرخص، لكننا نخاطر بهجمات إعادة الإرسال خلال النافذة الزمنية، وفشل المعاملات المشروعة إذا تم تجاوز النافذة الزمنية. + +## ميزات أخرى مفقودة {#other-missing-features} + +هناك ميزات إضافية سنضيفها في إعداد الإنتاج. + +### الوصول من خوادم أخرى {#other-servers} + +حاليًا، نسمح لأي عنوان بتقديم `sponsorSetGreeting`. قد يكون هذا هو بالضبط ما نريده، من أجل اللامركزية. أو ربما نريد التأكد من أن المعاملات المدعومة تمر عبر خادمنا _الخاص_، وفي هذه الحالة سنتحقق من `msg.sender` في العقد الذكي. + +في كلتا الحالتين، يجب أن يكون هذا قرار تصميم واعيًا، وليس مجرد نتيجة لعدم التفكير في المشكلة. + +### معالجة الأخطاء {#error-handling} + +يقدم المستخدم تحية. ربما يتم تحديثها في الكتلة التالية. وربما لا. الأخطاء غير مرئية. في نظام الإنتاج، يجب أن يكون المستخدم قادرًا على التمييز بين هذه الحالات: + +- لم يتم تقديم التحية الجديدة بعد +- تم تقديم التحية الجديدة، وهي قيد المعالجة +- تم رفض التحية الجديدة + +## الخلاصة {#conclusion} + +في هذه المرحلة، يجب أن تكون قادرًا على إنشاء تجربة بدون غاز لمستخدمي تطبيقك اللامركزي، على حساب بعض المركزية. + +ومع ذلك، هذا يعمل فقط مع العقود الذكية التي تدعم ERC-712. لنقل رمز ERC-20، على سبيل المثال، من الضروري أن يتم توقيع المعاملة من قبل المالك بدلاً من مجرد رسالة. الحل هو [تجريد الحساب (ERC-4337)](https://docs.erc4337.io/index.html). آمل أن أكتب برنامجًا تعليميًا مستقبليًا عنه. + +[انظر هنا لمزيد من أعمالي](https://cryptodocguy.pro/). diff --git a/public/content/translations/bn/developers/tutorials/gasless/index.md b/public/content/translations/bn/developers/tutorials/gasless/index.md new file mode 100644 index 00000000000..a97380dc9a0 --- /dev/null +++ b/public/content/translations/bn/developers/tutorials/gasless/index.md @@ -0,0 +1,362 @@ +--- +title: "গ্যাস ফি স্পনসর করা: আপনার ব্যবহারকারীদের জন্য লেনদেনের খরচ কীভাবে কভার করবেন" +description: "একটি প্রাইভেট কী এবং একটি ঠিকানা তৈরি করা সহজ; এটি কেবল সঠিক সফ্টওয়্যার চালানোর বিষয়। কিন্তু বিশ্বের অনেক জায়গায় লেনদেন পাঠানোর জন্য ETH পাওয়া অনেক বেশি কঠিন। এই টিউটোরিয়ালে আপনি আপনার স্মার্ট কন্ট্র্যাক্টে ব্যবহারকারী-স্বাক্ষরিত, অফচেইন স্ট্রাকচার্ড ডেটা কার্যকর করার জন্য অনচেইন গ্যাস খরচ কীভাবে কভার করতে হয় তা শিখবেন। আপনি ব্যবহারকারীকে লেনদেনের তথ্য সম্বলিত একটি কাঠামোতে স্বাক্ষর করান, যা আপনার অফচেইন কোড একটি লেনদেন হিসাবে ব্লকচেইনে জমা দেয়।" +author: Ori Pomerantz +tags: [ "গ্যাসলেস", "সলিডিটি", "eip-712", "মেটা-ট্রানজ্যাকশন" ] +skill: intermediate +lang: bn +published: 2026-02-27 +--- + +## ভূমিকা {#introduction} + +আমরা যদি চাই যে ইথেরিয়াম [আরও এক বিলিয়ন মানুষকে](https://blog.ethereum.org/category/next-billion) পরিষেবা দিক, আমাদের ঘর্ষণ দূর করতে হবে এবং এটিকে যতটা সম্ভব সহজে ব্যবহারযোগ্য করে তুলতে হবে। এই ঘর্ষণের একটি উৎস হল গ্যাস ফি প্রদানের জন্য ETH-এর প্রয়োজনীয়তা। + +আপনার যদি একটি ডিএ্যাপ থাকে যা ব্যবহারকারীদের কাছ থেকে অর্থ উপার্জন করে, তবে ব্যবহারকারীদের আপনার সার্ভারের মাধ্যমে লেনদেন জমা দেওয়ার অনুমতি দেওয়া এবং লেনদেনের ফি নিজেই প্রদান করা যুক্তিযুক্ত হতে পারে। যেহেতু ব্যবহারকারীরা এখনও তাদের ওয়ালেটে একটি [EIP-712 অনুমোদন বার্তা](https://eips.ethereum.org/EIPS/eip-712) স্বাক্ষর করে, তারা ইথেরিয়ামের অখণ্ডতার গ্যারান্টি বজায় রাখে। উপলভ্যতা নির্ভর করে যে সার্ভার লেনদেন রিলে করে তার উপর, তাই এটি আরও সীমিত। তবে, আপনি জিনিসগুলি এমনভাবে সেট করতে পারেন যাতে ব্যবহারকারীরা সরাসরি স্মার্ট কন্ট্র্যাক্ট অ্যাক্সেস করতে পারে (যদি তারা ETH পায়), এবং অন্যদের লেনদেন স্পনসর করতে চাইলে তাদের নিজস্ব সার্ভার সেট আপ করার অনুমতি দিতে পারেন। + +এই টিউটোরিয়ালের কৌশলটি তখনই কাজ করে যখন আপনি স্মার্ট কন্ট্র্যাক্ট নিয়ন্ত্রণ করেন। অন্যান্য কৌশল রয়েছে, যার মধ্যে রয়েছে [অ্যাকাউন্ট অ্যাবস্ট্রাকশন](https://eips.ethereum.org/EIPS/eip-4337) যা আপনাকে অন্যান্য স্মার্ট কন্ট্র্যাক্টে লেনদেন স্পনসর করার অনুমতি দেয়, যা আমি ভবিষ্যতের টিউটোরিয়ালে কভার করার আশা করি। + +দ্রষ্টব্য: এটি _নয়_ প্রোডাকশন-লেভেলের কোড। এটি উল্লেখযোগ্য আক্রমণের জন্য ঝুঁকিপূর্ণ এবং এতে প্রধান বৈশিষ্ট্যগুলির অভাব রয়েছে। [এই গাইডের দুর্বলতা বিভাগে](#vulnerabilities) আরও জানুন। + +### পূর্বশর্ত {#prerequisites} + +এই টিউটোরিয়ালটি বোঝার জন্য আপনাকে আগে থেকেই পরিচিত হতে হবে: + +- Solidity +- JavaScript +- React and WAGMI। আপনি যদি এই ব্যবহারকারী ইন্টারফেস টুলগুলির সাথে পরিচিত না হন, [তার জন্য আমাদের একটি টিউটোরিয়াল আছে](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/)। + +## নমুনা অ্যাপ্লিকেশন {#sample-app} + +এখানকার নমুনা অ্যাপ্লিকেশনটি Hardhat-এর `Greeter` কন্ট্র্যাক্টের একটি ভেরিয়েন্ট। আপনি এটি [GitHub-এ](https://github.com/qbzzt/260301-gasless) দেখতে পারেন। স্মার্ট কন্ট্র্যাক্টটি ইতিমধ্যে [Sepolia](https://sepolia.dev/)-তে [`0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA`](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA) ঠিকানায় স্থাপন করা হয়েছে। + +এটি কার্যকর অবস্থায় দেখতে, এই পদক্ষেপগুলি অনুসরণ করুন। + +1. রিপোজিটরি ক্লোন করুন এবং প্রয়োজনীয় সফ্টওয়্যার ইনস্টল করুন। + + ```sh + git clone https://github.com/qbzzt/260301-gasless.git + cd 260301-gasless/server + npm install + ``` + +2. Sepolia-তে ETH আছে এমন একটি ওয়ালেটে `PRIVATE_KEY` সেট করতে `.env` এডিট করুন। আপনার যদি Sepolia ETH প্রয়োজন হয়, [একটি ফসেট ব্যবহার করুন](/developers/docs/networks/#sepolia)। আদর্শগতভাবে, এই প্রাইভেট কীটি আপনার ব্রাউজার ওয়ালেটে থাকা কী থেকে ভিন্ন হওয়া উচিত। + +3. সার্ভার শুরু করুন। + + ```sh + npm run dev + ``` + +4. [`http://localhost:5173`](http://localhost:5173) URL-এ অ্যাপ্লিকেশনটি ব্রাউজ করুন। + +5. একটি ওয়ালেটের সাথে সংযোগ করতে **Connect with Injected**-এ ক্লিক করুন। ওয়ালেটে অনুমোদন করুন এবং প্রয়োজনে Sepolia-তে পরিবর্তনের অনুমোদন দিন। + +6. একটি নতুন সম্ভাষণ লিখুন এবং **স্পনসরের মাধ্যমে সম্ভাষণ আপডেট করুন**-এ ক্লিক করুন। + +7. বার্তাটিতে স্বাক্ষর করুন। + +8. প্রায় 12 সেকেন্ড অপেক্ষা করুন (Sepolia-তে ব্লক সময়)। অপেক্ষা করার সময় আপনি লেনদেনটি দেখতে সার্ভারের কনসোলে URLটি দেখতে পারেন। + +9. দেখুন যে সম্ভাষণটি পরিবর্তিত হয়েছে, এবং যে শেষ আপডেট করা ঠিকানা মানটি এখন আপনার ব্রাউজার ওয়ালেটের ঠিকানা। + +এটি কীভাবে কাজ করে তা বোঝার জন্য, আমাদের দেখতে হবে কীভাবে ব্যবহারকারী ইন্টারফেসে বার্তাটি তৈরি হয়, কীভাবে এটি সার্ভার দ্বারা রিলে করা হয় এবং কীভাবে স্মার্ট কন্ট্র্যাক্ট এটি প্রক্রিয়া করে। + +### ব্যবহারকারী ইন্টারফেস {#ui-changes} + +ব্যবহারকারী ইন্টারফেসটি [WAGMI](https://wagmi.sh/) এর উপর ভিত্তি করে তৈরি; আপনি এটি সম্পর্কে [এই টিউটোরিয়ালে](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/) পড়তে পারেন। + +এখানে আমরা কীভাবে বার্তাটিতে স্বাক্ষর করি: + +```js +const signGreeting = useCallback( +``` + +React হুক [`useCallback`](https://react.dev/reference/react/useCallback) আমাদের কর্মক্ষমতা উন্নত করতে দেয় যখন কম্পোনেন্টটি পুনরায় আঁকা হয় তখন একই ফাংশন পুনরায় ব্যবহার করে। + +```js + async (greeting) => { + if (!account) throw new Error("ওয়ালেট সংযুক্ত নেই") +``` + +যদি কোনো অ্যাকাউন্ট না থাকে, একটি ত্রুটি উত্থাপন করুন। এটি কখনই ঘটা উচিত নয় কারণ যে UI বোতামটি `signGreeting` কল করা প্রক্রিয়া শুরু করে তা সেই ক্ষেত্রে নিষ্ক্রিয় থাকে। তবে, ভবিষ্যতের প্রোগ্রামাররা সেই সুরক্ষা ব্যবস্থাটি সরিয়ে ফেলতে পারে, তাই এখানেও এই শর্তটি পরীক্ষা করা একটি ভাল ধারণা। + +```js + const domain = { + name: "Greeter", + version: "1", + chainId, + verifyingContract: contractAddr, + } +``` + +[ডোমেইন সেপারেটর](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator)-এর জন্য প্যারামিটার। এই মানটি ধ্রুবক, তাই একটি উন্নত-অপ্টিমাইজড বাস্তবায়নে, আমরা ফাংশনটি যতবার কল করা হয় ততবার পুনর্গণনা করার পরিবর্তে একবার এটি গণনা করতে পারি। + +- `name` একটি ব্যবহারকারী-পাঠযোগ্য নাম, যেমন যে ডিএ্যাপের জন্য আমরা স্বাক্ষর তৈরি করছি তার নাম। +- `version` হল সংস্করণ। ভিন্ন সংস্করণগুলি সামঞ্জস্যপূর্ণ নয়। +- `chainId` হল আমরা যে চেইনটি ব্যবহার করছি, যা [WAGMI দ্বারা প্রদত্ত](https://wagmi.sh/react/api/hooks/useChainId)। +- `verifyingContract` হল কন্ট্র্যাক্ট ঠিকানা যা এই স্বাক্ষরটি যাচাই করবে। আমরা চাই না যে একই স্বাক্ষর একাধিক কন্ট্র্যাক্টে প্রযোজ্য হোক, যদি বেশ কয়েকটি `Greeter` কন্ট্র্যাক্ট থাকে এবং আমরা চাই যে তাদের ভিন্ন সম্ভাষণ থাকুক। + +```js + + const types = { + GreetingRequest: [ + { name: "greeting", type: "string" }, + ], + } +``` + +আমরা যে ডেটা টাইপে স্বাক্ষর করি। এখানে, আমাদের একটি একক প্যারামিটার আছে, `greeting`, কিন্তু বাস্তব-জীবনের সিস্টেমে সাধারণত আরও বেশি থাকে। + +```js + const message = { greeting } +``` + +প্রকৃত বার্তা যা আমরা স্বাক্ষর করতে এবং পাঠাতে চাই। `greeting` হল ফিল্ডের নাম এবং এটি পূরণকারী ভেরিয়েবলের নাম উভয়ই। + +```js + const signature = await signTypedDataAsync({ + domain, + types, + primaryType: "GreetingRequest", + message, + }) +``` + +আসলে স্বাক্ষরটি পান। এই ফাংশনটি অ্যাসিঙ্ক্রোনাস কারণ ব্যবহারকারীরা ডেটা স্বাক্ষর করতে অনেক সময় নেয় (একটি কম্পিউটারের দৃষ্টিকোণ থেকে)। + +```js + const r = `0x${signature.slice(2, 66)}` + const s = `0x${signature.slice(66, 130)}` + const v = parseInt(signature.slice(130, 132), 16) + + return { + req: { greeting }, + v, + r, + s, + } + }, +``` + +ফাংশনটি একটি একক হেক্সাডেসিমেল মান প্রদান করে। এখানে আমরা এটিকে ফিল্ডে ভাগ করি। + +```js + [account, chainId, contractAddr, signTypedDataAsync], +) +``` + +যদি এই ভেরিয়েবলগুলির কোনোটি পরিবর্তন হয়, তবে ফাংশনের একটি নতুন ইনস্ট্যান্স তৈরি করুন। `account` এবং `chainId` প্যারামিটারগুলি ব্যবহারকারী ওয়ালেটে পরিবর্তন করতে পারেন। `contractAddr` হল চেইন আইডির একটি ফাংশন। `signTypedDataAsync` পরিবর্তন হওয়া উচিত নয়, কিন্তু আমরা এটি একটি [হুক](https://wagmi.sh/react/api/hooks/useSignTypedData) থেকে ইম্পোর্ট করি, তাই আমরা নিশ্চিত হতে পারি না, এবং এটি এখানে যোগ করাই ভাল। + +এখন যেহেতু নতুন সম্ভাষণটি স্বাক্ষরিত হয়েছে, আমাদের এটি সার্ভারে পাঠাতে হবে। + +```js + const sponsoredGreeting = async () => { + try { +``` + +এই ফাংশনটি একটি স্বাক্ষর নেয় এবং এটি সার্ভারে পাঠায়। + +```js + const signedMessage = await signGreeting(newGreeting) + const response = await fetch("/server/sponsor", { +``` + +আমরা যে সার্ভার থেকে এসেছি তার `/server/sponsor` পাথে পাঠান। + +```js + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signedMessage), + }) +``` + +তথ্য JSON-এনকোডেড পাঠাতে `POST` ব্যবহার করুন। + +```js + const data = await response.json() + console.log("Server response:", data) + } catch (err) { + console.error("Error:", err) + } + } +``` + +প্রতিক্রিয়া আউটপুট করুন। একটি প্রোডাকশন সিস্টেমে আমরা ব্যবহারকারীকে প্রতিক্রিয়াটিও দেখাতাম। + +### সার্ভার {#server} + +আমি আমার ফ্রন্ট-এন্ড হিসাবে [Vite](https://vite.dev/) ব্যবহার করতে পছন্দ করি। এটি স্বয়ংক্রিয়ভাবে React লাইব্রেরি পরিবেশন করে এবং ফ্রন্ট-এন্ড কোড পরিবর্তন হলে ব্রাউজার আপডেট করে। তবে, Vite-এ ব্যাকএন্ড টুলিং অন্তর্ভুক্ত নয়। + +সমাধানটি [`index.js`](https://github.com/qbzzt/260301-gasless/blob/main/server/index.js)-এ রয়েছে। + +```js + app.post("/server/sponsor", async (req, res) => { + ... + }) + + // Vite কে বাকি সব কিছু হ্যান্ডেল করতে দিন + const vite = await createViteServer({ + server: { middlewareMode: true } + }) + + app.use(vite.middlewares) +``` + +প্রথমে আমরা নিজেদের হ্যান্ডেল করা অনুরোধের জন্য একটি হ্যান্ডলার রেজিস্টার করি (`/server/sponsor`-এ `POST`)। তারপর আমরা অন্যান্য সমস্ত URL হ্যান্ডেল করার জন্য একটি Vite সার্ভার তৈরি করি এবং ব্যবহার করি। + +```js + app.post("/server/sponsor", async (req, res) => { + try { + const signed = req.body + + const txHash = await sepoliaClient.writeContract({ + address: greeterAddr, + abi: greeterABI, + functionName: 'sponsoredSetGreeting', + args: [signed.req, signed.v, signed.r, signed.s], + }) + } ... + }) +``` + +এটি শুধু একটি স্ট্যান্ডার্ড [viem](https://viem.sh/) ব্লকচেইন কল। + +### স্মার্ট কন্ট্র্যাক্ট {#smart-contract} + +অবশেষে, [`Greeter.sol`](https://github.com/qbzzt/260301-gasless/blob/main/contracts/src/Greeter.sol)-কে স্বাক্ষরটি যাচাই করতে হবে। + +```solidity + constructor(string memory _greeting) { + greeting = _greeting; + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("Greeter")), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } +``` + +কনস্ট্রাক্টর [ডোমেইন সেপারেটর](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator) তৈরি করে, যা উপরের ব্যবহারকারী ইন্টারফেস কোডের অনুরূপ। ব্লকচেইন এক্সিকিউশন অনেক বেশি ব্যয়বহুল, তাই আমরা এটি কেবল একবারই গণনা করি। + +```solidity + struct GreetingRequest { + string greeting; + } +``` + +এটি সেই কাঠামো যা স্বাক্ষরিত হয়। এখানে আমাদের কেবল একটি ফিল্ড আছে। + +```solidity + bytes32 private constant GREETING_TYPEHASH = + keccak256("GreetingRequest(string greeting)"); +``` + +এটি [কাঠামো শনাক্তকারী](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct)। এটি প্রতিবার ব্যবহারকারী ইন্টারফেসে গণনা করা হয়। + +```solidity + function sponsoredSetGreeting( + GreetingRequest calldata req, + uint8 v, + bytes32 r, + bytes32 s + ) external { +``` + +এই ফাংশনটি একটি স্বাক্ষরিত অনুরোধ গ্রহণ করে এবং সম্ভাষণ আপডেট করে। + +```solidity + // EIP-712 ডাইজেস্ট গণনা করুন + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + GREETING_TYPEHASH, + keccak256(bytes(req.greeting)) + ) + ) + ) + ); +``` + +[EIP 712](https://eips.ethereum.org/EIPS/eip-712) অনুসারে ডাইজেস্ট তৈরি করুন। + +```solidity + // সাইনার পুনরুদ্ধার করুন + address signer = ecrecover(digest, v, r, s); + require(signer != address(0), "Invalid signature"); +``` + +সাইনারের ঠিকানা পেতে [`ecrecover`](https://www.evm.codes/precompiled?fork=osaka#0x01) ব্যবহার করুন। লক্ষ্য করুন যে একটি খারাপ স্বাক্ষরও একটি বৈধ ঠিকানা তৈরি করতে পারে, তবে সেটি হবে একটি র‍্যান্ডম ঠিকানা। + +```solidity + // সাইনার কল করেছে এমনভাবে সম্ভাষণ প্রয়োগ করুন + greeting = req.greeting; + emit SetGreeting(signer, req.greeting); + } +``` + +সম্ভাষণ আপডেট করুন। + +## দুর্বলতা {#vulnerabilities} + +এটি _নয়_ প্রোডাকশন-লেভেলের কোড। এটি উল্লেখযোগ্য আক্রমণের জন্য ঝুঁকিপূর্ণ এবং এতে প্রধান বৈশিষ্ট্যগুলির অভাব রয়েছে। এখানে কয়েকটি দেওয়া হল, সাথে কীভাবে সেগুলি সমাধান করা যায়। + +এই আক্রমণগুলির কয়েকটি দেখতে, _আক্রমণ_ শিরোনামের নীচে বোতামগুলিতে ক্লিক করুন এবং দেখুন কী হয়। **অবৈধ স্বাক্ষর** বোতামের জন্য, লেনদেনের প্রতিক্রিয়া দেখতে সার্ভার কনসোলটি পরীক্ষা করুন। + +### সার্ভারে ডিনায়াল-অফ-সার্ভিস {#dos-on-server} + +সবচেয়ে সহজ আক্রমণ হল সার্ভারে একটি [ডিনায়াল-অফ-সার্ভিস](https://en.wikipedia.org/wiki/Denial-of-service_attack) আক্রমণ। সার্ভার ইন্টারনেটের যেকোনো জায়গা থেকে অনুরোধ গ্রহণ করে এবং সেই অনুরোধের ভিত্তিতে লেনদেন পাঠায়। একজন আক্রমণকারীকে বৈধ বা অবৈধ একগুচ্ছ স্বাক্ষর জারি করা থেকে বিরত রাখার মতো কিছুই নেই। প্রতিটি একটি লেনদেনের কারণ হবে। অবশেষে গ্যাসের জন্য অর্থ প্রদান করতে সার্ভারের ETH শেষ হয়ে যাবে। + +এই সমস্যার একটি সমাধান হল প্রতি ব্লকে একটি লেনদেনের হার সীমিত করা। যদি উদ্দেশ্য হয় [বাহ্যিকভাবে মালিকানাধীন অ্যাকাউন্ট](/developers/docs/accounts/#key-differences)-কে সম্ভাষণ দেখানো, তাহলে ব্লকের মাঝখানে সম্ভাষণটি কী তা কোনো ব্যাপার নয়। + +আরেকটি সমাধান হল ঠিকানাগুলির ট্র্যাক রাখা এবং শুধুমাত্র বৈধ গ্রাহকদের কাছ থেকে স্বাক্ষর অনুমোদন করা। + +### ভুল সম্ভাষণের স্বাক্ষর {#wrong-greeting-sigs} + +আপনি যখন **ভুল সম্ভাষণের জন্য স্বাক্ষর**-এ ক্লিক করেন, আপনি একটি নির্দিষ্ট ঠিকানা (`0xaA92c5d426430D4769c9E878C1333BDe3d689b3e`) এবং সম্ভাষণের (`Hello`) জন্য একটি বৈধ স্বাক্ষর জমা দেন। কিন্তু এটি একটি ভিন্ন সম্ভাষণ সহ জমা দেয়। এটি `ecrecover`-কে বিভ্রান্ত করে, যা সম্ভাষণ পরিবর্তন করে কিন্তু ভুল ঠিকানা দেখায়। + +এই সমস্যা সমাধানের জন্য, [স্বাক্ষরিত কাঠামোতে](https://github.com/qbzzt/260301-gasless/blob/main/server/src/Greeter.jsx#L122-L124) ঠিকানাটি যোগ করুন। এইভাবে, `ecrecover`-এর র‍্যান্ডম ঠিকানাটি স্বাক্ষরের ঠিকানার সাথে মিলবে না এবং স্মার্ট কন্ট্র্যাক্ট বার্তাটি প্রত্যাখ্যান করবে। + +### রিপ্লে অ্যাটাক {#replay-attack} + +আপনি যখন **রিপ্লে অ্যাটাক**-এ ক্লিক করেন, তখন আপনি একই "আমি 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, এবং আমি চাই সম্ভাষণটি `Hello` হোক" স্বাক্ষর জমা দেন, কিন্তু সঠিক সম্ভাষণের সাথে। ফলস্বরূপ, স্মার্ট কন্ট্র্যাক্ট বিশ্বাস করে যে ঠিকানাটি (যা আপনার নয়) সম্ভাষণটি আবার `Hello`-তে পরিবর্তন করেছে। এটি করার জন্য তথ্য [লেনদেনের তথ্যে](https://eth-sepolia.blockscout.com/tx/0xa66afe4bbf886f59533e677a798c802ceab1ac0f9db6e83a4d4b59a45cf7c1b1) সর্বজনীনভাবে উপলব্ধ। + +যদি এটি একটি সমস্যা হয়, তবে একটি সমাধান হল একটি [নন্স](https://en.wikipedia.org/wiki/Cryptographic_nonce) যোগ করা। ঠিকানা এবং সংখ্যার মধ্যে একটি [ম্যাপিং](https://docs.soliditylang.org/en/latest/types.html#mapping-types) রাখুন, এবং স্বাক্ষরে একটি নন্স ফিল্ড যোগ করুন। যদি নন্স ফিল্ডটি ঠিকানার জন্য ম্যাপিংয়ের সাথে মিলে যায়, তবে স্বাক্ষরটি গ্রহণ করুন এবং পরবর্তী বারের জন্য ম্যাপিংটি বৃদ্ধি করুন। যদি এটি না হয়, লেনদেনটি প্রত্যাখ্যান করুন। + +আরেকটি সমাধান হল স্বাক্ষরিত ডেটাতে একটি টাইমস্ট্যাম্প যোগ করা এবং সেই টাইমস্ট্যাম্পের কয়েক সেকেন্ডের জন্য স্বাক্ষরটিকে বৈধ হিসাবে গ্রহণ করা। এটি সহজ এবং সস্তা, তবে আমরা সময়ের জানালার মধ্যে রিপ্লে অ্যাটাকের ঝুঁকি নিই, এবং সময়ের জানালা অতিক্রম করলে বৈধ লেনদেনের ব্যর্থতার ঝুঁকি থাকে। + +## অন্যান্য অনুপস্থিত বৈশিষ্ট্য {#other-missing-features} + +প্রোডাকশন সেটিং-এ আমরা অতিরিক্ত বৈশিষ্ট্য যোগ করব। + +### অন্যান্য সার্ভার থেকে অ্যাক্সেস {#other-servers} + +বর্তমানে, আমরা যেকোনো ঠিকানাকে একটি `sponsorSetGreeting` জমা দেওয়ার অনুমতি দিই। বিকেন্দ্রীকরণের স্বার্থে, এটি ঠিক তাই হতে পারে যা আমরা চাই। অথবা হয়তো আমরা নিশ্চিত করতে চাই যে স্পনসর করা লেনদেনগুলি _আমাদের_ সার্ভারের মাধ্যমে যায়, সেক্ষেত্রে আমরা স্মার্ট কন্ট্র্যাক্টে `msg.sender` পরীক্ষা করব। + +যাইহোক, এটি একটি সচেতন নকশা সিদ্ধান্ত হওয়া উচিত, কেবল বিষয়টি নিয়ে চিন্তা না করার ফল নয়। + +### ত্রুটি হ্যান্ডলিং {#error-handling} + +একজন ব্যবহারকারী একটি সম্ভাষণ জমা দেন। হয়তো এটি পরবর্তী ব্লকে আপডেট হয়ে যায়। হয়তো হয় না। ত্রুটিগুলি অদৃশ্য। একটি প্রোডাকশন সিস্টেমে, ব্যবহারকারীর এই কেসগুলির মধ্যে পার্থক্য করতে সক্ষম হওয়া উচিত: + +- নতুন সম্ভাষণ এখনও জমা দেওয়া হয়নি +- নতুন সম্ভাষণ জমা দেওয়া হয়েছে, এবং এটি প্রক্রিয়াধীন +- নতুন সম্ভাষণ প্রত্যাখ্যান করা হয়েছে + +## উপসংহার {#conclusion} + +এই মুহুর্তে, আপনার ডিএ্যাপ ব্যবহারকারীদের জন্য একটি গ্যাসবিহীন অভিজ্ঞতা তৈরি করতে সক্ষম হওয়া উচিত, কিছু কেন্দ্রীকরণের মূল্যে। + +তবে, এটি কেবল ERC-712 সমর্থন করে এমন স্মার্ট কন্ট্র্যাক্টগুলির সাথেই কাজ করে। উদাহরণস্বরূপ, একটি ERC-20 টোকেন স্থানান্তর করতে, কেবল একটি বার্তার পরিবর্তে মালিক দ্বারা স্বাক্ষরিত লেনদেন থাকা প্রয়োজন। সমাধান হল [অ্যাকাউন্ট অ্যাবস্ট্রাকশন (ERC-4337)](https://docs.erc4337.io/index.html)। আমি ভবিষ্যতে এটি সম্পর্কে একটি টিউটোরিয়াল লেখার আশা করি। + +[আমার আরও কাজের জন্য এখানে দেখুন](https://cryptodocguy.pro/)। diff --git a/public/content/translations/cs/developers/tutorials/gasless/index.md b/public/content/translations/cs/developers/tutorials/gasless/index.md new file mode 100644 index 00000000000..004f1539a3e --- /dev/null +++ b/public/content/translations/cs/developers/tutorials/gasless/index.md @@ -0,0 +1,362 @@ +--- +title: "Sponsoring gas fees: How to cover transaction costs for your users" +description: It is easy to create a private key and an address; it's just a matter of running the right software. But there are many places in the world where getting the ETH to send transactions is much harder. In this tutorial you learn how to cover the onchain gas costs for executing user-signed, offchain structured data in your smart contract. You have the user sign a structure containing the transaction information, which your offchain code then submits to the blockchain as a transaction. +author: Ori Pomerantz +tags: [ "gasless", "solidity", "eip-712", "meta-transactions" ] +skill: intermediate +lang: cs +published: 2026-02-27 +--- + +## Úvod {#introduction} + +If we want Ethereum to serve [a billion more people](https://blog.ethereum.org/category/next-billion), we need to remove friction and make it as easy to use as possible. One source of this friction is the need for ETH to pay gas fees. + +If you have a dapp that makes money from users, it might make sense to let users submit transactions through your server and pay the transaction fees yourself. Because users still sign an [EIP-712 authorization message](https://eips.ethereum.org/EIPS/eip-712) in their wallets, they retain Ethereum's guarantees of integrity. Availability depends on the server that relays transactions, so it is more limited. However, you can set things up so users can also access the smart contract directly (if they get ETH), and let others set up their own servers if they want to sponsor transactions. + +The technique in this tutorial only works when you control the smart contract. There are other techniques, including [account abstraction](https://eips.ethereum.org/EIPS/eip-4337) that let you sponsor transactions to other smart contracts, which I hope to cover in a future tutorial. + +Note: This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Learn more in the [vulnerabilities section of this guide](#vulnerabilities). + +### Předpoklady {#prerequisites} + +To understand this tutorial you need to already be familiar with: + +- Solidity +- JavaScript +- React and WAGMI. If you are not familiar with these user interface tools, [we have a tutorial for that](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +## The sample application {#sample-app} + +The sample application here is a variant on Hardhat's `Greeter` contract. You can see it [on GitHub](https://github.com/qbzzt/260301-gasless). The smart contract is already deployed on the [Sepolia](https://sepolia.dev/), at address [`0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA`](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA). + +To see it in action, follow these steps. + +1. Clone the repository and install the necessary software. + + ```sh + git clone https://github.com/qbzzt/260301-gasless.git + cd 260301-gasless/server + npm install + ``` + +2. Edit `.env` to set `PRIVATE_KEY` to a wallet that has ETH on Sepolia. If you need Sepolia ETH, [use a faucet](/developers/docs/networks/#sepolia). Ideally, this private key should be different from the one you have in your browser wallet. + +3. Spusťte server. + + ```sh + npm run dev + ``` + +4. Browse to the application at URL [`http://localhost:5173`](http://localhost:5173). + +5. Click **Connect with Injected** to connect to a wallet. Approve in the wallet, and approve the change to Sepolia if necessary. + +6. Write a new greeting and click **Update greeting via sponsor**. + +7. Sign the message. + +8. Wait about 12 seconds (the block time on Sepolia). While waiting you can look at the URL in the server's console to see the transaction. + +9. See that the greeting changed, and that the last updated by address value is now the address of your browser wallet. + +To understand how this works, we need to look at how the message gets created in the user interface, how it is relayed by the server, and how the smart contract processes it. + +### The user interface {#ui-changes} + +The user interface is based on [WAGMI](https://wagmi.sh/); you can read about it [in this tutorial](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +Here is how we sign the message: + +```js +const signGreeting = useCallback( +``` + +The React hook [`useCallback`](https://react.dev/reference/react/useCallback) lets us improve performance by reusing the same function when the component is redrawn. + +```js + async (greeting) => { + if (!account) throw new Error("Wallet not connected") +``` + +If there is no account, raise an error. This should never happen because the UI button that starts the process that calls `signGreeting` is disabled in that case. However, future programmers may remove that safeguard, so it's a good idea to check this condition here as well. + +```js + const domain = { + name: "Greeter", + version: "1", + chainId, + verifyingContract: contractAddr, + } +``` + +Parameters for the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator). This value is constant, so in a better-optimized implementation, we might calculate it once rather than recalculate it each time the function is called. + +- `name` is a user-readable name, such as the name of the dapp for which we are producing signatures. +- `version` is the version. Different versions are not compatible. +- `chainId` is the chain we are using, as provided [by WAGMI](https://wagmi.sh/react/api/hooks/useChainId). +- `verifyingContract` is the contract address that will verify this signature. We do not want the same signature to apply to multiple contracts, in case there are several `Greeter` contracts and we want them to have different greetings. + +```js + + const types = { + GreetingRequest: [ + { name: "greeting", type: "string" }, + ], + } +``` + +The data type we sign. Here, we have a single parameter, `greeting`, but real-life systems typically have more. + +```js + const message = { greeting } +``` + +The actual message we want to sign and send. `greeting` is both the field name and the name of the variable that fills it. + +```js + const signature = await signTypedDataAsync({ + domain, + types, + primaryType: "GreetingRequest", + message, + }) +``` + +Actually get the signature. This function is asynchronous because users take a long time (from a computer's perspective) to sign data. + +```js + const r = `0x${signature.slice(2, 66)}` + const s = `0x${signature.slice(66, 130)}` + const v = parseInt(signature.slice(130, 132), 16) + + return { + req: { greeting }, + v, + r, + s, + } + }, +``` + +The function returns a single hexadecimal value. Here we divide it into fields. + +```js + [account, chainId, contractAddr, signTypedDataAsync], +) +``` + +If any of these variables change, create a new instance of the function. The `account` and `chainId` parameters can be changed by the user in the wallet. `contractAddr` is a function of the chain Id. `signTypedDataAsync` should not change, but we import it from [a hook](https://wagmi.sh/react/api/hooks/useSignTypedData), so we can't be sure, and it's best to add it here. + +Now that the new greeting is signed, we need to send it to the server. + +```js + const sponsoredGreeting = async () => { + try { +``` + +This function takes a signature and sends it to the server. + +```js + const signedMessage = await signGreeting(newGreeting) + const response = await fetch("/server/sponsor", { +``` + +Send to the path `/server/sponsor` in the server we came from. + +```js + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signedMessage), + }) +``` + +Use `POST` to send the information JSON-encoded. + +```js + const data = await response.json() + console.log("Server response:", data) + } catch (err) { + console.error("Error:", err) + } + } +``` + +Output the response. On a production system we'd also show the response to the user. + +### The server {#server} + +I like using [Vite](https://vite.dev/) as my front-end. It automatically serves the React libraries and updates the browser when the front-end code changes. However, Vite does not include backend tooling. + +The solution is in [`index.js`](https://github.com/qbzzt/260301-gasless/blob/main/server/index.js). + +```js + app.post("/server/sponsor", async (req, res) => { + ... + }) + + // Let Vite handle everything else + const vite = await createViteServer({ + server: { middlewareMode: true } + }) + + app.use(vite.middlewares) +``` + +First we register a handler for the requests we handle ourselves (`POST` to `/server/sponsor`). Then we create and use a Vite server to handle all other URLs. + +```js + app.post("/server/sponsor", async (req, res) => { + try { + const signed = req.body + + const txHash = await sepoliaClient.writeContract({ + address: greeterAddr, + abi: greeterABI, + functionName: 'sponsoredSetGreeting', + args: [signed.req, signed.v, signed.r, signed.s], + }) + } ... + }) +``` + +This is just a standard [viem](https://viem.sh/) blockchain call. + +### The smart contract {#smart-contract} + +Finally, [`Greeter.sol`](https://github.com/qbzzt/260301-gasless/blob/main/contracts/src/Greeter.sol) needs to verify the signature. + +```solidity + constructor(string memory _greeting) { + greeting = _greeting; + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("Greeter")), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } +``` + +The constructor creates the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator), similar to the user interface code above. Blockchain execution is much more expensive, so we only calculate it once. + +```solidity + struct GreetingRequest { + string greeting; + } +``` + +This is the structure that gets signed. Here we have just one field. + +```solidity + bytes32 private constant GREETING_TYPEHASH = + keccak256("GreetingRequest(string greeting)"); +``` + +This is the [structure identifier](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct). It is calculated each time in the user interface. + +```solidity + function sponsoredSetGreeting( + GreetingRequest calldata req, + uint8 v, + bytes32 r, + bytes32 s + ) external { +``` + +This function receives a signed request and updates the greeting. + +```solidity + // Compute EIP-712 digest + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + GREETING_TYPEHASH, + keccak256(bytes(req.greeting)) + ) + ) + ) + ); +``` + +Create the digest in accordance with [EIP 712](https://eips.ethereum.org/EIPS/eip-712). + +```solidity + // Recover signer + address signer = ecrecover(digest, v, r, s); + require(signer != address(0), "Invalid signature"); +``` + +Use [`ecrecover`](https://www.evm.codes/precompiled?fork=osaka#0x01) to get the signer address. Note that a bad signature can still result in a valid address, just a random one. + +```solidity + // Apply greeting as if signer called it + greeting = req.greeting; + emit SetGreeting(signer, req.greeting); + } +``` + +Update the greeting. + +## Vulnerabilities {#vulnerabilities} + +This is _not_ production-level code. Je zranitelný vůči významným útokům a postrádá hlavní funkce. Here are some, along with how to solve them. + +To see some of these attacks, click the buttons under the _Attacks_ heading and see what happens. For the **Invalid signature** button, check the server console to see the transaction response. + +### Denial of service on the server {#dos-on-server} + +The easiest attack is a [denial-of-service](https://en.wikipedia.org/wiki/Denial-of-service_attack) attack on the server. The server receives requests from anywhere on the Internet and based on those requests sends transactions. There is absolutely nothing preventing an attacker from issuing a bunch of signatures, valid or invalid. Each will cause a transaction. Eventually the server will run out of ETH to pay for gas. + +One solution to this problem is to limit the rate to one transaction per block. If the purpose is to show greetings to [externally owned accounts](/developers/docs/accounts/#key-differences), it does not matter what the greeting is in the middle of the block anyway. + +Another solution is to keep track of addresses and only allow signatures from valid customers. + +### Wrong greeting signatures {#wrong-greeting-sigs} + +When you click **Signature for wrong greeting**, you submit a valid signature for a specific address (`0xaA92c5d426430D4769c9E878C1333BDe3d689b3e`) and greeting (`Hello`). But it submits it with a different greeting. This confuses `ecrecover`, which changes the greeting but has the wrong address. + +To solve this problem, add the address to the [signed structure](https://github.com/qbzzt/260301-gasless/blob/main/server/src/Greeter.jsx#L122-L124). This way, the `ecrecover` random address won't match the address in the signature, and the smart contract will reject the message. + +### Replay attacks {#replay-attack} + +When you click **Replay attack**, you submit the same "I'm 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, and I'd like the greeting to be `Hello`" signature, but with the correct greeting. As a result, the smart contract believes that the address (which isn't yours) changed the greeting back to `Hello`. The information to do this is publicly available in the [transaction information](https://eth-sepolia.blockscout.com/tx/0xa66afe4bbf886f59533e677a798c802ceab1ac0f9db6e83a4d4b59a45cf7c1b1). + +If this is a problem, one solution is to add a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). Have a [mapping](https://docs.soliditylang.org/en/latest/types.html#mapping-types) between addresses and numbers, and add a nonce field to the signature. If the nonce field matches the mapping for the address, accept the signature and increment the mapping for next time. If it doesn't, reject the transaction. + +Another solution is to add a timestamp to the signed data and accept the signature as valid only for a few seconds after that timestamp. This is simpler and cheaper, but we risk replay attacks within the time window, and the failure of legitimate transactions if the time window is exceeded. + +## Other missing features {#other-missing-features} + +There are additional features we would add in a production setting. + +### Access from other servers {#other-servers} + +Currently, we allow any address to submit a `sponsorSetGreeting`. This may be exactly what we want, in the interest of decentralization. Or maybe we want to ensure that sponsored transactions go through _our_ server, in which case we'd check `msg.sender` in the smart contract. + +Either way, this should be a conscious design decision, not just the result of not thinking about the issue. + +### Error handling {#error-handling} + +A user submits a greeting. Maybe it gets updated at the next block. Maybe it doesn't. Errors are invisible. On a production system, the user should be able to distinguish between these cases: + +- The new greeting has not been submitted yet +- The new greeting has been submitted, and it's in process +- The new greeting has been rejected + +## Závěr {#conclusion} + +At this point, you should be able to create a gasless experience for your dapp users, at the cost of some centralization. + +However, this only works with smart contracts that support ERC-712. To transfer an ERC-20 token, for example, it is necessary to have the transaction signed by the owner rather than just a message. The solution is [account abstraction (ERC-4337)](https://docs.erc4337.io/index.html). I hope to write a future tutorial about it. + +[Více z mé práce najdete zde](https://cryptodocguy.pro/). diff --git a/public/content/translations/de/developers/tutorials/gasless/index.md b/public/content/translations/de/developers/tutorials/gasless/index.md new file mode 100644 index 00000000000..26fcbe3085e --- /dev/null +++ b/public/content/translations/de/developers/tutorials/gasless/index.md @@ -0,0 +1,362 @@ +--- +title: "Sponsoring gas fees: How to cover transaction costs for your users" +description: It is easy to create a private key and an address; it's just a matter of running the right software. But there are many places in the world where getting the ETH to send transactions is much harder. In this tutorial you learn how to cover the onchain gas costs for executing user-signed, offchain structured data in your smart contract. You have the user sign a structure containing the transaction information, which your offchain code then submits to the blockchain as a transaction. +author: Ori Pomerantz ist der Autor des Linux Kernel Module Programming Guide +tags: [ "gasless", "solidity", "eip-712", "meta-transactions" ] +skill: intermediate +lang: de +published: 2026-02-27 +--- + +## Einführung {#introduction} + +If we want Ethereum to serve [a billion more people](https://blog.ethereum.org/category/next-billion), we need to remove friction and make it as easy to use as possible. One source of this friction is the need for ETH to pay gas fees. + +If you have a dapp that makes money from users, it might make sense to let users submit transactions through your server and pay the transaction fees yourself. Because users still sign an [EIP-712 authorization message](https://eips.ethereum.org/EIPS/eip-712) in their wallets, they retain Ethereum's guarantees of integrity. Availability depends on the server that relays transactions, so it is more limited. However, you can set things up so users can also access the smart contract directly (if they get ETH), and let others set up their own servers if they want to sponsor transactions. + +The technique in this tutorial only works when you control the smart contract. There are other techniques, including [account abstraction](https://eips.ethereum.org/EIPS/eip-4337) that let you sponsor transactions to other smart contracts, which I hope to cover in a future tutorial. + +Note: This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Learn more in the [vulnerabilities section of this guide](#vulnerabilities). + +### Voraussetzungen {#prerequisites} + +To understand this tutorial you need to already be familiar with: + +- Solidity +- JavaScript +- React and WAGMI. If you are not familiar with these user interface tools, [we have a tutorial for that](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +## The sample application {#sample-app} + +The sample application here is a variant on Hardhat's `Greeter` contract. You can see it [on GitHub](https://github.com/qbzzt/260301-gasless). The smart contract is already deployed on the [Sepolia](https://sepolia.dev/), at address [`0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA`](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA). + +To see it in action, follow these steps. + +1. Clone the repository and install the necessary software. + + ```sh + git clone https://github.com/qbzzt/260301-gasless.git + cd 260301-gasless/server + npm install + ``` + +2. Edit `.env` to set `PRIVATE_KEY` to a wallet that has ETH on Sepolia. If you need Sepolia ETH, [use a faucet](/developers/docs/networks/#sepolia). Ideally, this private key should be different from the one you have in your browser wallet. + +3. Starten Sie den Server. + + ```sh + npm run dev + ``` + +4. Browse to the application at URL [`http://localhost:5173`](http://localhost:5173). + +5. Click **Connect with Injected** to connect to a wallet. Approve in the wallet, and approve the change to Sepolia if necessary. + +6. Write a new greeting and click **Update greeting via sponsor**. + +7. Sign the message. + +8. Wait about 12 seconds (the block time on Sepolia). While waiting you can look at the URL in the server's console to see the transaction. + +9. See that the greeting changed, and that the last updated by address value is now the address of your browser wallet. + +To understand how this works, we need to look at how the message gets created in the user interface, how it is relayed by the server, and how the smart contract processes it. + +### The user interface {#ui-changes} + +The user interface is based on [WAGMI](https://wagmi.sh/); you can read about it [in this tutorial](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +Here is how we sign the message: + +```js +const signGreeting = useCallback( +``` + +The React hook [`useCallback`](https://react.dev/reference/react/useCallback) lets us improve performance by reusing the same function when the component is redrawn. + +```js + async (greeting) => { + if (!account) throw new Error("Wallet not connected") +``` + +If there is no account, raise an error. This should never happen because the UI button that starts the process that calls `signGreeting` is disabled in that case. However, future programmers may remove that safeguard, so it's a good idea to check this condition here as well. + +```js + const domain = { + name: "Greeter", + version: "1", + chainId, + verifyingContract: contractAddr, + } +``` + +Parameters for the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator). This value is constant, so in a better-optimized implementation, we might calculate it once rather than recalculate it each time the function is called. + +- `name` is a user-readable name, such as the name of the dapp for which we are producing signatures. +- `version` is the version. Different versions are not compatible. +- `chainId` is the chain we are using, as provided [by WAGMI](https://wagmi.sh/react/api/hooks/useChainId). +- `verifyingContract` is the contract address that will verify this signature. We do not want the same signature to apply to multiple contracts, in case there are several `Greeter` contracts and we want them to have different greetings. + +```js + + const types = { + GreetingRequest: [ + { name: "greeting", type: "string" }, + ], + } +``` + +The data type we sign. Here, we have a single parameter, `greeting`, but real-life systems typically have more. + +```js + const message = { greeting } +``` + +The actual message we want to sign and send. `greeting` is both the field name and the name of the variable that fills it. + +```js + const signature = await signTypedDataAsync({ + domain, + types, + primaryType: "GreetingRequest", + message, + }) +``` + +Actually get the signature. This function is asynchronous because users take a long time (from a computer's perspective) to sign data. + +```js + const r = `0x${signature.slice(2, 66)}` + const s = `0x${signature.slice(66, 130)}` + const v = parseInt(signature.slice(130, 132), 16) + + return { + req: { greeting }, + v, + r, + s, + } + }, +``` + +The function returns a single hexadecimal value. Here we divide it into fields. + +```js + [account, chainId, contractAddr, signTypedDataAsync], +) +``` + +If any of these variables change, create a new instance of the function. The `account` and `chainId` parameters can be changed by the user in the wallet. `contractAddr` is a function of the chain Id. `signTypedDataAsync` should not change, but we import it from [a hook](https://wagmi.sh/react/api/hooks/useSignTypedData), so we can't be sure, and it's best to add it here. + +Now that the new greeting is signed, we need to send it to the server. + +```js + const sponsoredGreeting = async () => { + try { +``` + +This function takes a signature and sends it to the server. + +```js + const signedMessage = await signGreeting(newGreeting) + const response = await fetch("/server/sponsor", { +``` + +Send to the path `/server/sponsor` in the server we came from. + +```js + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signedMessage), + }) +``` + +Use `POST` to send the information JSON-encoded. + +```js + const data = await response.json() + console.log("Server response:", data) + } catch (err) { + console.error("Error:", err) + } + } +``` + +Output the response. On a production system we'd also show the response to the user. + +### The server {#server} + +I like using [Vite](https://vite.dev/) as my front-end. It automatically serves the React libraries and updates the browser when the front-end code changes. However, Vite does not include backend tooling. + +The solution is in [`index.js`](https://github.com/qbzzt/260301-gasless/blob/main/server/index.js). + +```js + app.post("/server/sponsor", async (req, res) => { + ... + }) + + // Let Vite handle everything else + const vite = await createViteServer({ + server: { middlewareMode: true } + }) + + app.use(vite.middlewares) +``` + +First we register a handler for the requests we handle ourselves (`POST` to `/server/sponsor`). Then we create and use a Vite server to handle all other URLs. + +```js + app.post("/server/sponsor", async (req, res) => { + try { + const signed = req.body + + const txHash = await sepoliaClient.writeContract({ + address: greeterAddr, + abi: greeterABI, + functionName: 'sponsoredSetGreeting', + args: [signed.req, signed.v, signed.r, signed.s], + }) + } ... + }) +``` + +This is just a standard [viem](https://viem.sh/) blockchain call. + +### The smart contract {#smart-contract} + +Finally, [`Greeter.sol`](https://github.com/qbzzt/260301-gasless/blob/main/contracts/src/Greeter.sol) needs to verify the signature. + +```solidity + constructor(string memory _greeting) { + greeting = _greeting; + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("Greeter")), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } +``` + +The constructor creates the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator), similar to the user interface code above. Blockchain execution is much more expensive, so we only calculate it once. + +```solidity + struct GreetingRequest { + string greeting; + } +``` + +This is the structure that gets signed. Here we have just one field. + +```solidity + bytes32 private constant GREETING_TYPEHASH = + keccak256("GreetingRequest(string greeting)"); +``` + +This is the [structure identifier](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct). It is calculated each time in the user interface. + +```solidity + function sponsoredSetGreeting( + GreetingRequest calldata req, + uint8 v, + bytes32 r, + bytes32 s + ) external { +``` + +This function receives a signed request and updates the greeting. + +```solidity + // Compute EIP-712 digest + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + GREETING_TYPEHASH, + keccak256(bytes(req.greeting)) + ) + ) + ) + ); +``` + +Create the digest in accordance with [EIP 712](https://eips.ethereum.org/EIPS/eip-712). + +```solidity + // Recover signer + address signer = ecrecover(digest, v, r, s); + require(signer != address(0), "Invalid signature"); +``` + +Use [`ecrecover`](https://www.evm.codes/precompiled?fork=osaka#0x01) to get the signer address. Note that a bad signature can still result in a valid address, just a random one. + +```solidity + // Apply greeting as if signer called it + greeting = req.greeting; + emit SetGreeting(signer, req.greeting); + } +``` + +Update the greeting. + +## Vulnerabilities {#vulnerabilities} + +This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Here are some, along with how to solve them. + +To see some of these attacks, click the buttons under the _Attacks_ heading and see what happens. For the **Invalid signature** button, check the server console to see the transaction response. + +### Denial of service on the server {#dos-on-server} + +The easiest attack is a [denial-of-service](https://en.wikipedia.org/wiki/Denial-of-service_attack) attack on the server. The server receives requests from anywhere on the Internet and based on those requests sends transactions. There is absolutely nothing preventing an attacker from issuing a bunch of signatures, valid or invalid. Each will cause a transaction. Eventually the server will run out of ETH to pay for gas. + +One solution to this problem is to limit the rate to one transaction per block. If the purpose is to show greetings to [externally owned accounts](/developers/docs/accounts/#key-differences), it does not matter what the greeting is in the middle of the block anyway. + +Another solution is to keep track of addresses and only allow signatures from valid customers. + +### Wrong greeting signatures {#wrong-greeting-sigs} + +When you click **Signature for wrong greeting**, you submit a valid signature for a specific address (`0xaA92c5d426430D4769c9E878C1333BDe3d689b3e`) and greeting (`Hello`). But it submits it with a different greeting. This confuses `ecrecover`, which changes the greeting but has the wrong address. + +To solve this problem, add the address to the [signed structure](https://github.com/qbzzt/260301-gasless/blob/main/server/src/Greeter.jsx#L122-L124). This way, the `ecrecover` random address won't match the address in the signature, and the smart contract will reject the message. + +### Replay attacks {#replay-attack} + +When you click **Replay attack**, you submit the same "I'm 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, and I'd like the greeting to be `Hello`" signature, but with the correct greeting. As a result, the smart contract believes that the address (which isn't yours) changed the greeting back to `Hello`. The information to do this is publicly available in the [transaction information](https://eth-sepolia.blockscout.com/tx/0xa66afe4bbf886f59533e677a798c802ceab1ac0f9db6e83a4d4b59a45cf7c1b1). + +If this is a problem, one solution is to add a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). Have a [mapping](https://docs.soliditylang.org/en/latest/types.html#mapping-types) between addresses and numbers, and add a nonce field to the signature. If the nonce field matches the mapping for the address, accept the signature and increment the mapping for next time. If it doesn't, reject the transaction. + +Another solution is to add a timestamp to the signed data and accept the signature as valid only for a few seconds after that timestamp. This is simpler and cheaper, but we risk replay attacks within the time window, and the failure of legitimate transactions if the time window is exceeded. + +## Other missing features {#other-missing-features} + +There are additional features we would add in a production setting. + +### Access from other servers {#other-servers} + +Currently, we allow any address to submit a `sponsorSetGreeting`. This may be exactly what we want, in the interest of decentralization. Or maybe we want to ensure that sponsored transactions go through _our_ server, in which case we'd check `msg.sender` in the smart contract. + +Either way, this should be a conscious design decision, not just the result of not thinking about the issue. + +### Error handling {#error-handling} + +A user submits a greeting. Maybe it gets updated at the next block. Maybe it doesn't. Errors are invisible. On a production system, the user should be able to distinguish between these cases: + +- The new greeting has not been submitted yet +- The new greeting has been submitted, and it's in process +- The new greeting has been rejected + +## Fazit {#conclusion} + +At this point, you should be able to create a gasless experience for your dapp users, at the cost of some centralization. + +However, this only works with smart contracts that support ERC-712. To transfer an ERC-20 token, for example, it is necessary to have the transaction signed by the owner rather than just a message. The solution is [account abstraction (ERC-4337)](https://docs.erc4337.io/index.html). I hope to write a future tutorial about it. + +[Hier finden Sie mehr von meiner Arbeit](https://cryptodocguy.pro/). diff --git a/public/content/translations/es/developers/tutorials/gasless/index.md b/public/content/translations/es/developers/tutorials/gasless/index.md new file mode 100644 index 00000000000..0b705a8e61d --- /dev/null +++ b/public/content/translations/es/developers/tutorials/gasless/index.md @@ -0,0 +1,362 @@ +--- +title: "Sponsoring gas fees: How to cover transaction costs for your users" +description: It is easy to create a private key and an address; it's just a matter of running the right software. But there are many places in the world where getting the ETH to send transactions is much harder. In this tutorial you learn how to cover the onchain gas costs for executing user-signed, offchain structured data in your smart contract. You have the user sign a structure containing the transaction information, which your offchain code then submits to the blockchain as a transaction. +author: Ori Pomerantz +tags: [ "gasless", "Solidity", "eip-712", "meta-transactions" ] +skill: intermediate +lang: es +published: 2026-02-27 +--- + +## Introducción {#introduction} + +If we want Ethereum to serve [a billion more people](https://blog.ethereum.org/category/next-billion), we need to remove friction and make it as easy to use as possible. One source of this friction is the need for ETH to pay gas fees. + +If you have a dapp that makes money from users, it might make sense to let users submit transactions through your server and pay the transaction fees yourself. Because users still sign an [EIP-712 authorization message](https://eips.ethereum.org/EIPS/eip-712) in their wallets, they retain Ethereum's guarantees of integrity. Availability depends on the server that relays transactions, so it is more limited. However, you can set things up so users can also access the smart contract directly (if they get ETH), and let others set up their own servers if they want to sponsor transactions. + +The technique in this tutorial only works when you control the smart contract. There are other techniques, including [account abstraction](https://eips.ethereum.org/EIPS/eip-4337) that let you sponsor transactions to other smart contracts, which I hope to cover in a future tutorial. + +Note: This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Learn more in the [vulnerabilities section of this guide](#vulnerabilities). + +### Requisitos previos {#prerequisites} + +To understand this tutorial you need to already be familiar with: + +- Solidez +- JavaScript +- React and WAGMI. If you are not familiar with these user interface tools, [we have a tutorial for that](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +## The sample application {#sample-app} + +The sample application here is a variant on Hardhat's `Greeter` contract. You can see it [on GitHub](https://github.com/qbzzt/260301-gasless). The smart contract is already deployed on the [Sepolia](https://sepolia.dev/), at address [`0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA`](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA). + +To see it in action, follow these steps. + +1. Clone the repository and install the necessary software. + + ```sh + git clone https://github.com/qbzzt/260301-gasless.git + cd 260301-gasless/server + npm install + ``` + +2. Edit `.env` to set `PRIVATE_KEY` to a wallet that has ETH on Sepolia. If you need Sepolia ETH, [use a faucet](/developers/docs/networks/#sepolia). Ideally, this private key should be different from the one you have in your browser wallet. + +3. Inicie el servidor. + + ```sh + npm run dev + ``` + +4. Browse to the application at URL [`http://localhost:5173`](http://localhost:5173). + +5. Click **Connect with Injected** to connect to a wallet. Approve in the wallet, and approve the change to Sepolia if necessary. + +6. Write a new greeting and click **Update greeting via sponsor**. + +7. Sign the message. + +8. Wait about 12 seconds (the block time on Sepolia). While waiting you can look at the URL in the server's console to see the transaction. + +9. See that the greeting changed, and that the last updated by address value is now the address of your browser wallet. + +To understand how this works, we need to look at how the message gets created in the user interface, how it is relayed by the server, and how the smart contract processes it. + +### The user interface {#ui-changes} + +The user interface is based on [WAGMI](https://wagmi.sh/); you can read about it [in this tutorial](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +Here is how we sign the message: + +```js +const signGreeting = useCallback( +``` + +The React hook [`useCallback`](https://react.dev/reference/react/useCallback) lets us improve performance by reusing the same function when the component is redrawn. + +```js + async (greeting) => { + if (!account) throw new Error("Wallet not connected") +``` + +If there is no account, raise an error. This should never happen because the UI button that starts the process that calls `signGreeting` is disabled in that case. However, future programmers may remove that safeguard, so it's a good idea to check this condition here as well. + +```js + const domain = { + name: "Greeter", + version: "1", + chainId, + verifyingContract: contractAddr, + } +``` + +Parameters for the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator). This value is constant, so in a better-optimized implementation, we might calculate it once rather than recalculate it each time the function is called. + +- `name` is a user-readable name, such as the name of the dapp for which we are producing signatures. +- `version` is the version. Different versions are not compatible. +- `chainId` is the chain we are using, as provided [by WAGMI](https://wagmi.sh/react/api/hooks/useChainId). +- `verifyingContract` is the contract address that will verify this signature. We do not want the same signature to apply to multiple contracts, in case there are several `Greeter` contracts and we want them to have different greetings. + +```js + + const types = { + GreetingRequest: [ + { name: "greeting", type: "string" }, + ], + } +``` + +The data type we sign. Here, we have a single parameter, `greeting`, but real-life systems typically have more. + +```js + const message = { greeting } +``` + +The actual message we want to sign and send. `greeting` is both the field name and the name of the variable that fills it. + +```js + const signature = await signTypedDataAsync({ + domain, + types, + primaryType: "GreetingRequest", + message, + }) +``` + +Actually get the signature. This function is asynchronous because users take a long time (from a computer's perspective) to sign data. + +```js + const r = `0x${signature.slice(2, 66)}` + const s = `0x${signature.slice(66, 130)}` + const v = parseInt(signature.slice(130, 132), 16) + + return { + req: { greeting }, + v, + r, + s, + } + }, +``` + +The function returns a single hexadecimal value. Here we divide it into fields. + +```js + [account, chainId, contractAddr, signTypedDataAsync], +) +``` + +If any of these variables change, create a new instance of the function. The `account` and `chainId` parameters can be changed by the user in the wallet. `contractAddr` is a function of the chain Id. `signTypedDataAsync` should not change, but we import it from [a hook](https://wagmi.sh/react/api/hooks/useSignTypedData), so we can't be sure, and it's best to add it here. + +Now that the new greeting is signed, we need to send it to the server. + +```js + const sponsoredGreeting = async () => { + try { +``` + +This function takes a signature and sends it to the server. + +```js + const signedMessage = await signGreeting(newGreeting) + const response = await fetch("/server/sponsor", { +``` + +Send to the path `/server/sponsor` in the server we came from. + +```js + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signedMessage), + }) +``` + +Use `POST` to send the information JSON-encoded. + +```js + const data = await response.json() + console.log("Server response:", data) + } catch (err) { + console.error("Error:", err) + } + } +``` + +Output the response. On a production system we'd also show the response to the user. + +### The server {#server} + +I like using [Vite](https://vite.dev/) as my front-end. It automatically serves the React libraries and updates the browser when the front-end code changes. However, Vite does not include backend tooling. + +The solution is in [`index.js`](https://github.com/qbzzt/260301-gasless/blob/main/server/index.js). + +```js + app.post("/server/sponsor", async (req, res) => { + ... + }) + + // Let Vite handle everything else + const vite = await createViteServer({ + server: { middlewareMode: true } + }) + + app.use(vite.middlewares) +``` + +First we register a handler for the requests we handle ourselves (`POST` to `/server/sponsor`). Then we create and use a Vite server to handle all other URLs. + +```js + app.post("/server/sponsor", async (req, res) => { + try { + const signed = req.body + + const txHash = await sepoliaClient.writeContract({ + address: greeterAddr, + abi: greeterABI, + functionName: 'sponsoredSetGreeting', + args: [signed.req, signed.v, signed.r, signed.s], + }) + } ... + }) +``` + +This is just a standard [viem](https://viem.sh/) blockchain call. + +### The smart contract {#smart-contract} + +Finally, [`Greeter.sol`](https://github.com/qbzzt/260301-gasless/blob/main/contracts/src/Greeter.sol) needs to verify the signature. + +```solidity + constructor(string memory _greeting) { + greeting = _greeting; + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("Greeter")), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } +``` + +The constructor creates the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator), similar to the user interface code above. Blockchain execution is much more expensive, so we only calculate it once. + +```solidity + struct GreetingRequest { + string greeting; + } +``` + +This is the structure that gets signed. Here we have just one field. + +```solidity + bytes32 private constant GREETING_TYPEHASH = + keccak256("GreetingRequest(string greeting)"); +``` + +This is the [structure identifier](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct). It is calculated each time in the user interface. + +```solidity + function sponsoredSetGreeting( + GreetingRequest calldata req, + uint8 v, + bytes32 r, + bytes32 s + ) external { +``` + +This function receives a signed request and updates the greeting. + +```solidity + // Compute EIP-712 digest + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + GREETING_TYPEHASH, + keccak256(bytes(req.greeting)) + ) + ) + ) + ); +``` + +Create the digest in accordance with [EIP 712](https://eips.ethereum.org/EIPS/eip-712). + +```solidity + // Recover signer + address signer = ecrecover(digest, v, r, s); + require(signer != address(0), "Invalid signature"); +``` + +Use [`ecrecover`](https://www.evm.codes/precompiled?fork=osaka#0x01) to get the signer address. Note that a bad signature can still result in a valid address, just a random one. + +```solidity + // Apply greeting as if signer called it + greeting = req.greeting; + emit SetGreeting(signer, req.greeting); + } +``` + +Update the greeting. + +## Vulnerabilities {#vulnerabilities} + +This is _not_ production-level code. Es vulnerable a ataques importantes y carece de características principales. Here are some, along with how to solve them. + +To see some of these attacks, click the buttons under the _Attacks_ heading and see what happens. For the **Invalid signature** button, check the server console to see the transaction response. + +### Denial of service on the server {#dos-on-server} + +The easiest attack is a [denial-of-service](https://en.wikipedia.org/wiki/Denial-of-service_attack) attack on the server. The server receives requests from anywhere on the Internet and based on those requests sends transactions. There is absolutely nothing preventing an attacker from issuing a bunch of signatures, valid or invalid. Each will cause a transaction. Eventually the server will run out of ETH to pay for gas. + +One solution to this problem is to limit the rate to one transaction per block. If the purpose is to show greetings to [externally owned accounts](/developers/docs/accounts/#key-differences), it does not matter what the greeting is in the middle of the block anyway. + +Another solution is to keep track of addresses and only allow signatures from valid customers. + +### Wrong greeting signatures {#wrong-greeting-sigs} + +When you click **Signature for wrong greeting**, you submit a valid signature for a specific address (`0xaA92c5d426430D4769c9E878C1333BDe3d689b3e`) and greeting (`Hello`). But it submits it with a different greeting. This confuses `ecrecover`, which changes the greeting but has the wrong address. + +To solve this problem, add the address to the [signed structure](https://github.com/qbzzt/260301-gasless/blob/main/server/src/Greeter.jsx#L122-L124). This way, the `ecrecover` random address won't match the address in the signature, and the smart contract will reject the message. + +### Replay attacks {#replay-attack} + +When you click **Replay attack**, you submit the same "I'm 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, and I'd like the greeting to be `Hello`" signature, but with the correct greeting. As a result, the smart contract believes that the address (which isn't yours) changed the greeting back to `Hello`. The information to do this is publicly available in the [transaction information](https://eth-sepolia.blockscout.com/tx/0xa66afe4bbf886f59533e677a798c802ceab1ac0f9db6e83a4d4b59a45cf7c1b1). + +If this is a problem, one solution is to add a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). Have a [mapping](https://docs.soliditylang.org/en/latest/types.html#mapping-types) between addresses and numbers, and add a nonce field to the signature. If the nonce field matches the mapping for the address, accept the signature and increment the mapping for next time. If it doesn't, reject the transaction. + +Another solution is to add a timestamp to the signed data and accept the signature as valid only for a few seconds after that timestamp. This is simpler and cheaper, but we risk replay attacks within the time window, and the failure of legitimate transactions if the time window is exceeded. + +## Other missing features {#other-missing-features} + +There are additional features we would add in a production setting. + +### Access from other servers {#other-servers} + +Currently, we allow any address to submit a `sponsorSetGreeting`. This may be exactly what we want, in the interest of decentralization. Or maybe we want to ensure that sponsored transactions go through _our_ server, in which case we'd check `msg.sender` in the smart contract. + +Either way, this should be a conscious design decision, not just the result of not thinking about the issue. + +### Error handling {#error-handling} + +A user submits a greeting. Maybe it gets updated at the next block. Maybe it doesn't. Errors are invisible. On a production system, the user should be able to distinguish between these cases: + +- The new greeting has not been submitted yet +- The new greeting has been submitted, and it's in process +- The new greeting has been rejected + +## Conclusión {#conclusion} + +At this point, you should be able to create a gasless experience for your dapp users, at the cost of some centralization. + +However, this only works with smart contracts that support ERC-712. To transfer an ERC-20 token, for example, it is necessary to have the transaction signed by the owner rather than just a message. The solution is [account abstraction (ERC-4337)](https://docs.erc4337.io/index.html). I hope to write a future tutorial about it. + +[Vea aquí más de mi trabajo](https://cryptodocguy.pro/). diff --git a/public/content/translations/fr/developers/tutorials/gasless/index.md b/public/content/translations/fr/developers/tutorials/gasless/index.md new file mode 100644 index 00000000000..3305a34e05b --- /dev/null +++ b/public/content/translations/fr/developers/tutorials/gasless/index.md @@ -0,0 +1,362 @@ +--- +title: "Sponsoring gas fees: How to cover transaction costs for your users" +description: It is easy to create a private key and an address; it's just a matter of running the right software. But there are many places in the world where getting the ETH to send transactions is much harder. In this tutorial you learn how to cover the onchain gas costs for executing user-signed, offchain structured data in your smart contract. You have the user sign a structure containing the transaction information, which your offchain code then submits to the blockchain as a transaction. +author: Ori Pomerantz +tags: [ "gasless", "solidité", "eip-712", "meta-transactions" ] +skill: intermediate +lang: fr +published: 2026-02-27 +--- + +## Introduction {#introduction} + +If we want Ethereum to serve [a billion more people](https://blog.ethereum.org/category/next-billion), we need to remove friction and make it as easy to use as possible. One source of this friction is the need for ETH to pay gas fees. + +If you have a dapp that makes money from users, it might make sense to let users submit transactions through your server and pay the transaction fees yourself. Because users still sign an [EIP-712 authorization message](https://eips.ethereum.org/EIPS/eip-712) in their wallets, they retain Ethereum's guarantees of integrity. Availability depends on the server that relays transactions, so it is more limited. However, you can set things up so users can also access the smart contract directly (if they get ETH), and let others set up their own servers if they want to sponsor transactions. + +The technique in this tutorial only works when you control the smart contract. There are other techniques, including [account abstraction](https://eips.ethereum.org/EIPS/eip-4337) that let you sponsor transactions to other smart contracts, which I hope to cover in a future tutorial. + +Note: This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Learn more in the [vulnerabilities section of this guide](#vulnerabilities). + +### Prérequis {#prerequisites} + +To understand this tutorial you need to already be familiar with: + +- solidity +- JavaScript +- React and WAGMI. If you are not familiar with these user interface tools, [we have a tutorial for that](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +## The sample application {#sample-app} + +The sample application here is a variant on Hardhat's `Greeter` contract. You can see it [on GitHub](https://github.com/qbzzt/260301-gasless). The smart contract is already deployed on the [Sepolia](https://sepolia.dev/), at address [`0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA`](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA). + +To see it in action, follow these steps. + +1. Clone the repository and install the necessary software. + + ```sh + git clone https://github.com/qbzzt/260301-gasless.git + cd 260301-gasless/server + npm install + ``` + +2. Edit `.env` to set `PRIVATE_KEY` to a wallet that has ETH on Sepolia. If you need Sepolia ETH, [use a faucet](/developers/docs/networks/#sepolia). Ideally, this private key should be different from the one you have in your browser wallet. + +3. Démarrer le serveur. + + ```sh + npm run dev + ``` + +4. Browse to the application at URL [`http://localhost:5173`](http://localhost:5173). + +5. Click **Connect with Injected** to connect to a wallet. Approve in the wallet, and approve the change to Sepolia if necessary. + +6. Write a new greeting and click **Update greeting via sponsor**. + +7. Sign the message. + +8. Wait about 12 seconds (the block time on Sepolia). While waiting you can look at the URL in the server's console to see the transaction. + +9. See that the greeting changed, and that the last updated by address value is now the address of your browser wallet. + +To understand how this works, we need to look at how the message gets created in the user interface, how it is relayed by the server, and how the smart contract processes it. + +### The user interface {#ui-changes} + +The user interface is based on [WAGMI](https://wagmi.sh/); you can read about it [in this tutorial](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +Here is how we sign the message: + +```js +const signGreeting = useCallback( +``` + +The React hook [`useCallback`](https://react.dev/reference/react/useCallback) lets us improve performance by reusing the same function when the component is redrawn. + +```js + async (greeting) => { + if (!account) throw new Error("Wallet not connected") +``` + +If there is no account, raise an error. This should never happen because the UI button that starts the process that calls `signGreeting` is disabled in that case. However, future programmers may remove that safeguard, so it's a good idea to check this condition here as well. + +```js + const domain = { + name: "Greeter", + version: "1", + chainId, + verifyingContract: contractAddr, + } +``` + +Parameters for the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator). This value is constant, so in a better-optimized implementation, we might calculate it once rather than recalculate it each time the function is called. + +- `name` is a user-readable name, such as the name of the dapp for which we are producing signatures. +- `version` is the version. Different versions are not compatible. +- `chainId` is the chain we are using, as provided [by WAGMI](https://wagmi.sh/react/api/hooks/useChainId). +- `verifyingContract` is the contract address that will verify this signature. We do not want the same signature to apply to multiple contracts, in case there are several `Greeter` contracts and we want them to have different greetings. + +```js + + const types = { + GreetingRequest: [ + { name: "greeting", type: "string" }, + ], + } +``` + +The data type we sign. Here, we have a single parameter, `greeting`, but real-life systems typically have more. + +```js + const message = { greeting } +``` + +The actual message we want to sign and send. `greeting` is both the field name and the name of the variable that fills it. + +```js + const signature = await signTypedDataAsync({ + domain, + types, + primaryType: "GreetingRequest", + message, + }) +``` + +Actually get the signature. This function is asynchronous because users take a long time (from a computer's perspective) to sign data. + +```js + const r = `0x${signature.slice(2, 66)}` + const s = `0x${signature.slice(66, 130)}` + const v = parseInt(signature.slice(130, 132), 16) + + return { + req: { greeting }, + v, + r, + s, + } + }, +``` + +The function returns a single hexadecimal value. Here we divide it into fields. + +```js + [account, chainId, contractAddr, signTypedDataAsync], +) +``` + +If any of these variables change, create a new instance of the function. The `account` and `chainId` parameters can be changed by the user in the wallet. `contractAddr` is a function of the chain Id. `signTypedDataAsync` should not change, but we import it from [a hook](https://wagmi.sh/react/api/hooks/useSignTypedData), so we can't be sure, and it's best to add it here. + +Now that the new greeting is signed, we need to send it to the server. + +```js + const sponsoredGreeting = async () => { + try { +``` + +This function takes a signature and sends it to the server. + +```js + const signedMessage = await signGreeting(newGreeting) + const response = await fetch("/server/sponsor", { +``` + +Send to the path `/server/sponsor` in the server we came from. + +```js + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signedMessage), + }) +``` + +Use `POST` to send the information JSON-encoded. + +```js + const data = await response.json() + console.log("Server response:", data) + } catch (err) { + console.error("Error:", err) + } + } +``` + +Output the response. On a production system we'd also show the response to the user. + +### The server {#server} + +I like using [Vite](https://vite.dev/) as my front-end. It automatically serves the React libraries and updates the browser when the front-end code changes. However, Vite does not include backend tooling. + +The solution is in [`index.js`](https://github.com/qbzzt/260301-gasless/blob/main/server/index.js). + +```js + app.post("/server/sponsor", async (req, res) => { + ... + }) + + // Let Vite handle everything else + const vite = await createViteServer({ + server: { middlewareMode: true } + }) + + app.use(vite.middlewares) +``` + +First we register a handler for the requests we handle ourselves (`POST` to `/server/sponsor`). Then we create and use a Vite server to handle all other URLs. + +```js + app.post("/server/sponsor", async (req, res) => { + try { + const signed = req.body + + const txHash = await sepoliaClient.writeContract({ + address: greeterAddr, + abi: greeterABI, + functionName: 'sponsoredSetGreeting', + args: [signed.req, signed.v, signed.r, signed.s], + }) + } ... + }) +``` + +This is just a standard [viem](https://viem.sh/) blockchain call. + +### The smart contract {#smart-contract} + +Finally, [`Greeter.sol`](https://github.com/qbzzt/260301-gasless/blob/main/contracts/src/Greeter.sol) needs to verify the signature. + +```solidity + constructor(string memory _greeting) { + greeting = _greeting; + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("Greeter")), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } +``` + +The constructor creates the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator), similar to the user interface code above. Blockchain execution is much more expensive, so we only calculate it once. + +```solidity + struct GreetingRequest { + string greeting; + } +``` + +This is the structure that gets signed. Here we have just one field. + +```solidity + bytes32 private constant GREETING_TYPEHASH = + keccak256("GreetingRequest(string greeting)"); +``` + +This is the [structure identifier](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct). It is calculated each time in the user interface. + +```solidity + function sponsoredSetGreeting( + GreetingRequest calldata req, + uint8 v, + bytes32 r, + bytes32 s + ) external { +``` + +This function receives a signed request and updates the greeting. + +```solidity + // Compute EIP-712 digest + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + GREETING_TYPEHASH, + keccak256(bytes(req.greeting)) + ) + ) + ) + ); +``` + +Create the digest in accordance with [EIP 712](https://eips.ethereum.org/EIPS/eip-712). + +```solidity + // Recover signer + address signer = ecrecover(digest, v, r, s); + require(signer != address(0), "Invalid signature"); +``` + +Use [`ecrecover`](https://www.evm.codes/precompiled?fork=osaka#0x01) to get the signer address. Note that a bad signature can still result in a valid address, just a random one. + +```solidity + // Apply greeting as if signer called it + greeting = req.greeting; + emit SetGreeting(signer, req.greeting); + } +``` + +Update the greeting. + +## Vulnerabilities {#vulnerabilities} + +This is _not_ production-level code. Il est vulnérable à des attaques importantes et ne dispose pas de fonctionnalités majeures. Here are some, along with how to solve them. + +To see some of these attacks, click the buttons under the _Attacks_ heading and see what happens. For the **Invalid signature** button, check the server console to see the transaction response. + +### Denial of service on the server {#dos-on-server} + +The easiest attack is a [denial-of-service](https://en.wikipedia.org/wiki/Denial-of-service_attack) attack on the server. The server receives requests from anywhere on the Internet and based on those requests sends transactions. There is absolutely nothing preventing an attacker from issuing a bunch of signatures, valid or invalid. Each will cause a transaction. Eventually the server will run out of ETH to pay for gas. + +One solution to this problem is to limit the rate to one transaction per block. If the purpose is to show greetings to [externally owned accounts](/developers/docs/accounts/#key-differences), it does not matter what the greeting is in the middle of the block anyway. + +Another solution is to keep track of addresses and only allow signatures from valid customers. + +### Wrong greeting signatures {#wrong-greeting-sigs} + +When you click **Signature for wrong greeting**, you submit a valid signature for a specific address (`0xaA92c5d426430D4769c9E878C1333BDe3d689b3e`) and greeting (`Hello`). But it submits it with a different greeting. This confuses `ecrecover`, which changes the greeting but has the wrong address. + +To solve this problem, add the address to the [signed structure](https://github.com/qbzzt/260301-gasless/blob/main/server/src/Greeter.jsx#L122-L124). This way, the `ecrecover` random address won't match the address in the signature, and the smart contract will reject the message. + +### Replay attacks {#replay-attack} + +When you click **Replay attack**, you submit the same "I'm 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, and I'd like the greeting to be `Hello`" signature, but with the correct greeting. As a result, the smart contract believes that the address (which isn't yours) changed the greeting back to `Hello`. The information to do this is publicly available in the [transaction information](https://eth-sepolia.blockscout.com/tx/0xa66afe4bbf886f59533e677a798c802ceab1ac0f9db6e83a4d4b59a45cf7c1b1). + +If this is a problem, one solution is to add a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). Have a [mapping](https://docs.soliditylang.org/en/latest/types.html#mapping-types) between addresses and numbers, and add a nonce field to the signature. If the nonce field matches the mapping for the address, accept the signature and increment the mapping for next time. If it doesn't, reject the transaction. + +Another solution is to add a timestamp to the signed data and accept the signature as valid only for a few seconds after that timestamp. This is simpler and cheaper, but we risk replay attacks within the time window, and the failure of legitimate transactions if the time window is exceeded. + +## Other missing features {#other-missing-features} + +There are additional features we would add in a production setting. + +### Access from other servers {#other-servers} + +Currently, we allow any address to submit a `sponsorSetGreeting`. This may be exactly what we want, in the interest of decentralization. Or maybe we want to ensure that sponsored transactions go through _our_ server, in which case we'd check `msg.sender` in the smart contract. + +Either way, this should be a conscious design decision, not just the result of not thinking about the issue. + +### Error handling {#error-handling} + +A user submits a greeting. Maybe it gets updated at the next block. Maybe it doesn't. Errors are invisible. On a production system, the user should be able to distinguish between these cases: + +- The new greeting has not been submitted yet +- The new greeting has been submitted, and it's in process +- The new greeting has been rejected + +## Conclusion {#conclusion} + +At this point, you should be able to create a gasless experience for your dapp users, at the cost of some centralization. + +However, this only works with smart contracts that support ERC-712. To transfer an ERC-20 token, for example, it is necessary to have the transaction signed by the owner rather than just a message. The solution is [account abstraction (ERC-4337)](https://docs.erc4337.io/index.html). I hope to write a future tutorial about it. + +[Voir ici pour plus de mon travail](https://cryptodocguy.pro/). diff --git a/public/content/translations/hi/developers/tutorials/gasless/index.md b/public/content/translations/hi/developers/tutorials/gasless/index.md new file mode 100644 index 00000000000..404c0704cc0 --- /dev/null +++ b/public/content/translations/hi/developers/tutorials/gasless/index.md @@ -0,0 +1,362 @@ +--- +title: "Sponsoring gas fees: How to cover transaction costs for your users" +description: It is easy to create a private key and an address; it's just a matter of running the right software. But there are many places in the world where getting the ETH to send transactions is much harder. In this tutorial you learn how to cover the onchain gas costs for executing user-signed, offchain structured data in your smart contract. You have the user sign a structure containing the transaction information, which your offchain code then submits to the blockchain as a transaction. +author: "ओरी पोमेरेन्ट्ज़" +tags: [ "gasless", "सोलिडीटी", "eip-712", "meta-transactions" ] +skill: intermediate +lang: hi +published: 2026-02-27 +--- + +## परिचय {#introduction} + +If we want Ethereum to serve [a billion more people](https://blog.ethereum.org/category/next-billion), we need to remove friction and make it as easy to use as possible. One source of this friction is the need for ETH to pay gas fees. + +If you have a dapp that makes money from users, it might make sense to let users submit transactions through your server and pay the transaction fees yourself. Because users still sign an [EIP-712 authorization message](https://eips.ethereum.org/EIPS/eip-712) in their wallets, they retain Ethereum's guarantees of integrity. Availability depends on the server that relays transactions, so it is more limited. However, you can set things up so users can also access the smart contract directly (if they get ETH), and let others set up their own servers if they want to sponsor transactions. + +The technique in this tutorial only works when you control the smart contract. There are other techniques, including [account abstraction](https://eips.ethereum.org/EIPS/eip-4337) that let you sponsor transactions to other smart contracts, which I hope to cover in a future tutorial. + +Note: This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Learn more in the [vulnerabilities section of this guide](#vulnerabilities). + +### पूर्वापेक्षाएं {#prerequisites} + +To understand this tutorial you need to already be familiar with: + +- Solidity +- JavaScript +- React and WAGMI. If you are not familiar with these user interface tools, [we have a tutorial for that](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +## The sample application {#sample-app} + +The sample application here is a variant on Hardhat's `Greeter` contract. You can see it [on GitHub](https://github.com/qbzzt/260301-gasless). The smart contract is already deployed on the [Sepolia](https://sepolia.dev/), at address [`0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA`](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA). + +To see it in action, follow these steps. + +1. Clone the repository and install the necessary software. + + ```sh + git clone https://github.com/qbzzt/260301-gasless.git + cd 260301-gasless/server + npm install + ``` + +2. Edit `.env` to set `PRIVATE_KEY` to a wallet that has ETH on Sepolia. If you need Sepolia ETH, [use a faucet](/developers/docs/networks/#sepolia). Ideally, this private key should be different from the one you have in your browser wallet. + +3. सर्वर शुरू करें। + + ```sh + npm run dev + ``` + +4. Browse to the application at URL [`http://localhost:5173`](http://localhost:5173). + +5. Click **Connect with Injected** to connect to a wallet. Approve in the wallet, and approve the change to Sepolia if necessary. + +6. Write a new greeting and click **Update greeting via sponsor**. + +7. Sign the message. + +8. Wait about 12 seconds (the block time on Sepolia). While waiting you can look at the URL in the server's console to see the transaction. + +9. See that the greeting changed, and that the last updated by address value is now the address of your browser wallet. + +To understand how this works, we need to look at how the message gets created in the user interface, how it is relayed by the server, and how the smart contract processes it. + +### The user interface {#ui-changes} + +The user interface is based on [WAGMI](https://wagmi.sh/); you can read about it [in this tutorial](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +Here is how we sign the message: + +```js +const signGreeting = useCallback( +``` + +The React hook [`useCallback`](https://react.dev/reference/react/useCallback) lets us improve performance by reusing the same function when the component is redrawn. + +```js + async (greeting) => { + if (!account) throw new Error("Wallet not connected") +``` + +If there is no account, raise an error. This should never happen because the UI button that starts the process that calls `signGreeting` is disabled in that case. However, future programmers may remove that safeguard, so it's a good idea to check this condition here as well. + +```js + const domain = { + name: "Greeter", + version: "1", + chainId, + verifyingContract: contractAddr, + } +``` + +Parameters for the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator). This value is constant, so in a better-optimized implementation, we might calculate it once rather than recalculate it each time the function is called. + +- `name` is a user-readable name, such as the name of the dapp for which we are producing signatures. +- `version` is the version. Different versions are not compatible. +- `chainId` is the chain we are using, as provided [by WAGMI](https://wagmi.sh/react/api/hooks/useChainId). +- `verifyingContract` is the contract address that will verify this signature. We do not want the same signature to apply to multiple contracts, in case there are several `Greeter` contracts and we want them to have different greetings. + +```js + + const types = { + GreetingRequest: [ + { name: "greeting", type: "string" }, + ], + } +``` + +The data type we sign. Here, we have a single parameter, `greeting`, but real-life systems typically have more. + +```js + const message = { greeting } +``` + +The actual message we want to sign and send. `greeting` is both the field name and the name of the variable that fills it. + +```js + const signature = await signTypedDataAsync({ + domain, + types, + primaryType: "GreetingRequest", + message, + }) +``` + +Actually get the signature. This function is asynchronous because users take a long time (from a computer's perspective) to sign data. + +```js + const r = `0x${signature.slice(2, 66)}` + const s = `0x${signature.slice(66, 130)}` + const v = parseInt(signature.slice(130, 132), 16) + + return { + req: { greeting }, + v, + r, + s, + } + }, +``` + +The function returns a single hexadecimal value. Here we divide it into fields. + +```js + [account, chainId, contractAddr, signTypedDataAsync], +) +``` + +If any of these variables change, create a new instance of the function. The `account` and `chainId` parameters can be changed by the user in the wallet. `contractAddr` is a function of the chain Id. `signTypedDataAsync` should not change, but we import it from [a hook](https://wagmi.sh/react/api/hooks/useSignTypedData), so we can't be sure, and it's best to add it here. + +Now that the new greeting is signed, we need to send it to the server. + +```js + const sponsoredGreeting = async () => { + try { +``` + +This function takes a signature and sends it to the server. + +```js + const signedMessage = await signGreeting(newGreeting) + const response = await fetch("/server/sponsor", { +``` + +Send to the path `/server/sponsor` in the server we came from. + +```js + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signedMessage), + }) +``` + +Use `POST` to send the information JSON-encoded. + +```js + const data = await response.json() + console.log("Server response:", data) + } catch (err) { + console.error("Error:", err) + } + } +``` + +Output the response. On a production system we'd also show the response to the user. + +### The server {#server} + +I like using [Vite](https://vite.dev/) as my front-end. It automatically serves the React libraries and updates the browser when the front-end code changes. However, Vite does not include backend tooling. + +The solution is in [`index.js`](https://github.com/qbzzt/260301-gasless/blob/main/server/index.js). + +```js + app.post("/server/sponsor", async (req, res) => { + ... + }) + + // Let Vite handle everything else + const vite = await createViteServer({ + server: { middlewareMode: true } + }) + + app.use(vite.middlewares) +``` + +First we register a handler for the requests we handle ourselves (`POST` to `/server/sponsor`). Then we create and use a Vite server to handle all other URLs. + +```js + app.post("/server/sponsor", async (req, res) => { + try { + const signed = req.body + + const txHash = await sepoliaClient.writeContract({ + address: greeterAddr, + abi: greeterABI, + functionName: 'sponsoredSetGreeting', + args: [signed.req, signed.v, signed.r, signed.s], + }) + } ... + }) +``` + +This is just a standard [viem](https://viem.sh/) blockchain call. + +### The smart contract {#smart-contract} + +Finally, [`Greeter.sol`](https://github.com/qbzzt/260301-gasless/blob/main/contracts/src/Greeter.sol) needs to verify the signature. + +```solidity + constructor(string memory _greeting) { + greeting = _greeting; + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("Greeter")), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } +``` + +The constructor creates the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator), similar to the user interface code above. Blockchain execution is much more expensive, so we only calculate it once. + +```solidity + struct GreetingRequest { + string greeting; + } +``` + +This is the structure that gets signed. Here we have just one field. + +```solidity + bytes32 private constant GREETING_TYPEHASH = + keccak256("GreetingRequest(string greeting)"); +``` + +This is the [structure identifier](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct). It is calculated each time in the user interface. + +```solidity + function sponsoredSetGreeting( + GreetingRequest calldata req, + uint8 v, + bytes32 r, + bytes32 s + ) external { +``` + +This function receives a signed request and updates the greeting. + +```solidity + // Compute EIP-712 digest + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + GREETING_TYPEHASH, + keccak256(bytes(req.greeting)) + ) + ) + ) + ); +``` + +Create the digest in accordance with [EIP 712](https://eips.ethereum.org/EIPS/eip-712). + +```solidity + // Recover signer + address signer = ecrecover(digest, v, r, s); + require(signer != address(0), "Invalid signature"); +``` + +Use [`ecrecover`](https://www.evm.codes/precompiled?fork=osaka#0x01) to get the signer address. Note that a bad signature can still result in a valid address, just a random one. + +```solidity + // Apply greeting as if signer called it + greeting = req.greeting; + emit SetGreeting(signer, req.greeting); + } +``` + +Update the greeting. + +## Vulnerabilities {#vulnerabilities} + +This is _not_ production-level code. यह महत्वपूर्ण हमलों के प्रति असुरक्षित है और इसमें प्रमुख सुविधाओं का अभाव है। Here are some, along with how to solve them. + +To see some of these attacks, click the buttons under the _Attacks_ heading and see what happens. For the **Invalid signature** button, check the server console to see the transaction response. + +### Denial of service on the server {#dos-on-server} + +The easiest attack is a [denial-of-service](https://en.wikipedia.org/wiki/Denial-of-service_attack) attack on the server. The server receives requests from anywhere on the Internet and based on those requests sends transactions. There is absolutely nothing preventing an attacker from issuing a bunch of signatures, valid or invalid. Each will cause a transaction. Eventually the server will run out of ETH to pay for gas. + +One solution to this problem is to limit the rate to one transaction per block. If the purpose is to show greetings to [externally owned accounts](/developers/docs/accounts/#key-differences), it does not matter what the greeting is in the middle of the block anyway. + +Another solution is to keep track of addresses and only allow signatures from valid customers. + +### Wrong greeting signatures {#wrong-greeting-sigs} + +When you click **Signature for wrong greeting**, you submit a valid signature for a specific address (`0xaA92c5d426430D4769c9E878C1333BDe3d689b3e`) and greeting (`Hello`). But it submits it with a different greeting. This confuses `ecrecover`, which changes the greeting but has the wrong address. + +To solve this problem, add the address to the [signed structure](https://github.com/qbzzt/260301-gasless/blob/main/server/src/Greeter.jsx#L122-L124). This way, the `ecrecover` random address won't match the address in the signature, and the smart contract will reject the message. + +### Replay attacks {#replay-attack} + +When you click **Replay attack**, you submit the same "I'm 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, and I'd like the greeting to be `Hello`" signature, but with the correct greeting. As a result, the smart contract believes that the address (which isn't yours) changed the greeting back to `Hello`. The information to do this is publicly available in the [transaction information](https://eth-sepolia.blockscout.com/tx/0xa66afe4bbf886f59533e677a798c802ceab1ac0f9db6e83a4d4b59a45cf7c1b1). + +If this is a problem, one solution is to add a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). Have a [mapping](https://docs.soliditylang.org/en/latest/types.html#mapping-types) between addresses and numbers, and add a nonce field to the signature. If the nonce field matches the mapping for the address, accept the signature and increment the mapping for next time. If it doesn't, reject the transaction. + +Another solution is to add a timestamp to the signed data and accept the signature as valid only for a few seconds after that timestamp. This is simpler and cheaper, but we risk replay attacks within the time window, and the failure of legitimate transactions if the time window is exceeded. + +## Other missing features {#other-missing-features} + +There are additional features we would add in a production setting. + +### Access from other servers {#other-servers} + +Currently, we allow any address to submit a `sponsorSetGreeting`. This may be exactly what we want, in the interest of decentralization. Or maybe we want to ensure that sponsored transactions go through _our_ server, in which case we'd check `msg.sender` in the smart contract. + +Either way, this should be a conscious design decision, not just the result of not thinking about the issue. + +### Error handling {#error-handling} + +A user submits a greeting. Maybe it gets updated at the next block. Maybe it doesn't. Errors are invisible. On a production system, the user should be able to distinguish between these cases: + +- The new greeting has not been submitted yet +- The new greeting has been submitted, and it's in process +- The new greeting has been rejected + +## निष्कर्ष {#conclusion} + +At this point, you should be able to create a gasless experience for your dapp users, at the cost of some centralization. + +However, this only works with smart contracts that support ERC-712. To transfer an ERC-20 token, for example, it is necessary to have the transaction signed by the owner rather than just a message. The solution is [account abstraction (ERC-4337)](https://docs.erc4337.io/index.html). I hope to write a future tutorial about it. + +[मेरे और काम के लिए यहाँ देखें](https://cryptodocguy.pro/)। diff --git a/public/content/translations/id/developers/tutorials/gasless/index.md b/public/content/translations/id/developers/tutorials/gasless/index.md new file mode 100644 index 00000000000..615abfe1fe6 --- /dev/null +++ b/public/content/translations/id/developers/tutorials/gasless/index.md @@ -0,0 +1,362 @@ +--- +title: "Sponsoring gas fees: How to cover transaction costs for your users" +description: It is easy to create a private key and an address; it's just a matter of running the right software. But there are many places in the world where getting the ETH to send transactions is much harder. In this tutorial you learn how to cover the onchain gas costs for executing user-signed, offchain structured data in your smart contract. You have the user sign a structure containing the transaction information, which your offchain code then submits to the blockchain as a transaction. +author: Ori Pomerantz +tags: [ "gasless", "Solidity", "eip-712", "meta-transactions" ] +skill: intermediate +lang: id +published: 2026-02-27 +--- + +## Pengenalan {#introduction} + +If we want Ethereum to serve [a billion more people](https://blog.ethereum.org/category/next-billion), we need to remove friction and make it as easy to use as possible. One source of this friction is the need for ETH to pay gas fees. + +If you have a dapp that makes money from users, it might make sense to let users submit transactions through your server and pay the transaction fees yourself. Because users still sign an [EIP-712 authorization message](https://eips.ethereum.org/EIPS/eip-712) in their wallets, they retain Ethereum's guarantees of integrity. Availability depends on the server that relays transactions, so it is more limited. However, you can set things up so users can also access the smart contract directly (if they get ETH), and let others set up their own servers if they want to sponsor transactions. + +The technique in this tutorial only works when you control the smart contract. There are other techniques, including [account abstraction](https://eips.ethereum.org/EIPS/eip-4337) that let you sponsor transactions to other smart contracts, which I hope to cover in a future tutorial. + +Note: This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Learn more in the [vulnerabilities section of this guide](#vulnerabilities). + +### Persyaratan {#prerequisites} + +To understand this tutorial you need to already be familiar with: + +- Solidity +- JavaScript +- React and WAGMI. If you are not familiar with these user interface tools, [we have a tutorial for that](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +## The sample application {#sample-app} + +The sample application here is a variant on Hardhat's `Greeter` contract. You can see it [on GitHub](https://github.com/qbzzt/260301-gasless). The smart contract is already deployed on the [Sepolia](https://sepolia.dev/), at address [`0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA`](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA). + +To see it in action, follow these steps. + +1. Clone the repository and install the necessary software. + + ```sh + git clone https://github.com/qbzzt/260301-gasless.git + cd 260301-gasless/server + npm install + ``` + +2. Edit `.env` to set `PRIVATE_KEY` to a wallet that has ETH on Sepolia. If you need Sepolia ETH, [use a faucet](/developers/docs/networks/#sepolia). Ideally, this private key should be different from the one you have in your browser wallet. + +3. Mulai server. + + ```sh + npm run dev + ``` + +4. Browse to the application at URL [`http://localhost:5173`](http://localhost:5173). + +5. Click **Connect with Injected** to connect to a wallet. Approve in the wallet, and approve the change to Sepolia if necessary. + +6. Write a new greeting and click **Update greeting via sponsor**. + +7. Sign the message. + +8. Wait about 12 seconds (the block time on Sepolia). While waiting you can look at the URL in the server's console to see the transaction. + +9. See that the greeting changed, and that the last updated by address value is now the address of your browser wallet. + +To understand how this works, we need to look at how the message gets created in the user interface, how it is relayed by the server, and how the smart contract processes it. + +### The user interface {#ui-changes} + +The user interface is based on [WAGMI](https://wagmi.sh/); you can read about it [in this tutorial](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +Here is how we sign the message: + +```js +const signGreeting = useCallback( +``` + +The React hook [`useCallback`](https://react.dev/reference/react/useCallback) lets us improve performance by reusing the same function when the component is redrawn. + +```js + async (greeting) => { + if (!account) throw new Error("Wallet not connected") +``` + +If there is no account, raise an error. This should never happen because the UI button that starts the process that calls `signGreeting` is disabled in that case. However, future programmers may remove that safeguard, so it's a good idea to check this condition here as well. + +```js + const domain = { + name: "Greeter", + version: "1", + chainId, + verifyingContract: contractAddr, + } +``` + +Parameters for the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator). This value is constant, so in a better-optimized implementation, we might calculate it once rather than recalculate it each time the function is called. + +- `name` is a user-readable name, such as the name of the dapp for which we are producing signatures. +- `version` is the version. Different versions are not compatible. +- `chainId` is the chain we are using, as provided [by WAGMI](https://wagmi.sh/react/api/hooks/useChainId). +- `verifyingContract` is the contract address that will verify this signature. We do not want the same signature to apply to multiple contracts, in case there are several `Greeter` contracts and we want them to have different greetings. + +```js + + const types = { + GreetingRequest: [ + { name: "greeting", type: "string" }, + ], + } +``` + +The data type we sign. Here, we have a single parameter, `greeting`, but real-life systems typically have more. + +```js + const message = { greeting } +``` + +The actual message we want to sign and send. `greeting` is both the field name and the name of the variable that fills it. + +```js + const signature = await signTypedDataAsync({ + domain, + types, + primaryType: "GreetingRequest", + message, + }) +``` + +Actually get the signature. This function is asynchronous because users take a long time (from a computer's perspective) to sign data. + +```js + const r = `0x${signature.slice(2, 66)}` + const s = `0x${signature.slice(66, 130)}` + const v = parseInt(signature.slice(130, 132), 16) + + return { + req: { greeting }, + v, + r, + s, + } + }, +``` + +The function returns a single hexadecimal value. Here we divide it into fields. + +```js + [account, chainId, contractAddr, signTypedDataAsync], +) +``` + +If any of these variables change, create a new instance of the function. The `account` and `chainId` parameters can be changed by the user in the wallet. `contractAddr` is a function of the chain Id. `signTypedDataAsync` should not change, but we import it from [a hook](https://wagmi.sh/react/api/hooks/useSignTypedData), so we can't be sure, and it's best to add it here. + +Now that the new greeting is signed, we need to send it to the server. + +```js + const sponsoredGreeting = async () => { + try { +``` + +This function takes a signature and sends it to the server. + +```js + const signedMessage = await signGreeting(newGreeting) + const response = await fetch("/server/sponsor", { +``` + +Send to the path `/server/sponsor` in the server we came from. + +```js + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signedMessage), + }) +``` + +Use `POST` to send the information JSON-encoded. + +```js + const data = await response.json() + console.log("Server response:", data) + } catch (err) { + console.error("Error:", err) + } + } +``` + +Output the response. On a production system we'd also show the response to the user. + +### The server {#server} + +I like using [Vite](https://vite.dev/) as my front-end. It automatically serves the React libraries and updates the browser when the front-end code changes. However, Vite does not include backend tooling. + +The solution is in [`index.js`](https://github.com/qbzzt/260301-gasless/blob/main/server/index.js). + +```js + app.post("/server/sponsor", async (req, res) => { + ... + }) + + // Let Vite handle everything else + const vite = await createViteServer({ + server: { middlewareMode: true } + }) + + app.use(vite.middlewares) +``` + +First we register a handler for the requests we handle ourselves (`POST` to `/server/sponsor`). Then we create and use a Vite server to handle all other URLs. + +```js + app.post("/server/sponsor", async (req, res) => { + try { + const signed = req.body + + const txHash = await sepoliaClient.writeContract({ + address: greeterAddr, + abi: greeterABI, + functionName: 'sponsoredSetGreeting', + args: [signed.req, signed.v, signed.r, signed.s], + }) + } ... + }) +``` + +This is just a standard [viem](https://viem.sh/) blockchain call. + +### The smart contract {#smart-contract} + +Finally, [`Greeter.sol`](https://github.com/qbzzt/260301-gasless/blob/main/contracts/src/Greeter.sol) needs to verify the signature. + +```solidity + constructor(string memory _greeting) { + greeting = _greeting; + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("Greeter")), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } +``` + +The constructor creates the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator), similar to the user interface code above. Blockchain execution is much more expensive, so we only calculate it once. + +```solidity + struct GreetingRequest { + string greeting; + } +``` + +This is the structure that gets signed. Here we have just one field. + +```solidity + bytes32 private constant GREETING_TYPEHASH = + keccak256("GreetingRequest(string greeting)"); +``` + +This is the [structure identifier](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct). It is calculated each time in the user interface. + +```solidity + function sponsoredSetGreeting( + GreetingRequest calldata req, + uint8 v, + bytes32 r, + bytes32 s + ) external { +``` + +This function receives a signed request and updates the greeting. + +```solidity + // Compute EIP-712 digest + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + GREETING_TYPEHASH, + keccak256(bytes(req.greeting)) + ) + ) + ) + ); +``` + +Create the digest in accordance with [EIP 712](https://eips.ethereum.org/EIPS/eip-712). + +```solidity + // Recover signer + address signer = ecrecover(digest, v, r, s); + require(signer != address(0), "Invalid signature"); +``` + +Use [`ecrecover`](https://www.evm.codes/precompiled?fork=osaka#0x01) to get the signer address. Note that a bad signature can still result in a valid address, just a random one. + +```solidity + // Apply greeting as if signer called it + greeting = req.greeting; + emit SetGreeting(signer, req.greeting); + } +``` + +Update the greeting. + +## Vulnerabilities {#vulnerabilities} + +This is _not_ production-level code. Ini rentan terhadap serangan signifikan dan tidak memiliki fitur utama. Here are some, along with how to solve them. + +To see some of these attacks, click the buttons under the _Attacks_ heading and see what happens. For the **Invalid signature** button, check the server console to see the transaction response. + +### Denial of service on the server {#dos-on-server} + +The easiest attack is a [denial-of-service](https://en.wikipedia.org/wiki/Denial-of-service_attack) attack on the server. The server receives requests from anywhere on the Internet and based on those requests sends transactions. There is absolutely nothing preventing an attacker from issuing a bunch of signatures, valid or invalid. Each will cause a transaction. Eventually the server will run out of ETH to pay for gas. + +One solution to this problem is to limit the rate to one transaction per block. If the purpose is to show greetings to [externally owned accounts](/developers/docs/accounts/#key-differences), it does not matter what the greeting is in the middle of the block anyway. + +Another solution is to keep track of addresses and only allow signatures from valid customers. + +### Wrong greeting signatures {#wrong-greeting-sigs} + +When you click **Signature for wrong greeting**, you submit a valid signature for a specific address (`0xaA92c5d426430D4769c9E878C1333BDe3d689b3e`) and greeting (`Hello`). But it submits it with a different greeting. This confuses `ecrecover`, which changes the greeting but has the wrong address. + +To solve this problem, add the address to the [signed structure](https://github.com/qbzzt/260301-gasless/blob/main/server/src/Greeter.jsx#L122-L124). This way, the `ecrecover` random address won't match the address in the signature, and the smart contract will reject the message. + +### Replay attacks {#replay-attack} + +When you click **Replay attack**, you submit the same "I'm 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, and I'd like the greeting to be `Hello`" signature, but with the correct greeting. As a result, the smart contract believes that the address (which isn't yours) changed the greeting back to `Hello`. The information to do this is publicly available in the [transaction information](https://eth-sepolia.blockscout.com/tx/0xa66afe4bbf886f59533e677a798c802ceab1ac0f9db6e83a4d4b59a45cf7c1b1). + +If this is a problem, one solution is to add a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). Have a [mapping](https://docs.soliditylang.org/en/latest/types.html#mapping-types) between addresses and numbers, and add a nonce field to the signature. If the nonce field matches the mapping for the address, accept the signature and increment the mapping for next time. If it doesn't, reject the transaction. + +Another solution is to add a timestamp to the signed data and accept the signature as valid only for a few seconds after that timestamp. This is simpler and cheaper, but we risk replay attacks within the time window, and the failure of legitimate transactions if the time window is exceeded. + +## Other missing features {#other-missing-features} + +There are additional features we would add in a production setting. + +### Access from other servers {#other-servers} + +Currently, we allow any address to submit a `sponsorSetGreeting`. This may be exactly what we want, in the interest of decentralization. Or maybe we want to ensure that sponsored transactions go through _our_ server, in which case we'd check `msg.sender` in the smart contract. + +Either way, this should be a conscious design decision, not just the result of not thinking about the issue. + +### Error handling {#error-handling} + +A user submits a greeting. Maybe it gets updated at the next block. Maybe it doesn't. Errors are invisible. On a production system, the user should be able to distinguish between these cases: + +- The new greeting has not been submitted yet +- The new greeting has been submitted, and it's in process +- The new greeting has been rejected + +## Kesimpulan {#conclusion} + +At this point, you should be able to create a gasless experience for your dapp users, at the cost of some centralization. + +However, this only works with smart contracts that support ERC-712. To transfer an ERC-20 token, for example, it is necessary to have the transaction signed by the owner rather than just a message. The solution is [account abstraction (ERC-4337)](https://docs.erc4337.io/index.html). I hope to write a future tutorial about it. + +[Lihat di sini untuk lebih banyak pekerjaan saya](https://cryptodocguy.pro/). diff --git a/public/content/translations/it/developers/tutorials/gasless/index.md b/public/content/translations/it/developers/tutorials/gasless/index.md new file mode 100644 index 00000000000..4fbce48e531 --- /dev/null +++ b/public/content/translations/it/developers/tutorials/gasless/index.md @@ -0,0 +1,362 @@ +--- +title: "Sponsoring gas fees: How to cover transaction costs for your users" +description: It is easy to create a private key and an address; it's just a matter of running the right software. But there are many places in the world where getting the ETH to send transactions is much harder. In this tutorial you learn how to cover the onchain gas costs for executing user-signed, offchain structured data in your smart contract. You have the user sign a structure containing the transaction information, which your offchain code then submits to the blockchain as a transaction. +author: Ori Pomerantz +tags: [ "gasless", "Solidity", "eip-712", "meta-transactions" ] +skill: intermediate +lang: it +published: 2026-02-27 +--- + +## Introduzione {#introduction} + +If we want Ethereum to serve [a billion more people](https://blog.ethereum.org/category/next-billion), we need to remove friction and make it as easy to use as possible. One source of this friction is the need for ETH to pay gas fees. + +If you have a dapp that makes money from users, it might make sense to let users submit transactions through your server and pay the transaction fees yourself. Because users still sign an [EIP-712 authorization message](https://eips.ethereum.org/EIPS/eip-712) in their wallets, they retain Ethereum's guarantees of integrity. Availability depends on the server that relays transactions, so it is more limited. However, you can set things up so users can also access the smart contract directly (if they get ETH), and let others set up their own servers if they want to sponsor transactions. + +The technique in this tutorial only works when you control the smart contract. There are other techniques, including [account abstraction](https://eips.ethereum.org/EIPS/eip-4337) that let you sponsor transactions to other smart contracts, which I hope to cover in a future tutorial. + +Note: This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Learn more in the [vulnerabilities section of this guide](#vulnerabilities). + +### Prerequisiti {#prerequisites} + +To understand this tutorial you need to already be familiar with: + +- Solidity +- JavaScript +- React and WAGMI. If you are not familiar with these user interface tools, [we have a tutorial for that](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +## The sample application {#sample-app} + +The sample application here is a variant on Hardhat's `Greeter` contract. You can see it [on GitHub](https://github.com/qbzzt/260301-gasless). The smart contract is already deployed on the [Sepolia](https://sepolia.dev/), at address [`0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA`](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA). + +To see it in action, follow these steps. + +1. Clone the repository and install the necessary software. + + ```sh + git clone https://github.com/qbzzt/260301-gasless.git + cd 260301-gasless/server + npm install + ``` + +2. Edit `.env` to set `PRIVATE_KEY` to a wallet that has ETH on Sepolia. If you need Sepolia ETH, [use a faucet](/developers/docs/networks/#sepolia). Ideally, this private key should be different from the one you have in your browser wallet. + +3. Avvia il server. + + ```sh + npm run dev + ``` + +4. Browse to the application at URL [`http://localhost:5173`](http://localhost:5173). + +5. Click **Connect with Injected** to connect to a wallet. Approve in the wallet, and approve the change to Sepolia if necessary. + +6. Write a new greeting and click **Update greeting via sponsor**. + +7. Sign the message. + +8. Wait about 12 seconds (the block time on Sepolia). While waiting you can look at the URL in the server's console to see the transaction. + +9. See that the greeting changed, and that the last updated by address value is now the address of your browser wallet. + +To understand how this works, we need to look at how the message gets created in the user interface, how it is relayed by the server, and how the smart contract processes it. + +### The user interface {#ui-changes} + +The user interface is based on [WAGMI](https://wagmi.sh/); you can read about it [in this tutorial](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +Here is how we sign the message: + +```js +const signGreeting = useCallback( +``` + +The React hook [`useCallback`](https://react.dev/reference/react/useCallback) lets us improve performance by reusing the same function when the component is redrawn. + +```js + async (greeting) => { + if (!account) throw new Error("Wallet not connected") +``` + +If there is no account, raise an error. This should never happen because the UI button that starts the process that calls `signGreeting` is disabled in that case. However, future programmers may remove that safeguard, so it's a good idea to check this condition here as well. + +```js + const domain = { + name: "Greeter", + version: "1", + chainId, + verifyingContract: contractAddr, + } +``` + +Parameters for the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator). This value is constant, so in a better-optimized implementation, we might calculate it once rather than recalculate it each time the function is called. + +- `name` is a user-readable name, such as the name of the dapp for which we are producing signatures. +- `version` is the version. Different versions are not compatible. +- `chainId` is the chain we are using, as provided [by WAGMI](https://wagmi.sh/react/api/hooks/useChainId). +- `verifyingContract` is the contract address that will verify this signature. We do not want the same signature to apply to multiple contracts, in case there are several `Greeter` contracts and we want them to have different greetings. + +```js + + const types = { + GreetingRequest: [ + { name: "greeting", type: "string" }, + ], + } +``` + +The data type we sign. Here, we have a single parameter, `greeting`, but real-life systems typically have more. + +```js + const message = { greeting } +``` + +The actual message we want to sign and send. `greeting` is both the field name and the name of the variable that fills it. + +```js + const signature = await signTypedDataAsync({ + domain, + types, + primaryType: "GreetingRequest", + message, + }) +``` + +Actually get the signature. This function is asynchronous because users take a long time (from a computer's perspective) to sign data. + +```js + const r = `0x${signature.slice(2, 66)}` + const s = `0x${signature.slice(66, 130)}` + const v = parseInt(signature.slice(130, 132), 16) + + return { + req: { greeting }, + v, + r, + s, + } + }, +``` + +The function returns a single hexadecimal value. Here we divide it into fields. + +```js + [account, chainId, contractAddr, signTypedDataAsync], +) +``` + +If any of these variables change, create a new instance of the function. The `account` and `chainId` parameters can be changed by the user in the wallet. `contractAddr` is a function of the chain Id. `signTypedDataAsync` should not change, but we import it from [a hook](https://wagmi.sh/react/api/hooks/useSignTypedData), so we can't be sure, and it's best to add it here. + +Now that the new greeting is signed, we need to send it to the server. + +```js + const sponsoredGreeting = async () => { + try { +``` + +This function takes a signature and sends it to the server. + +```js + const signedMessage = await signGreeting(newGreeting) + const response = await fetch("/server/sponsor", { +``` + +Send to the path `/server/sponsor` in the server we came from. + +```js + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signedMessage), + }) +``` + +Use `POST` to send the information JSON-encoded. + +```js + const data = await response.json() + console.log("Server response:", data) + } catch (err) { + console.error("Error:", err) + } + } +``` + +Output the response. On a production system we'd also show the response to the user. + +### The server {#server} + +I like using [Vite](https://vite.dev/) as my front-end. It automatically serves the React libraries and updates the browser when the front-end code changes. However, Vite does not include backend tooling. + +The solution is in [`index.js`](https://github.com/qbzzt/260301-gasless/blob/main/server/index.js). + +```js + app.post("/server/sponsor", async (req, res) => { + ... + }) + + // Let Vite handle everything else + const vite = await createViteServer({ + server: { middlewareMode: true } + }) + + app.use(vite.middlewares) +``` + +First we register a handler for the requests we handle ourselves (`POST` to `/server/sponsor`). Then we create and use a Vite server to handle all other URLs. + +```js + app.post("/server/sponsor", async (req, res) => { + try { + const signed = req.body + + const txHash = await sepoliaClient.writeContract({ + address: greeterAddr, + abi: greeterABI, + functionName: 'sponsoredSetGreeting', + args: [signed.req, signed.v, signed.r, signed.s], + }) + } ... + }) +``` + +This is just a standard [viem](https://viem.sh/) blockchain call. + +### The smart contract {#smart-contract} + +Finally, [`Greeter.sol`](https://github.com/qbzzt/260301-gasless/blob/main/contracts/src/Greeter.sol) needs to verify the signature. + +```solidity + constructor(string memory _greeting) { + greeting = _greeting; + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("Greeter")), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } +``` + +The constructor creates the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator), similar to the user interface code above. Blockchain execution is much more expensive, so we only calculate it once. + +```solidity + struct GreetingRequest { + string greeting; + } +``` + +This is the structure that gets signed. Here we have just one field. + +```solidity + bytes32 private constant GREETING_TYPEHASH = + keccak256("GreetingRequest(string greeting)"); +``` + +This is the [structure identifier](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct). It is calculated each time in the user interface. + +```solidity + function sponsoredSetGreeting( + GreetingRequest calldata req, + uint8 v, + bytes32 r, + bytes32 s + ) external { +``` + +This function receives a signed request and updates the greeting. + +```solidity + // Compute EIP-712 digest + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + GREETING_TYPEHASH, + keccak256(bytes(req.greeting)) + ) + ) + ) + ); +``` + +Create the digest in accordance with [EIP 712](https://eips.ethereum.org/EIPS/eip-712). + +```solidity + // Recover signer + address signer = ecrecover(digest, v, r, s); + require(signer != address(0), "Invalid signature"); +``` + +Use [`ecrecover`](https://www.evm.codes/precompiled?fork=osaka#0x01) to get the signer address. Note that a bad signature can still result in a valid address, just a random one. + +```solidity + // Apply greeting as if signer called it + greeting = req.greeting; + emit SetGreeting(signer, req.greeting); + } +``` + +Update the greeting. + +## Vulnerabilities {#vulnerabilities} + +This is _not_ production-level code. È vulnerabile a importanti attacchi e manca di funzionalità principali. Here are some, along with how to solve them. + +To see some of these attacks, click the buttons under the _Attacks_ heading and see what happens. For the **Invalid signature** button, check the server console to see the transaction response. + +### Denial of service on the server {#dos-on-server} + +The easiest attack is a [denial-of-service](https://en.wikipedia.org/wiki/Denial-of-service_attack) attack on the server. The server receives requests from anywhere on the Internet and based on those requests sends transactions. There is absolutely nothing preventing an attacker from issuing a bunch of signatures, valid or invalid. Each will cause a transaction. Eventually the server will run out of ETH to pay for gas. + +One solution to this problem is to limit the rate to one transaction per block. If the purpose is to show greetings to [externally owned accounts](/developers/docs/accounts/#key-differences), it does not matter what the greeting is in the middle of the block anyway. + +Another solution is to keep track of addresses and only allow signatures from valid customers. + +### Wrong greeting signatures {#wrong-greeting-sigs} + +When you click **Signature for wrong greeting**, you submit a valid signature for a specific address (`0xaA92c5d426430D4769c9E878C1333BDe3d689b3e`) and greeting (`Hello`). But it submits it with a different greeting. This confuses `ecrecover`, which changes the greeting but has the wrong address. + +To solve this problem, add the address to the [signed structure](https://github.com/qbzzt/260301-gasless/blob/main/server/src/Greeter.jsx#L122-L124). This way, the `ecrecover` random address won't match the address in the signature, and the smart contract will reject the message. + +### Replay attacks {#replay-attack} + +When you click **Replay attack**, you submit the same "I'm 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, and I'd like the greeting to be `Hello`" signature, but with the correct greeting. As a result, the smart contract believes that the address (which isn't yours) changed the greeting back to `Hello`. The information to do this is publicly available in the [transaction information](https://eth-sepolia.blockscout.com/tx/0xa66afe4bbf886f59533e677a798c802ceab1ac0f9db6e83a4d4b59a45cf7c1b1). + +If this is a problem, one solution is to add a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). Have a [mapping](https://docs.soliditylang.org/en/latest/types.html#mapping-types) between addresses and numbers, and add a nonce field to the signature. If the nonce field matches the mapping for the address, accept the signature and increment the mapping for next time. If it doesn't, reject the transaction. + +Another solution is to add a timestamp to the signed data and accept the signature as valid only for a few seconds after that timestamp. This is simpler and cheaper, but we risk replay attacks within the time window, and the failure of legitimate transactions if the time window is exceeded. + +## Other missing features {#other-missing-features} + +There are additional features we would add in a production setting. + +### Access from other servers {#other-servers} + +Currently, we allow any address to submit a `sponsorSetGreeting`. This may be exactly what we want, in the interest of decentralization. Or maybe we want to ensure that sponsored transactions go through _our_ server, in which case we'd check `msg.sender` in the smart contract. + +Either way, this should be a conscious design decision, not just the result of not thinking about the issue. + +### Error handling {#error-handling} + +A user submits a greeting. Maybe it gets updated at the next block. Maybe it doesn't. Errors are invisible. On a production system, the user should be able to distinguish between these cases: + +- The new greeting has not been submitted yet +- The new greeting has been submitted, and it's in process +- The new greeting has been rejected + +## Conclusione {#conclusion} + +At this point, you should be able to create a gasless experience for your dapp users, at the cost of some centralization. + +However, this only works with smart contracts that support ERC-712. To transfer an ERC-20 token, for example, it is necessary to have the transaction signed by the owner rather than just a message. The solution is [account abstraction (ERC-4337)](https://docs.erc4337.io/index.html). I hope to write a future tutorial about it. + +[Vedi qui per altri miei lavori](https://cryptodocguy.pro/). diff --git a/public/content/translations/ja/developers/tutorials/gasless/index.md b/public/content/translations/ja/developers/tutorials/gasless/index.md new file mode 100644 index 00000000000..e4157acf58a --- /dev/null +++ b/public/content/translations/ja/developers/tutorials/gasless/index.md @@ -0,0 +1,362 @@ +--- +title: "Sponsoring gas fees: How to cover transaction costs for your users" +description: It is easy to create a private key and an address; it's just a matter of running the right software. But there are many places in the world where getting the ETH to send transactions is much harder. In this tutorial you learn how to cover the onchain gas costs for executing user-signed, offchain structured data in your smart contract. You have the user sign a structure containing the transaction information, which your offchain code then submits to the blockchain as a transaction. +author: Ori Pomerantz +tags: [ "gasless", "Solidity", "eip-712", "meta-transactions" ] +skill: intermediate +lang: ja +published: 2026-02-27 +--- + +## はじめに {#introduction} + +If we want Ethereum to serve [a billion more people](https://blog.ethereum.org/category/next-billion), we need to remove friction and make it as easy to use as possible. One source of this friction is the need for ETH to pay gas fees. + +If you have a dapp that makes money from users, it might make sense to let users submit transactions through your server and pay the transaction fees yourself. Because users still sign an [EIP-712 authorization message](https://eips.ethereum.org/EIPS/eip-712) in their wallets, they retain Ethereum's guarantees of integrity. Availability depends on the server that relays transactions, so it is more limited. However, you can set things up so users can also access the smart contract directly (if they get ETH), and let others set up their own servers if they want to sponsor transactions. + +The technique in this tutorial only works when you control the smart contract. There are other techniques, including [account abstraction](https://eips.ethereum.org/EIPS/eip-4337) that let you sponsor transactions to other smart contracts, which I hope to cover in a future tutorial. + +Note: This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Learn more in the [vulnerabilities section of this guide](#vulnerabilities). + +### 前提条件{#prerequisites} + +To understand this tutorial you need to already be familiar with: + +- Solidity +- JavaScript +- React and WAGMI. If you are not familiar with these user interface tools, [we have a tutorial for that](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +## The sample application {#sample-app} + +The sample application here is a variant on Hardhat's `Greeter` contract. You can see it [on GitHub](https://github.com/qbzzt/260301-gasless). The smart contract is already deployed on the [Sepolia](https://sepolia.dev/), at address [`0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA`](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA). + +To see it in action, follow these steps. + +1. Clone the repository and install the necessary software. + + ```sh + git clone https://github.com/qbzzt/260301-gasless.git + cd 260301-gasless/server + npm install + ``` + +2. Edit `.env` to set `PRIVATE_KEY` to a wallet that has ETH on Sepolia. If you need Sepolia ETH, [use a faucet](/developers/docs/networks/#sepolia). Ideally, this private key should be different from the one you have in your browser wallet. + +3. サーバーを起動します。 + + ```sh + npm run dev + ``` + +4. Browse to the application at URL [`http://localhost:5173`](http://localhost:5173). + +5. Click **Connect with Injected** to connect to a wallet. Approve in the wallet, and approve the change to Sepolia if necessary. + +6. Write a new greeting and click **Update greeting via sponsor**. + +7. Sign the message. + +8. Wait about 12 seconds (the block time on Sepolia). While waiting you can look at the URL in the server's console to see the transaction. + +9. See that the greeting changed, and that the last updated by address value is now the address of your browser wallet. + +To understand how this works, we need to look at how the message gets created in the user interface, how it is relayed by the server, and how the smart contract processes it. + +### The user interface {#ui-changes} + +The user interface is based on [WAGMI](https://wagmi.sh/); you can read about it [in this tutorial](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +Here is how we sign the message: + +```js +const signGreeting = useCallback( +``` + +The React hook [`useCallback`](https://react.dev/reference/react/useCallback) lets us improve performance by reusing the same function when the component is redrawn. + +```js + async (greeting) => { + if (!account) throw new Error("Wallet not connected") +``` + +If there is no account, raise an error. This should never happen because the UI button that starts the process that calls `signGreeting` is disabled in that case. However, future programmers may remove that safeguard, so it's a good idea to check this condition here as well. + +```js + const domain = { + name: "Greeter", + version: "1", + chainId, + verifyingContract: contractAddr, + } +``` + +Parameters for the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator). This value is constant, so in a better-optimized implementation, we might calculate it once rather than recalculate it each time the function is called. + +- `name` is a user-readable name, such as the name of the dapp for which we are producing signatures. +- `version` is the version. Different versions are not compatible. +- `chainId` is the chain we are using, as provided [by WAGMI](https://wagmi.sh/react/api/hooks/useChainId). +- `verifyingContract` is the contract address that will verify this signature. We do not want the same signature to apply to multiple contracts, in case there are several `Greeter` contracts and we want them to have different greetings. + +```js + + const types = { + GreetingRequest: [ + { name: "greeting", type: "string" }, + ], + } +``` + +The data type we sign. Here, we have a single parameter, `greeting`, but real-life systems typically have more. + +```js + const message = { greeting } +``` + +The actual message we want to sign and send. `greeting` is both the field name and the name of the variable that fills it. + +```js + const signature = await signTypedDataAsync({ + domain, + types, + primaryType: "GreetingRequest", + message, + }) +``` + +Actually get the signature. This function is asynchronous because users take a long time (from a computer's perspective) to sign data. + +```js + const r = `0x${signature.slice(2, 66)}` + const s = `0x${signature.slice(66, 130)}` + const v = parseInt(signature.slice(130, 132), 16) + + return { + req: { greeting }, + v, + r, + s, + } + }, +``` + +The function returns a single hexadecimal value. Here we divide it into fields. + +```js + [account, chainId, contractAddr, signTypedDataAsync], +) +``` + +If any of these variables change, create a new instance of the function. The `account` and `chainId` parameters can be changed by the user in the wallet. `contractAddr` is a function of the chain Id. `signTypedDataAsync` should not change, but we import it from [a hook](https://wagmi.sh/react/api/hooks/useSignTypedData), so we can't be sure, and it's best to add it here. + +Now that the new greeting is signed, we need to send it to the server. + +```js + const sponsoredGreeting = async () => { + try { +``` + +This function takes a signature and sends it to the server. + +```js + const signedMessage = await signGreeting(newGreeting) + const response = await fetch("/server/sponsor", { +``` + +Send to the path `/server/sponsor` in the server we came from. + +```js + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signedMessage), + }) +``` + +Use `POST` to send the information JSON-encoded. + +```js + const data = await response.json() + console.log("Server response:", data) + } catch (err) { + console.error("Error:", err) + } + } +``` + +Output the response. On a production system we'd also show the response to the user. + +### The server {#server} + +I like using [Vite](https://vite.dev/) as my front-end. It automatically serves the React libraries and updates the browser when the front-end code changes. However, Vite does not include backend tooling. + +The solution is in [`index.js`](https://github.com/qbzzt/260301-gasless/blob/main/server/index.js). + +```js + app.post("/server/sponsor", async (req, res) => { + ... + }) + + // Let Vite handle everything else + const vite = await createViteServer({ + server: { middlewareMode: true } + }) + + app.use(vite.middlewares) +``` + +First we register a handler for the requests we handle ourselves (`POST` to `/server/sponsor`). Then we create and use a Vite server to handle all other URLs. + +```js + app.post("/server/sponsor", async (req, res) => { + try { + const signed = req.body + + const txHash = await sepoliaClient.writeContract({ + address: greeterAddr, + abi: greeterABI, + functionName: 'sponsoredSetGreeting', + args: [signed.req, signed.v, signed.r, signed.s], + }) + } ... + }) +``` + +This is just a standard [viem](https://viem.sh/) blockchain call. + +### The smart contract {#smart-contract} + +Finally, [`Greeter.sol`](https://github.com/qbzzt/260301-gasless/blob/main/contracts/src/Greeter.sol) needs to verify the signature. + +```solidity + constructor(string memory _greeting) { + greeting = _greeting; + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("Greeter")), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } +``` + +The constructor creates the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator), similar to the user interface code above. Blockchain execution is much more expensive, so we only calculate it once. + +```solidity + struct GreetingRequest { + string greeting; + } +``` + +This is the structure that gets signed. Here we have just one field. + +```solidity + bytes32 private constant GREETING_TYPEHASH = + keccak256("GreetingRequest(string greeting)"); +``` + +This is the [structure identifier](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct). It is calculated each time in the user interface. + +```solidity + function sponsoredSetGreeting( + GreetingRequest calldata req, + uint8 v, + bytes32 r, + bytes32 s + ) external { +``` + +This function receives a signed request and updates the greeting. + +```solidity + // Compute EIP-712 digest + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + GREETING_TYPEHASH, + keccak256(bytes(req.greeting)) + ) + ) + ) + ); +``` + +Create the digest in accordance with [EIP 712](https://eips.ethereum.org/EIPS/eip-712). + +```solidity + // Recover signer + address signer = ecrecover(digest, v, r, s); + require(signer != address(0), "Invalid signature"); +``` + +Use [`ecrecover`](https://www.evm.codes/precompiled?fork=osaka#0x01) to get the signer address. Note that a bad signature can still result in a valid address, just a random one. + +```solidity + // Apply greeting as if signer called it + greeting = req.greeting; + emit SetGreeting(signer, req.greeting); + } +``` + +Update the greeting. + +## Vulnerabilities {#vulnerabilities} + +This is _not_ production-level code. 重大な攻撃に対して脆弱であり、主要な機能が不足しています。 Here are some, along with how to solve them. + +To see some of these attacks, click the buttons under the _Attacks_ heading and see what happens. For the **Invalid signature** button, check the server console to see the transaction response. + +### Denial of service on the server {#dos-on-server} + +The easiest attack is a [denial-of-service](https://en.wikipedia.org/wiki/Denial-of-service_attack) attack on the server. The server receives requests from anywhere on the Internet and based on those requests sends transactions. There is absolutely nothing preventing an attacker from issuing a bunch of signatures, valid or invalid. Each will cause a transaction. Eventually the server will run out of ETH to pay for gas. + +One solution to this problem is to limit the rate to one transaction per block. If the purpose is to show greetings to [externally owned accounts](/developers/docs/accounts/#key-differences), it does not matter what the greeting is in the middle of the block anyway. + +Another solution is to keep track of addresses and only allow signatures from valid customers. + +### Wrong greeting signatures {#wrong-greeting-sigs} + +When you click **Signature for wrong greeting**, you submit a valid signature for a specific address (`0xaA92c5d426430D4769c9E878C1333BDe3d689b3e`) and greeting (`Hello`). But it submits it with a different greeting. This confuses `ecrecover`, which changes the greeting but has the wrong address. + +To solve this problem, add the address to the [signed structure](https://github.com/qbzzt/260301-gasless/blob/main/server/src/Greeter.jsx#L122-L124). This way, the `ecrecover` random address won't match the address in the signature, and the smart contract will reject the message. + +### Replay attacks {#replay-attack} + +When you click **Replay attack**, you submit the same "I'm 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, and I'd like the greeting to be `Hello`" signature, but with the correct greeting. As a result, the smart contract believes that the address (which isn't yours) changed the greeting back to `Hello`. The information to do this is publicly available in the [transaction information](https://eth-sepolia.blockscout.com/tx/0xa66afe4bbf886f59533e677a798c802ceab1ac0f9db6e83a4d4b59a45cf7c1b1). + +If this is a problem, one solution is to add a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). Have a [mapping](https://docs.soliditylang.org/en/latest/types.html#mapping-types) between addresses and numbers, and add a nonce field to the signature. If the nonce field matches the mapping for the address, accept the signature and increment the mapping for next time. If it doesn't, reject the transaction. + +Another solution is to add a timestamp to the signed data and accept the signature as valid only for a few seconds after that timestamp. This is simpler and cheaper, but we risk replay attacks within the time window, and the failure of legitimate transactions if the time window is exceeded. + +## Other missing features {#other-missing-features} + +There are additional features we would add in a production setting. + +### Access from other servers {#other-servers} + +Currently, we allow any address to submit a `sponsorSetGreeting`. This may be exactly what we want, in the interest of decentralization. Or maybe we want to ensure that sponsored transactions go through _our_ server, in which case we'd check `msg.sender` in the smart contract. + +Either way, this should be a conscious design decision, not just the result of not thinking about the issue. + +### Error handling {#error-handling} + +A user submits a greeting. Maybe it gets updated at the next block. Maybe it doesn't. Errors are invisible. On a production system, the user should be able to distinguish between these cases: + +- The new greeting has not been submitted yet +- The new greeting has been submitted, and it's in process +- The new greeting has been rejected + +## 結論 {#conclusion} + +At this point, you should be able to create a gasless experience for your dapp users, at the cost of some centralization. + +However, this only works with smart contracts that support ERC-712. To transfer an ERC-20 token, for example, it is necessary to have the transaction signed by the owner rather than just a message. The solution is [account abstraction (ERC-4337)](https://docs.erc4337.io/index.html). I hope to write a future tutorial about it. + +[私の他の作品はこちらでご覧いただけます](https://cryptodocguy.pro/). diff --git a/public/content/translations/ko/developers/tutorials/gasless/index.md b/public/content/translations/ko/developers/tutorials/gasless/index.md new file mode 100644 index 00000000000..0348efeb3c8 --- /dev/null +++ b/public/content/translations/ko/developers/tutorials/gasless/index.md @@ -0,0 +1,362 @@ +--- +title: "Sponsoring gas fees: How to cover transaction costs for your users" +description: It is easy to create a private key and an address; it's just a matter of running the right software. But there are many places in the world where getting the ETH to send transactions is much harder. In this tutorial you learn how to cover the onchain gas costs for executing user-signed, offchain structured data in your smart contract. You have the user sign a structure containing the transaction information, which your offchain code then submits to the blockchain as a transaction. +author: Ori Pomerantz +tags: [ "gasless", "솔리디티", "eip-712", "meta-transactions" ] +skill: intermediate +lang: ko +published: 2026-02-27 +--- + +## 소개 {#introduction} + +If we want Ethereum to serve [a billion more people](https://blog.ethereum.org/category/next-billion), we need to remove friction and make it as easy to use as possible. One source of this friction is the need for ETH to pay gas fees. + +If you have a dapp that makes money from users, it might make sense to let users submit transactions through your server and pay the transaction fees yourself. Because users still sign an [EIP-712 authorization message](https://eips.ethereum.org/EIPS/eip-712) in their wallets, they retain Ethereum's guarantees of integrity. Availability depends on the server that relays transactions, so it is more limited. However, you can set things up so users can also access the smart contract directly (if they get ETH), and let others set up their own servers if they want to sponsor transactions. + +The technique in this tutorial only works when you control the smart contract. There are other techniques, including [account abstraction](https://eips.ethereum.org/EIPS/eip-4337) that let you sponsor transactions to other smart contracts, which I hope to cover in a future tutorial. + +Note: This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Learn more in the [vulnerabilities section of this guide](#vulnerabilities). + +### 필수 구성 요소 {#prerequisites} + +To understand this tutorial you need to already be familiar with: + +- 솔리디티 +- 자바스크립트 +- React and WAGMI. If you are not familiar with these user interface tools, [we have a tutorial for that](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +## The sample application {#sample-app} + +The sample application here is a variant on Hardhat's `Greeter` contract. You can see it [on GitHub](https://github.com/qbzzt/260301-gasless). The smart contract is already deployed on the [Sepolia](https://sepolia.dev/), at address [`0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA`](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA). + +To see it in action, follow these steps. + +1. Clone the repository and install the necessary software. + + ```sh + git clone https://github.com/qbzzt/260301-gasless.git + cd 260301-gasless/server + npm install + ``` + +2. Edit `.env` to set `PRIVATE_KEY` to a wallet that has ETH on Sepolia. If you need Sepolia ETH, [use a faucet](/developers/docs/networks/#sepolia). Ideally, this private key should be different from the one you have in your browser wallet. + +3. 서버를 시작하세요. + + ```sh + npm run dev + ``` + +4. Browse to the application at URL [`http://localhost:5173`](http://localhost:5173). + +5. Click **Connect with Injected** to connect to a wallet. Approve in the wallet, and approve the change to Sepolia if necessary. + +6. Write a new greeting and click **Update greeting via sponsor**. + +7. Sign the message. + +8. Wait about 12 seconds (the block time on Sepolia). While waiting you can look at the URL in the server's console to see the transaction. + +9. See that the greeting changed, and that the last updated by address value is now the address of your browser wallet. + +To understand how this works, we need to look at how the message gets created in the user interface, how it is relayed by the server, and how the smart contract processes it. + +### The user interface {#ui-changes} + +The user interface is based on [WAGMI](https://wagmi.sh/); you can read about it [in this tutorial](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +Here is how we sign the message: + +```js +const signGreeting = useCallback( +``` + +The React hook [`useCallback`](https://react.dev/reference/react/useCallback) lets us improve performance by reusing the same function when the component is redrawn. + +```js + async (greeting) => { + if (!account) throw new Error("Wallet not connected") +``` + +If there is no account, raise an error. This should never happen because the UI button that starts the process that calls `signGreeting` is disabled in that case. However, future programmers may remove that safeguard, so it's a good idea to check this condition here as well. + +```js + const domain = { + name: "Greeter", + version: "1", + chainId, + verifyingContract: contractAddr, + } +``` + +Parameters for the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator). This value is constant, so in a better-optimized implementation, we might calculate it once rather than recalculate it each time the function is called. + +- `name` is a user-readable name, such as the name of the dapp for which we are producing signatures. +- `version` is the version. Different versions are not compatible. +- `chainId` is the chain we are using, as provided [by WAGMI](https://wagmi.sh/react/api/hooks/useChainId). +- `verifyingContract` is the contract address that will verify this signature. We do not want the same signature to apply to multiple contracts, in case there are several `Greeter` contracts and we want them to have different greetings. + +```js + + const types = { + GreetingRequest: [ + { name: "greeting", type: "string" }, + ], + } +``` + +The data type we sign. Here, we have a single parameter, `greeting`, but real-life systems typically have more. + +```js + const message = { greeting } +``` + +The actual message we want to sign and send. `greeting` is both the field name and the name of the variable that fills it. + +```js + const signature = await signTypedDataAsync({ + domain, + types, + primaryType: "GreetingRequest", + message, + }) +``` + +Actually get the signature. This function is asynchronous because users take a long time (from a computer's perspective) to sign data. + +```js + const r = `0x${signature.slice(2, 66)}` + const s = `0x${signature.slice(66, 130)}` + const v = parseInt(signature.slice(130, 132), 16) + + return { + req: { greeting }, + v, + r, + s, + } + }, +``` + +The function returns a single hexadecimal value. Here we divide it into fields. + +```js + [account, chainId, contractAddr, signTypedDataAsync], +) +``` + +If any of these variables change, create a new instance of the function. The `account` and `chainId` parameters can be changed by the user in the wallet. `contractAddr` is a function of the chain Id. `signTypedDataAsync` should not change, but we import it from [a hook](https://wagmi.sh/react/api/hooks/useSignTypedData), so we can't be sure, and it's best to add it here. + +Now that the new greeting is signed, we need to send it to the server. + +```js + const sponsoredGreeting = async () => { + try { +``` + +This function takes a signature and sends it to the server. + +```js + const signedMessage = await signGreeting(newGreeting) + const response = await fetch("/server/sponsor", { +``` + +Send to the path `/server/sponsor` in the server we came from. + +```js + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signedMessage), + }) +``` + +Use `POST` to send the information JSON-encoded. + +```js + const data = await response.json() + console.log("Server response:", data) + } catch (err) { + console.error("Error:", err) + } + } +``` + +Output the response. On a production system we'd also show the response to the user. + +### The server {#server} + +I like using [Vite](https://vite.dev/) as my front-end. It automatically serves the React libraries and updates the browser when the front-end code changes. However, Vite does not include backend tooling. + +The solution is in [`index.js`](https://github.com/qbzzt/260301-gasless/blob/main/server/index.js). + +```js + app.post("/server/sponsor", async (req, res) => { + ... + }) + + // Let Vite handle everything else + const vite = await createViteServer({ + server: { middlewareMode: true } + }) + + app.use(vite.middlewares) +``` + +First we register a handler for the requests we handle ourselves (`POST` to `/server/sponsor`). Then we create and use a Vite server to handle all other URLs. + +```js + app.post("/server/sponsor", async (req, res) => { + try { + const signed = req.body + + const txHash = await sepoliaClient.writeContract({ + address: greeterAddr, + abi: greeterABI, + functionName: 'sponsoredSetGreeting', + args: [signed.req, signed.v, signed.r, signed.s], + }) + } ... + }) +``` + +This is just a standard [viem](https://viem.sh/) blockchain call. + +### The smart contract {#smart-contract} + +Finally, [`Greeter.sol`](https://github.com/qbzzt/260301-gasless/blob/main/contracts/src/Greeter.sol) needs to verify the signature. + +```solidity + constructor(string memory _greeting) { + greeting = _greeting; + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("Greeter")), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } +``` + +The constructor creates the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator), similar to the user interface code above. Blockchain execution is much more expensive, so we only calculate it once. + +```solidity + struct GreetingRequest { + string greeting; + } +``` + +This is the structure that gets signed. Here we have just one field. + +```solidity + bytes32 private constant GREETING_TYPEHASH = + keccak256("GreetingRequest(string greeting)"); +``` + +This is the [structure identifier](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct). It is calculated each time in the user interface. + +```solidity + function sponsoredSetGreeting( + GreetingRequest calldata req, + uint8 v, + bytes32 r, + bytes32 s + ) external { +``` + +This function receives a signed request and updates the greeting. + +```solidity + // Compute EIP-712 digest + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + GREETING_TYPEHASH, + keccak256(bytes(req.greeting)) + ) + ) + ) + ); +``` + +Create the digest in accordance with [EIP 712](https://eips.ethereum.org/EIPS/eip-712). + +```solidity + // Recover signer + address signer = ecrecover(digest, v, r, s); + require(signer != address(0), "Invalid signature"); +``` + +Use [`ecrecover`](https://www.evm.codes/precompiled?fork=osaka#0x01) to get the signer address. Note that a bad signature can still result in a valid address, just a random one. + +```solidity + // Apply greeting as if signer called it + greeting = req.greeting; + emit SetGreeting(signer, req.greeting); + } +``` + +Update the greeting. + +## Vulnerabilities {#vulnerabilities} + +This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Here are some, along with how to solve them. + +To see some of these attacks, click the buttons under the _Attacks_ heading and see what happens. For the **Invalid signature** button, check the server console to see the transaction response. + +### Denial of service on the server {#dos-on-server} + +The easiest attack is a [denial-of-service](https://en.wikipedia.org/wiki/Denial-of-service_attack) attack on the server. The server receives requests from anywhere on the Internet and based on those requests sends transactions. There is absolutely nothing preventing an attacker from issuing a bunch of signatures, valid or invalid. Each will cause a transaction. Eventually the server will run out of ETH to pay for gas. + +One solution to this problem is to limit the rate to one transaction per block. If the purpose is to show greetings to [externally owned accounts](/developers/docs/accounts/#key-differences), it does not matter what the greeting is in the middle of the block anyway. + +Another solution is to keep track of addresses and only allow signatures from valid customers. + +### Wrong greeting signatures {#wrong-greeting-sigs} + +When you click **Signature for wrong greeting**, you submit a valid signature for a specific address (`0xaA92c5d426430D4769c9E878C1333BDe3d689b3e`) and greeting (`Hello`). But it submits it with a different greeting. This confuses `ecrecover`, which changes the greeting but has the wrong address. + +To solve this problem, add the address to the [signed structure](https://github.com/qbzzt/260301-gasless/blob/main/server/src/Greeter.jsx#L122-L124). This way, the `ecrecover` random address won't match the address in the signature, and the smart contract will reject the message. + +### Replay attacks {#replay-attack} + +When you click **Replay attack**, you submit the same "I'm 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, and I'd like the greeting to be `Hello`" signature, but with the correct greeting. As a result, the smart contract believes that the address (which isn't yours) changed the greeting back to `Hello`. The information to do this is publicly available in the [transaction information](https://eth-sepolia.blockscout.com/tx/0xa66afe4bbf886f59533e677a798c802ceab1ac0f9db6e83a4d4b59a45cf7c1b1). + +If this is a problem, one solution is to add a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). Have a [mapping](https://docs.soliditylang.org/en/latest/types.html#mapping-types) between addresses and numbers, and add a nonce field to the signature. If the nonce field matches the mapping for the address, accept the signature and increment the mapping for next time. If it doesn't, reject the transaction. + +Another solution is to add a timestamp to the signed data and accept the signature as valid only for a few seconds after that timestamp. This is simpler and cheaper, but we risk replay attacks within the time window, and the failure of legitimate transactions if the time window is exceeded. + +## Other missing features {#other-missing-features} + +There are additional features we would add in a production setting. + +### Access from other servers {#other-servers} + +Currently, we allow any address to submit a `sponsorSetGreeting`. This may be exactly what we want, in the interest of decentralization. Or maybe we want to ensure that sponsored transactions go through _our_ server, in which case we'd check `msg.sender` in the smart contract. + +Either way, this should be a conscious design decision, not just the result of not thinking about the issue. + +### Error handling {#error-handling} + +A user submits a greeting. Maybe it gets updated at the next block. Maybe it doesn't. Errors are invisible. On a production system, the user should be able to distinguish between these cases: + +- The new greeting has not been submitted yet +- The new greeting has been submitted, and it's in process +- The new greeting has been rejected + +## 결론 {#conclusion} + +At this point, you should be able to create a gasless experience for your dapp users, at the cost of some centralization. + +However, this only works with smart contracts that support ERC-712. To transfer an ERC-20 token, for example, it is necessary to have the transaction signed by the owner rather than just a message. The solution is [account abstraction (ERC-4337)](https://docs.erc4337.io/index.html). I hope to write a future tutorial about it. + +[여기서 제 작업에 대한 자세한 내용을 확인하세요](https://cryptodocguy.pro/). diff --git a/public/content/translations/mr/developers/tutorials/gasless/index.md b/public/content/translations/mr/developers/tutorials/gasless/index.md new file mode 100644 index 00000000000..c19e033a784 --- /dev/null +++ b/public/content/translations/mr/developers/tutorials/gasless/index.md @@ -0,0 +1,362 @@ +--- +title: "Sponsoring gas fees: How to cover transaction costs for your users" +description: It is easy to create a private key and an address; it's just a matter of running the right software. But there are many places in the world where getting the ETH to send transactions is much harder. In this tutorial you learn how to cover the onchain gas costs for executing user-signed, offchain structured data in your smart contract. You have the user sign a structure containing the transaction information, which your offchain code then submits to the blockchain as a transaction. +author: "ओरी पोमेरँट्झ" +tags: [ "gasless", "सॉलिडिटी", "eip-712", "meta-transactions" ] +skill: intermediate +lang: mr +published: 2026-02-27 +--- + +## प्रस्तावना {#introduction} + +If we want Ethereum to serve [a billion more people](https://blog.ethereum.org/category/next-billion), we need to remove friction and make it as easy to use as possible. One source of this friction is the need for ETH to pay gas fees. + +If you have a dapp that makes money from users, it might make sense to let users submit transactions through your server and pay the transaction fees yourself. Because users still sign an [EIP-712 authorization message](https://eips.ethereum.org/EIPS/eip-712) in their wallets, they retain Ethereum's guarantees of integrity. Availability depends on the server that relays transactions, so it is more limited. However, you can set things up so users can also access the smart contract directly (if they get ETH), and let others set up their own servers if they want to sponsor transactions. + +The technique in this tutorial only works when you control the smart contract. There are other techniques, including [account abstraction](https://eips.ethereum.org/EIPS/eip-4337) that let you sponsor transactions to other smart contracts, which I hope to cover in a future tutorial. + +Note: This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Learn more in the [vulnerabilities section of this guide](#vulnerabilities). + +### पूर्वतयारी {#prerequisites} + +To understand this tutorial you need to already be familiar with: + +- Solidity +- JavaScript +- React and WAGMI. If you are not familiar with these user interface tools, [we have a tutorial for that](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +## The sample application {#sample-app} + +The sample application here is a variant on Hardhat's `Greeter` contract. You can see it [on GitHub](https://github.com/qbzzt/260301-gasless). The smart contract is already deployed on the [Sepolia](https://sepolia.dev/), at address [`0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA`](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA). + +To see it in action, follow these steps. + +1. Clone the repository and install the necessary software. + + ```sh + git clone https://github.com/qbzzt/260301-gasless.git + cd 260301-gasless/server + npm install + ``` + +2. Edit `.env` to set `PRIVATE_KEY` to a wallet that has ETH on Sepolia. If you need Sepolia ETH, [use a faucet](/developers/docs/networks/#sepolia). Ideally, this private key should be different from the one you have in your browser wallet. + +3. सर्व्हर सुरू करा. + + ```sh + npm run dev + ``` + +4. Browse to the application at URL [`http://localhost:5173`](http://localhost:5173). + +5. Click **Connect with Injected** to connect to a wallet. Approve in the wallet, and approve the change to Sepolia if necessary. + +6. Write a new greeting and click **Update greeting via sponsor**. + +7. Sign the message. + +8. Wait about 12 seconds (the block time on Sepolia). While waiting you can look at the URL in the server's console to see the transaction. + +9. See that the greeting changed, and that the last updated by address value is now the address of your browser wallet. + +To understand how this works, we need to look at how the message gets created in the user interface, how it is relayed by the server, and how the smart contract processes it. + +### The user interface {#ui-changes} + +The user interface is based on [WAGMI](https://wagmi.sh/); you can read about it [in this tutorial](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +Here is how we sign the message: + +```js +const signGreeting = useCallback( +``` + +The React hook [`useCallback`](https://react.dev/reference/react/useCallback) lets us improve performance by reusing the same function when the component is redrawn. + +```js + async (greeting) => { + if (!account) throw new Error("Wallet not connected") +``` + +If there is no account, raise an error. This should never happen because the UI button that starts the process that calls `signGreeting` is disabled in that case. However, future programmers may remove that safeguard, so it's a good idea to check this condition here as well. + +```js + const domain = { + name: "Greeter", + version: "1", + chainId, + verifyingContract: contractAddr, + } +``` + +Parameters for the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator). This value is constant, so in a better-optimized implementation, we might calculate it once rather than recalculate it each time the function is called. + +- `name` is a user-readable name, such as the name of the dapp for which we are producing signatures. +- `version` is the version. Different versions are not compatible. +- `chainId` is the chain we are using, as provided [by WAGMI](https://wagmi.sh/react/api/hooks/useChainId). +- `verifyingContract` is the contract address that will verify this signature. We do not want the same signature to apply to multiple contracts, in case there are several `Greeter` contracts and we want them to have different greetings. + +```js + + const types = { + GreetingRequest: [ + { name: "greeting", type: "string" }, + ], + } +``` + +The data type we sign. Here, we have a single parameter, `greeting`, but real-life systems typically have more. + +```js + const message = { greeting } +``` + +The actual message we want to sign and send. `greeting` is both the field name and the name of the variable that fills it. + +```js + const signature = await signTypedDataAsync({ + domain, + types, + primaryType: "GreetingRequest", + message, + }) +``` + +Actually get the signature. This function is asynchronous because users take a long time (from a computer's perspective) to sign data. + +```js + const r = `0x${signature.slice(2, 66)}` + const s = `0x${signature.slice(66, 130)}` + const v = parseInt(signature.slice(130, 132), 16) + + return { + req: { greeting }, + v, + r, + s, + } + }, +``` + +The function returns a single hexadecimal value. Here we divide it into fields. + +```js + [account, chainId, contractAddr, signTypedDataAsync], +) +``` + +If any of these variables change, create a new instance of the function. The `account` and `chainId` parameters can be changed by the user in the wallet. `contractAddr` is a function of the chain Id. `signTypedDataAsync` should not change, but we import it from [a hook](https://wagmi.sh/react/api/hooks/useSignTypedData), so we can't be sure, and it's best to add it here. + +Now that the new greeting is signed, we need to send it to the server. + +```js + const sponsoredGreeting = async () => { + try { +``` + +This function takes a signature and sends it to the server. + +```js + const signedMessage = await signGreeting(newGreeting) + const response = await fetch("/server/sponsor", { +``` + +Send to the path `/server/sponsor` in the server we came from. + +```js + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signedMessage), + }) +``` + +Use `POST` to send the information JSON-encoded. + +```js + const data = await response.json() + console.log("Server response:", data) + } catch (err) { + console.error("Error:", err) + } + } +``` + +Output the response. On a production system we'd also show the response to the user. + +### The server {#server} + +I like using [Vite](https://vite.dev/) as my front-end. It automatically serves the React libraries and updates the browser when the front-end code changes. However, Vite does not include backend tooling. + +The solution is in [`index.js`](https://github.com/qbzzt/260301-gasless/blob/main/server/index.js). + +```js + app.post("/server/sponsor", async (req, res) => { + ... + }) + + // Let Vite handle everything else + const vite = await createViteServer({ + server: { middlewareMode: true } + }) + + app.use(vite.middlewares) +``` + +First we register a handler for the requests we handle ourselves (`POST` to `/server/sponsor`). Then we create and use a Vite server to handle all other URLs. + +```js + app.post("/server/sponsor", async (req, res) => { + try { + const signed = req.body + + const txHash = await sepoliaClient.writeContract({ + address: greeterAddr, + abi: greeterABI, + functionName: 'sponsoredSetGreeting', + args: [signed.req, signed.v, signed.r, signed.s], + }) + } ... + }) +``` + +This is just a standard [viem](https://viem.sh/) blockchain call. + +### The smart contract {#smart-contract} + +Finally, [`Greeter.sol`](https://github.com/qbzzt/260301-gasless/blob/main/contracts/src/Greeter.sol) needs to verify the signature. + +```solidity + constructor(string memory _greeting) { + greeting = _greeting; + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("Greeter")), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } +``` + +The constructor creates the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator), similar to the user interface code above. Blockchain execution is much more expensive, so we only calculate it once. + +```solidity + struct GreetingRequest { + string greeting; + } +``` + +This is the structure that gets signed. Here we have just one field. + +```solidity + bytes32 private constant GREETING_TYPEHASH = + keccak256("GreetingRequest(string greeting)"); +``` + +This is the [structure identifier](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct). It is calculated each time in the user interface. + +```solidity + function sponsoredSetGreeting( + GreetingRequest calldata req, + uint8 v, + bytes32 r, + bytes32 s + ) external { +``` + +This function receives a signed request and updates the greeting. + +```solidity + // Compute EIP-712 digest + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + GREETING_TYPEHASH, + keccak256(bytes(req.greeting)) + ) + ) + ) + ); +``` + +Create the digest in accordance with [EIP 712](https://eips.ethereum.org/EIPS/eip-712). + +```solidity + // Recover signer + address signer = ecrecover(digest, v, r, s); + require(signer != address(0), "Invalid signature"); +``` + +Use [`ecrecover`](https://www.evm.codes/precompiled?fork=osaka#0x01) to get the signer address. Note that a bad signature can still result in a valid address, just a random one. + +```solidity + // Apply greeting as if signer called it + greeting = req.greeting; + emit SetGreeting(signer, req.greeting); + } +``` + +Update the greeting. + +## Vulnerabilities {#vulnerabilities} + +This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Here are some, along with how to solve them. + +To see some of these attacks, click the buttons under the _Attacks_ heading and see what happens. For the **Invalid signature** button, check the server console to see the transaction response. + +### Denial of service on the server {#dos-on-server} + +The easiest attack is a [denial-of-service](https://en.wikipedia.org/wiki/Denial-of-service_attack) attack on the server. The server receives requests from anywhere on the Internet and based on those requests sends transactions. There is absolutely nothing preventing an attacker from issuing a bunch of signatures, valid or invalid. Each will cause a transaction. Eventually the server will run out of ETH to pay for gas. + +One solution to this problem is to limit the rate to one transaction per block. If the purpose is to show greetings to [externally owned accounts](/developers/docs/accounts/#key-differences), it does not matter what the greeting is in the middle of the block anyway. + +Another solution is to keep track of addresses and only allow signatures from valid customers. + +### Wrong greeting signatures {#wrong-greeting-sigs} + +When you click **Signature for wrong greeting**, you submit a valid signature for a specific address (`0xaA92c5d426430D4769c9E878C1333BDe3d689b3e`) and greeting (`Hello`). But it submits it with a different greeting. This confuses `ecrecover`, which changes the greeting but has the wrong address. + +To solve this problem, add the address to the [signed structure](https://github.com/qbzzt/260301-gasless/blob/main/server/src/Greeter.jsx#L122-L124). This way, the `ecrecover` random address won't match the address in the signature, and the smart contract will reject the message. + +### Replay attacks {#replay-attack} + +When you click **Replay attack**, you submit the same "I'm 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, and I'd like the greeting to be `Hello`" signature, but with the correct greeting. As a result, the smart contract believes that the address (which isn't yours) changed the greeting back to `Hello`. The information to do this is publicly available in the [transaction information](https://eth-sepolia.blockscout.com/tx/0xa66afe4bbf886f59533e677a798c802ceab1ac0f9db6e83a4d4b59a45cf7c1b1). + +If this is a problem, one solution is to add a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). Have a [mapping](https://docs.soliditylang.org/en/latest/types.html#mapping-types) between addresses and numbers, and add a nonce field to the signature. If the nonce field matches the mapping for the address, accept the signature and increment the mapping for next time. If it doesn't, reject the transaction. + +Another solution is to add a timestamp to the signed data and accept the signature as valid only for a few seconds after that timestamp. This is simpler and cheaper, but we risk replay attacks within the time window, and the failure of legitimate transactions if the time window is exceeded. + +## Other missing features {#other-missing-features} + +There are additional features we would add in a production setting. + +### Access from other servers {#other-servers} + +Currently, we allow any address to submit a `sponsorSetGreeting`. This may be exactly what we want, in the interest of decentralization. Or maybe we want to ensure that sponsored transactions go through _our_ server, in which case we'd check `msg.sender` in the smart contract. + +Either way, this should be a conscious design decision, not just the result of not thinking about the issue. + +### Error handling {#error-handling} + +A user submits a greeting. Maybe it gets updated at the next block. Maybe it doesn't. Errors are invisible. On a production system, the user should be able to distinguish between these cases: + +- The new greeting has not been submitted yet +- The new greeting has been submitted, and it's in process +- The new greeting has been rejected + +## निष्कर्ष {#conclusion} + +At this point, you should be able to create a gasless experience for your dapp users, at the cost of some centralization. + +However, this only works with smart contracts that support ERC-712. To transfer an ERC-20 token, for example, it is necessary to have the transaction signed by the owner rather than just a message. The solution is [account abstraction (ERC-4337)](https://docs.erc4337.io/index.html). I hope to write a future tutorial about it. + +[माझ्या कामाबद्दल अधिक माहितीसाठी येथे पहा](https://cryptodocguy.pro/). diff --git a/public/content/translations/pl/developers/tutorials/gasless/index.md b/public/content/translations/pl/developers/tutorials/gasless/index.md new file mode 100644 index 00000000000..b15b7496564 --- /dev/null +++ b/public/content/translations/pl/developers/tutorials/gasless/index.md @@ -0,0 +1,362 @@ +--- +title: "Sponsoring gas fees: How to cover transaction costs for your users" +description: It is easy to create a private key and an address; it's just a matter of running the right software. But there are many places in the world where getting the ETH to send transactions is much harder. In this tutorial you learn how to cover the onchain gas costs for executing user-signed, offchain structured data in your smart contract. You have the user sign a structure containing the transaction information, which your offchain code then submits to the blockchain as a transaction. +author: Ori Pomerantz +tags: [ "gasless", "solidity", "eip-712", "meta-transactions" ] +skill: intermediate +lang: pl +published: 2026-02-27 +--- + +## Wprowadzenie {#introduction} + +If we want Ethereum to serve [a billion more people](https://blog.ethereum.org/category/next-billion), we need to remove friction and make it as easy to use as possible. One source of this friction is the need for ETH to pay gas fees. + +If you have a dapp that makes money from users, it might make sense to let users submit transactions through your server and pay the transaction fees yourself. Because users still sign an [EIP-712 authorization message](https://eips.ethereum.org/EIPS/eip-712) in their wallets, they retain Ethereum's guarantees of integrity. Availability depends on the server that relays transactions, so it is more limited. However, you can set things up so users can also access the smart contract directly (if they get ETH), and let others set up their own servers if they want to sponsor transactions. + +The technique in this tutorial only works when you control the smart contract. There are other techniques, including [account abstraction](https://eips.ethereum.org/EIPS/eip-4337) that let you sponsor transactions to other smart contracts, which I hope to cover in a future tutorial. + +Note: This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Learn more in the [vulnerabilities section of this guide](#vulnerabilities). + +### Wymagania wstępne {#prerequisites} + +To understand this tutorial you need to already be familiar with: + +- Solidity +- JavaScript +- React and WAGMI. If you are not familiar with these user interface tools, [we have a tutorial for that](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +## The sample application {#sample-app} + +The sample application here is a variant on Hardhat's `Greeter` contract. You can see it [on GitHub](https://github.com/qbzzt/260301-gasless). The smart contract is already deployed on the [Sepolia](https://sepolia.dev/), at address [`0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA`](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA). + +To see it in action, follow these steps. + +1. Clone the repository and install the necessary software. + + ```sh + git clone https://github.com/qbzzt/260301-gasless.git + cd 260301-gasless/server + npm install + ``` + +2. Edit `.env` to set `PRIVATE_KEY` to a wallet that has ETH on Sepolia. If you need Sepolia ETH, [use a faucet](/developers/docs/networks/#sepolia). Ideally, this private key should be different from the one you have in your browser wallet. + +3. Uruchom serwer. + + ```sh + npm run dev + ``` + +4. Browse to the application at URL [`http://localhost:5173`](http://localhost:5173). + +5. Click **Connect with Injected** to connect to a wallet. Approve in the wallet, and approve the change to Sepolia if necessary. + +6. Write a new greeting and click **Update greeting via sponsor**. + +7. Sign the message. + +8. Wait about 12 seconds (the block time on Sepolia). While waiting you can look at the URL in the server's console to see the transaction. + +9. See that the greeting changed, and that the last updated by address value is now the address of your browser wallet. + +To understand how this works, we need to look at how the message gets created in the user interface, how it is relayed by the server, and how the smart contract processes it. + +### The user interface {#ui-changes} + +The user interface is based on [WAGMI](https://wagmi.sh/); you can read about it [in this tutorial](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +Here is how we sign the message: + +```js +const signGreeting = useCallback( +``` + +The React hook [`useCallback`](https://react.dev/reference/react/useCallback) lets us improve performance by reusing the same function when the component is redrawn. + +```js + async (greeting) => { + if (!account) throw new Error("Wallet not connected") +``` + +If there is no account, raise an error. This should never happen because the UI button that starts the process that calls `signGreeting` is disabled in that case. However, future programmers may remove that safeguard, so it's a good idea to check this condition here as well. + +```js + const domain = { + name: "Greeter", + version: "1", + chainId, + verifyingContract: contractAddr, + } +``` + +Parameters for the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator). This value is constant, so in a better-optimized implementation, we might calculate it once rather than recalculate it each time the function is called. + +- `name` is a user-readable name, such as the name of the dapp for which we are producing signatures. +- `version` is the version. Different versions are not compatible. +- `chainId` is the chain we are using, as provided [by WAGMI](https://wagmi.sh/react/api/hooks/useChainId). +- `verifyingContract` is the contract address that will verify this signature. We do not want the same signature to apply to multiple contracts, in case there are several `Greeter` contracts and we want them to have different greetings. + +```js + + const types = { + GreetingRequest: [ + { name: "greeting", type: "string" }, + ], + } +``` + +The data type we sign. Here, we have a single parameter, `greeting`, but real-life systems typically have more. + +```js + const message = { greeting } +``` + +The actual message we want to sign and send. `greeting` is both the field name and the name of the variable that fills it. + +```js + const signature = await signTypedDataAsync({ + domain, + types, + primaryType: "GreetingRequest", + message, + }) +``` + +Actually get the signature. This function is asynchronous because users take a long time (from a computer's perspective) to sign data. + +```js + const r = `0x${signature.slice(2, 66)}` + const s = `0x${signature.slice(66, 130)}` + const v = parseInt(signature.slice(130, 132), 16) + + return { + req: { greeting }, + v, + r, + s, + } + }, +``` + +The function returns a single hexadecimal value. Here we divide it into fields. + +```js + [account, chainId, contractAddr, signTypedDataAsync], +) +``` + +If any of these variables change, create a new instance of the function. The `account` and `chainId` parameters can be changed by the user in the wallet. `contractAddr` is a function of the chain Id. `signTypedDataAsync` should not change, but we import it from [a hook](https://wagmi.sh/react/api/hooks/useSignTypedData), so we can't be sure, and it's best to add it here. + +Now that the new greeting is signed, we need to send it to the server. + +```js + const sponsoredGreeting = async () => { + try { +``` + +This function takes a signature and sends it to the server. + +```js + const signedMessage = await signGreeting(newGreeting) + const response = await fetch("/server/sponsor", { +``` + +Send to the path `/server/sponsor` in the server we came from. + +```js + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signedMessage), + }) +``` + +Use `POST` to send the information JSON-encoded. + +```js + const data = await response.json() + console.log("Server response:", data) + } catch (err) { + console.error("Error:", err) + } + } +``` + +Output the response. On a production system we'd also show the response to the user. + +### The server {#server} + +I like using [Vite](https://vite.dev/) as my front-end. It automatically serves the React libraries and updates the browser when the front-end code changes. However, Vite does not include backend tooling. + +The solution is in [`index.js`](https://github.com/qbzzt/260301-gasless/blob/main/server/index.js). + +```js + app.post("/server/sponsor", async (req, res) => { + ... + }) + + // Let Vite handle everything else + const vite = await createViteServer({ + server: { middlewareMode: true } + }) + + app.use(vite.middlewares) +``` + +First we register a handler for the requests we handle ourselves (`POST` to `/server/sponsor`). Then we create and use a Vite server to handle all other URLs. + +```js + app.post("/server/sponsor", async (req, res) => { + try { + const signed = req.body + + const txHash = await sepoliaClient.writeContract({ + address: greeterAddr, + abi: greeterABI, + functionName: 'sponsoredSetGreeting', + args: [signed.req, signed.v, signed.r, signed.s], + }) + } ... + }) +``` + +This is just a standard [viem](https://viem.sh/) blockchain call. + +### The smart contract {#smart-contract} + +Finally, [`Greeter.sol`](https://github.com/qbzzt/260301-gasless/blob/main/contracts/src/Greeter.sol) needs to verify the signature. + +```solidity + constructor(string memory _greeting) { + greeting = _greeting; + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("Greeter")), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } +``` + +The constructor creates the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator), similar to the user interface code above. Blockchain execution is much more expensive, so we only calculate it once. + +```solidity + struct GreetingRequest { + string greeting; + } +``` + +This is the structure that gets signed. Here we have just one field. + +```solidity + bytes32 private constant GREETING_TYPEHASH = + keccak256("GreetingRequest(string greeting)"); +``` + +This is the [structure identifier](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct). It is calculated each time in the user interface. + +```solidity + function sponsoredSetGreeting( + GreetingRequest calldata req, + uint8 v, + bytes32 r, + bytes32 s + ) external { +``` + +This function receives a signed request and updates the greeting. + +```solidity + // Compute EIP-712 digest + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + GREETING_TYPEHASH, + keccak256(bytes(req.greeting)) + ) + ) + ) + ); +``` + +Create the digest in accordance with [EIP 712](https://eips.ethereum.org/EIPS/eip-712). + +```solidity + // Recover signer + address signer = ecrecover(digest, v, r, s); + require(signer != address(0), "Invalid signature"); +``` + +Use [`ecrecover`](https://www.evm.codes/precompiled?fork=osaka#0x01) to get the signer address. Note that a bad signature can still result in a valid address, just a random one. + +```solidity + // Apply greeting as if signer called it + greeting = req.greeting; + emit SetGreeting(signer, req.greeting); + } +``` + +Update the greeting. + +## Vulnerabilities {#vulnerabilities} + +This is _not_ production-level code. Jest podatny na znaczące ataki i brakuje mu głównych funkcji. Here are some, along with how to solve them. + +To see some of these attacks, click the buttons under the _Attacks_ heading and see what happens. For the **Invalid signature** button, check the server console to see the transaction response. + +### Denial of service on the server {#dos-on-server} + +The easiest attack is a [denial-of-service](https://en.wikipedia.org/wiki/Denial-of-service_attack) attack on the server. The server receives requests from anywhere on the Internet and based on those requests sends transactions. There is absolutely nothing preventing an attacker from issuing a bunch of signatures, valid or invalid. Each will cause a transaction. Eventually the server will run out of ETH to pay for gas. + +One solution to this problem is to limit the rate to one transaction per block. If the purpose is to show greetings to [externally owned accounts](/developers/docs/accounts/#key-differences), it does not matter what the greeting is in the middle of the block anyway. + +Another solution is to keep track of addresses and only allow signatures from valid customers. + +### Wrong greeting signatures {#wrong-greeting-sigs} + +When you click **Signature for wrong greeting**, you submit a valid signature for a specific address (`0xaA92c5d426430D4769c9E878C1333BDe3d689b3e`) and greeting (`Hello`). But it submits it with a different greeting. This confuses `ecrecover`, which changes the greeting but has the wrong address. + +To solve this problem, add the address to the [signed structure](https://github.com/qbzzt/260301-gasless/blob/main/server/src/Greeter.jsx#L122-L124). This way, the `ecrecover` random address won't match the address in the signature, and the smart contract will reject the message. + +### Replay attacks {#replay-attack} + +When you click **Replay attack**, you submit the same "I'm 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, and I'd like the greeting to be `Hello`" signature, but with the correct greeting. As a result, the smart contract believes that the address (which isn't yours) changed the greeting back to `Hello`. The information to do this is publicly available in the [transaction information](https://eth-sepolia.blockscout.com/tx/0xa66afe4bbf886f59533e677a798c802ceab1ac0f9db6e83a4d4b59a45cf7c1b1). + +If this is a problem, one solution is to add a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). Have a [mapping](https://docs.soliditylang.org/en/latest/types.html#mapping-types) between addresses and numbers, and add a nonce field to the signature. If the nonce field matches the mapping for the address, accept the signature and increment the mapping for next time. If it doesn't, reject the transaction. + +Another solution is to add a timestamp to the signed data and accept the signature as valid only for a few seconds after that timestamp. This is simpler and cheaper, but we risk replay attacks within the time window, and the failure of legitimate transactions if the time window is exceeded. + +## Other missing features {#other-missing-features} + +There are additional features we would add in a production setting. + +### Access from other servers {#other-servers} + +Currently, we allow any address to submit a `sponsorSetGreeting`. This may be exactly what we want, in the interest of decentralization. Or maybe we want to ensure that sponsored transactions go through _our_ server, in which case we'd check `msg.sender` in the smart contract. + +Either way, this should be a conscious design decision, not just the result of not thinking about the issue. + +### Error handling {#error-handling} + +A user submits a greeting. Maybe it gets updated at the next block. Maybe it doesn't. Errors are invisible. On a production system, the user should be able to distinguish between these cases: + +- The new greeting has not been submitted yet +- The new greeting has been submitted, and it's in process +- The new greeting has been rejected + +## Wnioski {#conclusion} + +At this point, you should be able to create a gasless experience for your dapp users, at the cost of some centralization. + +However, this only works with smart contracts that support ERC-712. To transfer an ERC-20 token, for example, it is necessary to have the transaction signed by the owner rather than just a message. The solution is [account abstraction (ERC-4337)](https://docs.erc4337.io/index.html). I hope to write a future tutorial about it. + +[Zobacz więcej mojej pracy tutaj](https://cryptodocguy.pro/). diff --git a/public/content/translations/pt-br/developers/tutorials/gasless/index.md b/public/content/translations/pt-br/developers/tutorials/gasless/index.md new file mode 100644 index 00000000000..f1ca8f98912 --- /dev/null +++ b/public/content/translations/pt-br/developers/tutorials/gasless/index.md @@ -0,0 +1,363 @@ +--- +title: "Sponsoring gas fees: How to cover transaction costs for your users" +description: It is easy to create a private key and an address; it's just a matter of running the right software. But there are many places in the world where getting the ETH to send transactions is much harder. In this tutorial you learn how to cover the onchain gas costs for executing user-signed, offchain structured data in your smart contract. You have the user sign a structure containing the transaction information, which your offchain code then submits to the blockchain as a transaction. +author: | + Ori Pomerantz +tags: [ "gasless", "solidez", "eip-712", "meta-transactions" ] +skill: intermediate +lang: pt-br +published: 2026-02-27 +--- + +## Introdução {#introduction} + +If we want Ethereum to serve [a billion more people](https://blog.ethereum.org/category/next-billion), we need to remove friction and make it as easy to use as possible. One source of this friction is the need for ETH to pay gas fees. + +If you have a dapp that makes money from users, it might make sense to let users submit transactions through your server and pay the transaction fees yourself. Because users still sign an [EIP-712 authorization message](https://eips.ethereum.org/EIPS/eip-712) in their wallets, they retain Ethereum's guarantees of integrity. Availability depends on the server that relays transactions, so it is more limited. However, you can set things up so users can also access the smart contract directly (if they get ETH), and let others set up their own servers if they want to sponsor transactions. + +The technique in this tutorial only works when you control the smart contract. There are other techniques, including [account abstraction](https://eips.ethereum.org/EIPS/eip-4337) that let you sponsor transactions to other smart contracts, which I hope to cover in a future tutorial. + +Note: This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Learn more in the [vulnerabilities section of this guide](#vulnerabilities). + +### Pré-requisitos {#prerequisites} + +To understand this tutorial you need to already be familiar with: + +- Solidity +- JavaScript +- React and WAGMI. If you are not familiar with these user interface tools, [we have a tutorial for that](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +## The sample application {#sample-app} + +The sample application here is a variant on Hardhat's `Greeter` contract. You can see it [on GitHub](https://github.com/qbzzt/260301-gasless). The smart contract is already deployed on the [Sepolia](https://sepolia.dev/), at address [`0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA`](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA). + +To see it in action, follow these steps. + +1. Clone the repository and install the necessary software. + + ```sh + git clone https://github.com/qbzzt/260301-gasless.git + cd 260301-gasless/server + npm install + ``` + +2. Edit `.env` to set `PRIVATE_KEY` to a wallet that has ETH on Sepolia. If you need Sepolia ETH, [use a faucet](/developers/docs/networks/#sepolia). Ideally, this private key should be different from the one you have in your browser wallet. + +3. Inicie o servidor. + + ```sh + npm run dev + ``` + +4. Browse to the application at URL [`http://localhost:5173`](http://localhost:5173). + +5. Click **Connect with Injected** to connect to a wallet. Approve in the wallet, and approve the change to Sepolia if necessary. + +6. Write a new greeting and click **Update greeting via sponsor**. + +7. Sign the message. + +8. Wait about 12 seconds (the block time on Sepolia). While waiting you can look at the URL in the server's console to see the transaction. + +9. See that the greeting changed, and that the last updated by address value is now the address of your browser wallet. + +To understand how this works, we need to look at how the message gets created in the user interface, how it is relayed by the server, and how the smart contract processes it. + +### The user interface {#ui-changes} + +The user interface is based on [WAGMI](https://wagmi.sh/); you can read about it [in this tutorial](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +Here is how we sign the message: + +```js +const signGreeting = useCallback( +``` + +The React hook [`useCallback`](https://react.dev/reference/react/useCallback) lets us improve performance by reusing the same function when the component is redrawn. + +```js + async (greeting) => { + if (!account) throw new Error("Wallet not connected") +``` + +If there is no account, raise an error. This should never happen because the UI button that starts the process that calls `signGreeting` is disabled in that case. However, future programmers may remove that safeguard, so it's a good idea to check this condition here as well. + +```js + const domain = { + name: "Greeter", + version: "1", + chainId, + verifyingContract: contractAddr, + } +``` + +Parameters for the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator). This value is constant, so in a better-optimized implementation, we might calculate it once rather than recalculate it each time the function is called. + +- `name` is a user-readable name, such as the name of the dapp for which we are producing signatures. +- `version` is the version. Different versions are not compatible. +- `chainId` is the chain we are using, as provided [by WAGMI](https://wagmi.sh/react/api/hooks/useChainId). +- `verifyingContract` is the contract address that will verify this signature. We do not want the same signature to apply to multiple contracts, in case there are several `Greeter` contracts and we want them to have different greetings. + +```js + + const types = { + GreetingRequest: [ + { name: "greeting", type: "string" }, + ], + } +``` + +The data type we sign. Here, we have a single parameter, `greeting`, but real-life systems typically have more. + +```js + const message = { greeting } +``` + +The actual message we want to sign and send. `greeting` is both the field name and the name of the variable that fills it. + +```js + const signature = await signTypedDataAsync({ + domain, + types, + primaryType: "GreetingRequest", + message, + }) +``` + +Actually get the signature. This function is asynchronous because users take a long time (from a computer's perspective) to sign data. + +```js + const r = `0x${signature.slice(2, 66)}` + const s = `0x${signature.slice(66, 130)}` + const v = parseInt(signature.slice(130, 132), 16) + + return { + req: { greeting }, + v, + r, + s, + } + }, +``` + +The function returns a single hexadecimal value. Here we divide it into fields. + +```js + [account, chainId, contractAddr, signTypedDataAsync], +) +``` + +If any of these variables change, create a new instance of the function. The `account` and `chainId` parameters can be changed by the user in the wallet. `contractAddr` is a function of the chain Id. `signTypedDataAsync` should not change, but we import it from [a hook](https://wagmi.sh/react/api/hooks/useSignTypedData), so we can't be sure, and it's best to add it here. + +Now that the new greeting is signed, we need to send it to the server. + +```js + const sponsoredGreeting = async () => { + try { +``` + +This function takes a signature and sends it to the server. + +```js + const signedMessage = await signGreeting(newGreeting) + const response = await fetch("/server/sponsor", { +``` + +Send to the path `/server/sponsor` in the server we came from. + +```js + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signedMessage), + }) +``` + +Use `POST` to send the information JSON-encoded. + +```js + const data = await response.json() + console.log("Server response:", data) + } catch (err) { + console.error("Error:", err) + } + } +``` + +Output the response. On a production system we'd also show the response to the user. + +### The server {#server} + +I like using [Vite](https://vite.dev/) as my front-end. It automatically serves the React libraries and updates the browser when the front-end code changes. However, Vite does not include backend tooling. + +The solution is in [`index.js`](https://github.com/qbzzt/260301-gasless/blob/main/server/index.js). + +```js + app.post("/server/sponsor", async (req, res) => { + ... + }) + + // Let Vite handle everything else + const vite = await createViteServer({ + server: { middlewareMode: true } + }) + + app.use(vite.middlewares) +``` + +First we register a handler for the requests we handle ourselves (`POST` to `/server/sponsor`). Then we create and use a Vite server to handle all other URLs. + +```js + app.post("/server/sponsor", async (req, res) => { + try { + const signed = req.body + + const txHash = await sepoliaClient.writeContract({ + address: greeterAddr, + abi: greeterABI, + functionName: 'sponsoredSetGreeting', + args: [signed.req, signed.v, signed.r, signed.s], + }) + } ... + }) +``` + +This is just a standard [viem](https://viem.sh/) blockchain call. + +### The smart contract {#smart-contract} + +Finally, [`Greeter.sol`](https://github.com/qbzzt/260301-gasless/blob/main/contracts/src/Greeter.sol) needs to verify the signature. + +```solidity + constructor(string memory _greeting) { + greeting = _greeting; + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("Greeter")), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } +``` + +The constructor creates the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator), similar to the user interface code above. Blockchain execution is much more expensive, so we only calculate it once. + +```solidity + struct GreetingRequest { + string greeting; + } +``` + +This is the structure that gets signed. Here we have just one field. + +```solidity + bytes32 private constant GREETING_TYPEHASH = + keccak256("GreetingRequest(string greeting)"); +``` + +This is the [structure identifier](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct). It is calculated each time in the user interface. + +```solidity + function sponsoredSetGreeting( + GreetingRequest calldata req, + uint8 v, + bytes32 r, + bytes32 s + ) external { +``` + +This function receives a signed request and updates the greeting. + +```solidity + // Compute EIP-712 digest + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + GREETING_TYPEHASH, + keccak256(bytes(req.greeting)) + ) + ) + ) + ); +``` + +Create the digest in accordance with [EIP 712](https://eips.ethereum.org/EIPS/eip-712). + +```solidity + // Recover signer + address signer = ecrecover(digest, v, r, s); + require(signer != address(0), "Invalid signature"); +``` + +Use [`ecrecover`](https://www.evm.codes/precompiled?fork=osaka#0x01) to get the signer address. Note that a bad signature can still result in a valid address, just a random one. + +```solidity + // Apply greeting as if signer called it + greeting = req.greeting; + emit SetGreeting(signer, req.greeting); + } +``` + +Update the greeting. + +## Vulnerabilities {#vulnerabilities} + +This is _not_ production-level code. É vulnerável a ataques significativos e carece de recursos principais. Here are some, along with how to solve them. + +To see some of these attacks, click the buttons under the _Attacks_ heading and see what happens. For the **Invalid signature** button, check the server console to see the transaction response. + +### Denial of service on the server {#dos-on-server} + +The easiest attack is a [denial-of-service](https://en.wikipedia.org/wiki/Denial-of-service_attack) attack on the server. The server receives requests from anywhere on the Internet and based on those requests sends transactions. There is absolutely nothing preventing an attacker from issuing a bunch of signatures, valid or invalid. Each will cause a transaction. Eventually the server will run out of ETH to pay for gas. + +One solution to this problem is to limit the rate to one transaction per block. If the purpose is to show greetings to [externally owned accounts](/developers/docs/accounts/#key-differences), it does not matter what the greeting is in the middle of the block anyway. + +Another solution is to keep track of addresses and only allow signatures from valid customers. + +### Wrong greeting signatures {#wrong-greeting-sigs} + +When you click **Signature for wrong greeting**, you submit a valid signature for a specific address (`0xaA92c5d426430D4769c9E878C1333BDe3d689b3e`) and greeting (`Hello`). But it submits it with a different greeting. This confuses `ecrecover`, which changes the greeting but has the wrong address. + +To solve this problem, add the address to the [signed structure](https://github.com/qbzzt/260301-gasless/blob/main/server/src/Greeter.jsx#L122-L124). This way, the `ecrecover` random address won't match the address in the signature, and the smart contract will reject the message. + +### Replay attacks {#replay-attack} + +When you click **Replay attack**, you submit the same "I'm 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, and I'd like the greeting to be `Hello`" signature, but with the correct greeting. As a result, the smart contract believes that the address (which isn't yours) changed the greeting back to `Hello`. The information to do this is publicly available in the [transaction information](https://eth-sepolia.blockscout.com/tx/0xa66afe4bbf886f59533e677a798c802ceab1ac0f9db6e83a4d4b59a45cf7c1b1). + +If this is a problem, one solution is to add a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). Have a [mapping](https://docs.soliditylang.org/en/latest/types.html#mapping-types) between addresses and numbers, and add a nonce field to the signature. If the nonce field matches the mapping for the address, accept the signature and increment the mapping for next time. If it doesn't, reject the transaction. + +Another solution is to add a timestamp to the signed data and accept the signature as valid only for a few seconds after that timestamp. This is simpler and cheaper, but we risk replay attacks within the time window, and the failure of legitimate transactions if the time window is exceeded. + +## Other missing features {#other-missing-features} + +There are additional features we would add in a production setting. + +### Access from other servers {#other-servers} + +Currently, we allow any address to submit a `sponsorSetGreeting`. This may be exactly what we want, in the interest of decentralization. Or maybe we want to ensure that sponsored transactions go through _our_ server, in which case we'd check `msg.sender` in the smart contract. + +Either way, this should be a conscious design decision, not just the result of not thinking about the issue. + +### Error handling {#error-handling} + +A user submits a greeting. Maybe it gets updated at the next block. Maybe it doesn't. Errors are invisible. On a production system, the user should be able to distinguish between these cases: + +- The new greeting has not been submitted yet +- The new greeting has been submitted, and it's in process +- The new greeting has been rejected + +## Conclusão {#conclusion} + +At this point, you should be able to create a gasless experience for your dapp users, at the cost of some centralization. + +However, this only works with smart contracts that support ERC-712. To transfer an ERC-20 token, for example, it is necessary to have the transaction signed by the owner rather than just a message. The solution is [account abstraction (ERC-4337)](https://docs.erc4337.io/index.html). I hope to write a future tutorial about it. + +[Veja aqui mais do meu trabalho](https://cryptodocguy.pro/). diff --git a/public/content/translations/ru/developers/tutorials/gasless/index.md b/public/content/translations/ru/developers/tutorials/gasless/index.md new file mode 100644 index 00000000000..578aacb2a76 --- /dev/null +++ b/public/content/translations/ru/developers/tutorials/gasless/index.md @@ -0,0 +1,368 @@ +--- +title: "Sponsoring gas fees: How to cover transaction costs for your users" +description: It is easy to create a private key and an address; it's just a matter of running the right software. But there are many places in the world where getting the ETH to send transactions is much harder. In this tutorial you learn how to cover the onchain gas costs for executing user-signed, offchain structured data in your smart contract. You have the user sign a structure containing the transaction information, which your offchain code then submits to the blockchain as a transaction. +author: "Ори Померанц" +tags: + [ + "gasless", + "твердость", + "eip-712", + "meta-transactions" + ] +skill: intermediate +lang: ru +published: 2026-02-27 +--- + +## Введение {#introduction} + +If we want Ethereum to serve [a billion more people](https://blog.ethereum.org/category/next-billion), we need to remove friction and make it as easy to use as possible. One source of this friction is the need for ETH to pay gas fees. + +If you have a dapp that makes money from users, it might make sense to let users submit transactions through your server and pay the transaction fees yourself. Because users still sign an [EIP-712 authorization message](https://eips.ethereum.org/EIPS/eip-712) in their wallets, they retain Ethereum's guarantees of integrity. Availability depends on the server that relays transactions, so it is more limited. However, you can set things up so users can also access the smart contract directly (if they get ETH), and let others set up their own servers if they want to sponsor transactions. + +The technique in this tutorial only works when you control the smart contract. There are other techniques, including [account abstraction](https://eips.ethereum.org/EIPS/eip-4337) that let you sponsor transactions to other smart contracts, which I hope to cover in a future tutorial. + +Note: This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Learn more in the [vulnerabilities section of this guide](#vulnerabilities). + +### Предварительные условия {#prerequisites} + +To understand this tutorial you need to already be familiar with: + +- Solidity +- JavaScript +- React and WAGMI. If you are not familiar with these user interface tools, [we have a tutorial for that](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +## The sample application {#sample-app} + +The sample application here is a variant on Hardhat's `Greeter` contract. You can see it [on GitHub](https://github.com/qbzzt/260301-gasless). The smart contract is already deployed on the [Sepolia](https://sepolia.dev/), at address [`0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA`](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA). + +To see it in action, follow these steps. + +1. Clone the repository and install the necessary software. + + ```sh + git clone https://github.com/qbzzt/260301-gasless.git + cd 260301-gasless/server + npm install + ``` + +2. Edit `.env` to set `PRIVATE_KEY` to a wallet that has ETH on Sepolia. If you need Sepolia ETH, [use a faucet](/developers/docs/networks/#sepolia). Ideally, this private key should be different from the one you have in your browser wallet. + +3. Запустите сервер. + + ```sh + npm run dev + ``` + +4. Browse to the application at URL [`http://localhost:5173`](http://localhost:5173). + +5. Click **Connect with Injected** to connect to a wallet. Approve in the wallet, and approve the change to Sepolia if necessary. + +6. Write a new greeting and click **Update greeting via sponsor**. + +7. Sign the message. + +8. Wait about 12 seconds (the block time on Sepolia). While waiting you can look at the URL in the server's console to see the transaction. + +9. See that the greeting changed, and that the last updated by address value is now the address of your browser wallet. + +To understand how this works, we need to look at how the message gets created in the user interface, how it is relayed by the server, and how the smart contract processes it. + +### The user interface {#ui-changes} + +The user interface is based on [WAGMI](https://wagmi.sh/); you can read about it [in this tutorial](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +Here is how we sign the message: + +```js +const signGreeting = useCallback( +``` + +The React hook [`useCallback`](https://react.dev/reference/react/useCallback) lets us improve performance by reusing the same function when the component is redrawn. + +```js + async (greeting) => { + if (!account) throw new Error("Wallet not connected") +``` + +If there is no account, raise an error. This should never happen because the UI button that starts the process that calls `signGreeting` is disabled in that case. However, future programmers may remove that safeguard, so it's a good idea to check this condition here as well. + +```js + const domain = { + name: "Greeter", + version: "1", + chainId, + verifyingContract: contractAddr, + } +``` + +Parameters for the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator). This value is constant, so in a better-optimized implementation, we might calculate it once rather than recalculate it each time the function is called. + +- `name` is a user-readable name, such as the name of the dapp for which we are producing signatures. +- `version` is the version. Different versions are not compatible. +- `chainId` is the chain we are using, as provided [by WAGMI](https://wagmi.sh/react/api/hooks/useChainId). +- `verifyingContract` is the contract address that will verify this signature. We do not want the same signature to apply to multiple contracts, in case there are several `Greeter` contracts and we want them to have different greetings. + +```js + + const types = { + GreetingRequest: [ + { name: "greeting", type: "string" }, + ], + } +``` + +The data type we sign. Here, we have a single parameter, `greeting`, but real-life systems typically have more. + +```js + const message = { greeting } +``` + +The actual message we want to sign and send. `greeting` is both the field name and the name of the variable that fills it. + +```js + const signature = await signTypedDataAsync({ + domain, + types, + primaryType: "GreetingRequest", + message, + }) +``` + +Actually get the signature. This function is asynchronous because users take a long time (from a computer's perspective) to sign data. + +```js + const r = `0x${signature.slice(2, 66)}` + const s = `0x${signature.slice(66, 130)}` + const v = parseInt(signature.slice(130, 132), 16) + + return { + req: { greeting }, + v, + r, + s, + } + }, +``` + +The function returns a single hexadecimal value. Here we divide it into fields. + +```js + [account, chainId, contractAddr, signTypedDataAsync], +) +``` + +If any of these variables change, create a new instance of the function. The `account` and `chainId` parameters can be changed by the user in the wallet. `contractAddr` is a function of the chain Id. `signTypedDataAsync` should not change, but we import it from [a hook](https://wagmi.sh/react/api/hooks/useSignTypedData), so we can't be sure, and it's best to add it here. + +Now that the new greeting is signed, we need to send it to the server. + +```js + const sponsoredGreeting = async () => { + try { +``` + +This function takes a signature and sends it to the server. + +```js + const signedMessage = await signGreeting(newGreeting) + const response = await fetch("/server/sponsor", { +``` + +Send to the path `/server/sponsor` in the server we came from. + +```js + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signedMessage), + }) +``` + +Use `POST` to send the information JSON-encoded. + +```js + const data = await response.json() + console.log("Server response:", data) + } catch (err) { + console.error("Error:", err) + } + } +``` + +Output the response. On a production system we'd also show the response to the user. + +### The server {#server} + +I like using [Vite](https://vite.dev/) as my front-end. It automatically serves the React libraries and updates the browser when the front-end code changes. However, Vite does not include backend tooling. + +The solution is in [`index.js`](https://github.com/qbzzt/260301-gasless/blob/main/server/index.js). + +```js + app.post("/server/sponsor", async (req, res) => { + ... + }) + + // Let Vite handle everything else + const vite = await createViteServer({ + server: { middlewareMode: true } + }) + + app.use(vite.middlewares) +``` + +First we register a handler for the requests we handle ourselves (`POST` to `/server/sponsor`). Then we create and use a Vite server to handle all other URLs. + +```js + app.post("/server/sponsor", async (req, res) => { + try { + const signed = req.body + + const txHash = await sepoliaClient.writeContract({ + address: greeterAddr, + abi: greeterABI, + functionName: 'sponsoredSetGreeting', + args: [signed.req, signed.v, signed.r, signed.s], + }) + } ... + }) +``` + +This is just a standard [viem](https://viem.sh/) blockchain call. + +### The smart contract {#smart-contract} + +Finally, [`Greeter.sol`](https://github.com/qbzzt/260301-gasless/blob/main/contracts/src/Greeter.sol) needs to verify the signature. + +```solidity + constructor(string memory _greeting) { + greeting = _greeting; + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("Greeter")), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } +``` + +The constructor creates the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator), similar to the user interface code above. Blockchain execution is much more expensive, so we only calculate it once. + +```solidity + struct GreetingRequest { + string greeting; + } +``` + +This is the structure that gets signed. Here we have just one field. + +```solidity + bytes32 private constant GREETING_TYPEHASH = + keccak256("GreetingRequest(string greeting)"); +``` + +This is the [structure identifier](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct). It is calculated each time in the user interface. + +```solidity + function sponsoredSetGreeting( + GreetingRequest calldata req, + uint8 v, + bytes32 r, + bytes32 s + ) external { +``` + +This function receives a signed request and updates the greeting. + +```solidity + // Compute EIP-712 digest + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + GREETING_TYPEHASH, + keccak256(bytes(req.greeting)) + ) + ) + ) + ); +``` + +Create the digest in accordance with [EIP 712](https://eips.ethereum.org/EIPS/eip-712). + +```solidity + // Recover signer + address signer = ecrecover(digest, v, r, s); + require(signer != address(0), "Invalid signature"); +``` + +Use [`ecrecover`](https://www.evm.codes/precompiled?fork=osaka#0x01) to get the signer address. Note that a bad signature can still result in a valid address, just a random one. + +```solidity + // Apply greeting as if signer called it + greeting = req.greeting; + emit SetGreeting(signer, req.greeting); + } +``` + +Update the greeting. + +## Vulnerabilities {#vulnerabilities} + +This is _not_ production-level code. Она уязвима для серьезных атак, и в ней отсутствуют основные функции. Here are some, along with how to solve them. + +To see some of these attacks, click the buttons under the _Attacks_ heading and see what happens. For the **Invalid signature** button, check the server console to see the transaction response. + +### Denial of service on the server {#dos-on-server} + +The easiest attack is a [denial-of-service](https://en.wikipedia.org/wiki/Denial-of-service_attack) attack on the server. The server receives requests from anywhere on the Internet and based on those requests sends transactions. There is absolutely nothing preventing an attacker from issuing a bunch of signatures, valid or invalid. Each will cause a transaction. Eventually the server will run out of ETH to pay for gas. + +One solution to this problem is to limit the rate to one transaction per block. If the purpose is to show greetings to [externally owned accounts](/developers/docs/accounts/#key-differences), it does not matter what the greeting is in the middle of the block anyway. + +Another solution is to keep track of addresses and only allow signatures from valid customers. + +### Wrong greeting signatures {#wrong-greeting-sigs} + +When you click **Signature for wrong greeting**, you submit a valid signature for a specific address (`0xaA92c5d426430D4769c9E878C1333BDe3d689b3e`) and greeting (`Hello`). But it submits it with a different greeting. This confuses `ecrecover`, which changes the greeting but has the wrong address. + +To solve this problem, add the address to the [signed structure](https://github.com/qbzzt/260301-gasless/blob/main/server/src/Greeter.jsx#L122-L124). This way, the `ecrecover` random address won't match the address in the signature, and the smart contract will reject the message. + +### Replay attacks {#replay-attack} + +When you click **Replay attack**, you submit the same "I'm 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, and I'd like the greeting to be `Hello`" signature, but with the correct greeting. As a result, the smart contract believes that the address (which isn't yours) changed the greeting back to `Hello`. The information to do this is publicly available in the [transaction information](https://eth-sepolia.blockscout.com/tx/0xa66afe4bbf886f59533e677a798c802ceab1ac0f9db6e83a4d4b59a45cf7c1b1). + +If this is a problem, one solution is to add a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). Have a [mapping](https://docs.soliditylang.org/en/latest/types.html#mapping-types) between addresses and numbers, and add a nonce field to the signature. If the nonce field matches the mapping for the address, accept the signature and increment the mapping for next time. If it doesn't, reject the transaction. + +Another solution is to add a timestamp to the signed data and accept the signature as valid only for a few seconds after that timestamp. This is simpler and cheaper, but we risk replay attacks within the time window, and the failure of legitimate transactions if the time window is exceeded. + +## Other missing features {#other-missing-features} + +There are additional features we would add in a production setting. + +### Access from other servers {#other-servers} + +Currently, we allow any address to submit a `sponsorSetGreeting`. This may be exactly what we want, in the interest of decentralization. Or maybe we want to ensure that sponsored transactions go through _our_ server, in which case we'd check `msg.sender` in the smart contract. + +Either way, this should be a conscious design decision, not just the result of not thinking about the issue. + +### Error handling {#error-handling} + +A user submits a greeting. Maybe it gets updated at the next block. Maybe it doesn't. Errors are invisible. On a production system, the user should be able to distinguish between these cases: + +- The new greeting has not been submitted yet +- The new greeting has been submitted, and it's in process +- The new greeting has been rejected + +## Заключение {#conclusion} + +At this point, you should be able to create a gasless experience for your dapp users, at the cost of some centralization. + +However, this only works with smart contracts that support ERC-712. To transfer an ERC-20 token, for example, it is necessary to have the transaction signed by the owner rather than just a message. The solution is [account abstraction (ERC-4337)](https://docs.erc4337.io/index.html). I hope to write a future tutorial about it. + +[Больше моих работ смотрите здесь](https://cryptodocguy.pro/). diff --git a/public/content/translations/sw/developers/tutorials/gasless/index.md b/public/content/translations/sw/developers/tutorials/gasless/index.md new file mode 100644 index 00000000000..ee79f15b2eb --- /dev/null +++ b/public/content/translations/sw/developers/tutorials/gasless/index.md @@ -0,0 +1,362 @@ +--- +title: "Sponsoring gas fees: How to cover transaction costs for your users" +description: It is easy to create a private key and an address; it's just a matter of running the right software. But there are many places in the world where getting the ETH to send transactions is much harder. In this tutorial you learn how to cover the onchain gas costs for executing user-signed, offchain structured data in your smart contract. You have the user sign a structure containing the transaction information, which your offchain code then submits to the blockchain as a transaction. +author: Ori Pomerantz +tags: [ "gasless", "uimara", "eip-712", "meta-transactions" ] +skill: intermediate +lang: sw +published: 2026-02-27 +--- + +## Utangulizi {#introduction} + +If we want Ethereum to serve [a billion more people](https://blog.ethereum.org/category/next-billion), we need to remove friction and make it as easy to use as possible. One source of this friction is the need for ETH to pay gas fees. + +If you have a dapp that makes money from users, it might make sense to let users submit transactions through your server and pay the transaction fees yourself. Because users still sign an [EIP-712 authorization message](https://eips.ethereum.org/EIPS/eip-712) in their wallets, they retain Ethereum's guarantees of integrity. Availability depends on the server that relays transactions, so it is more limited. However, you can set things up so users can also access the smart contract directly (if they get ETH), and let others set up their own servers if they want to sponsor transactions. + +The technique in this tutorial only works when you control the smart contract. There are other techniques, including [account abstraction](https://eips.ethereum.org/EIPS/eip-4337) that let you sponsor transactions to other smart contracts, which I hope to cover in a future tutorial. + +Note: This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Learn more in the [vulnerabilities section of this guide](#vulnerabilities). + +### Mahitaji ya awali {#prerequisites} + +To understand this tutorial you need to already be familiar with: + +- Uimara +- JavaScript +- React and WAGMI. If you are not familiar with these user interface tools, [we have a tutorial for that](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +## The sample application {#sample-app} + +The sample application here is a variant on Hardhat's `Greeter` contract. You can see it [on GitHub](https://github.com/qbzzt/260301-gasless). The smart contract is already deployed on the [Sepolia](https://sepolia.dev/), at address [`0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA`](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA). + +To see it in action, follow these steps. + +1. Clone the repository and install the necessary software. + + ```sh + git clone https://github.com/qbzzt/260301-gasless.git + cd 260301-gasless/server + npm install + ``` + +2. Edit `.env` to set `PRIVATE_KEY` to a wallet that has ETH on Sepolia. If you need Sepolia ETH, [use a faucet](/developers/docs/networks/#sepolia). Ideally, this private key should be different from the one you have in your browser wallet. + +3. Anzisha seva. + + ```sh + npm run dev + ``` + +4. Browse to the application at URL [`http://localhost:5173`](http://localhost:5173). + +5. Click **Connect with Injected** to connect to a wallet. Approve in the wallet, and approve the change to Sepolia if necessary. + +6. Write a new greeting and click **Update greeting via sponsor**. + +7. Sign the message. + +8. Wait about 12 seconds (the block time on Sepolia). While waiting you can look at the URL in the server's console to see the transaction. + +9. See that the greeting changed, and that the last updated by address value is now the address of your browser wallet. + +To understand how this works, we need to look at how the message gets created in the user interface, how it is relayed by the server, and how the smart contract processes it. + +### The user interface {#ui-changes} + +The user interface is based on [WAGMI](https://wagmi.sh/); you can read about it [in this tutorial](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +Here is how we sign the message: + +```js +const signGreeting = useCallback( +``` + +The React hook [`useCallback`](https://react.dev/reference/react/useCallback) lets us improve performance by reusing the same function when the component is redrawn. + +```js + async (greeting) => { + if (!account) throw new Error("Wallet not connected") +``` + +If there is no account, raise an error. This should never happen because the UI button that starts the process that calls `signGreeting` is disabled in that case. However, future programmers may remove that safeguard, so it's a good idea to check this condition here as well. + +```js + const domain = { + name: "Greeter", + version: "1", + chainId, + verifyingContract: contractAddr, + } +``` + +Parameters for the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator). This value is constant, so in a better-optimized implementation, we might calculate it once rather than recalculate it each time the function is called. + +- `name` is a user-readable name, such as the name of the dapp for which we are producing signatures. +- `version` is the version. Different versions are not compatible. +- `chainId` is the chain we are using, as provided [by WAGMI](https://wagmi.sh/react/api/hooks/useChainId). +- `verifyingContract` is the contract address that will verify this signature. We do not want the same signature to apply to multiple contracts, in case there are several `Greeter` contracts and we want them to have different greetings. + +```js + + const types = { + GreetingRequest: [ + { name: "greeting", type: "string" }, + ], + } +``` + +The data type we sign. Here, we have a single parameter, `greeting`, but real-life systems typically have more. + +```js + const message = { greeting } +``` + +The actual message we want to sign and send. `greeting` is both the field name and the name of the variable that fills it. + +```js + const signature = await signTypedDataAsync({ + domain, + types, + primaryType: "GreetingRequest", + message, + }) +``` + +Actually get the signature. This function is asynchronous because users take a long time (from a computer's perspective) to sign data. + +```js + const r = `0x${signature.slice(2, 66)}` + const s = `0x${signature.slice(66, 130)}` + const v = parseInt(signature.slice(130, 132), 16) + + return { + req: { greeting }, + v, + r, + s, + } + }, +``` + +The function returns a single hexadecimal value. Here we divide it into fields. + +```js + [account, chainId, contractAddr, signTypedDataAsync], +) +``` + +If any of these variables change, create a new instance of the function. The `account` and `chainId` parameters can be changed by the user in the wallet. `contractAddr` is a function of the chain Id. `signTypedDataAsync` should not change, but we import it from [a hook](https://wagmi.sh/react/api/hooks/useSignTypedData), so we can't be sure, and it's best to add it here. + +Now that the new greeting is signed, we need to send it to the server. + +```js + const sponsoredGreeting = async () => { + try { +``` + +This function takes a signature and sends it to the server. + +```js + const signedMessage = await signGreeting(newGreeting) + const response = await fetch("/server/sponsor", { +``` + +Send to the path `/server/sponsor` in the server we came from. + +```js + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signedMessage), + }) +``` + +Use `POST` to send the information JSON-encoded. + +```js + const data = await response.json() + console.log("Server response:", data) + } catch (err) { + console.error("Error:", err) + } + } +``` + +Output the response. On a production system we'd also show the response to the user. + +### The server {#server} + +I like using [Vite](https://vite.dev/) as my front-end. It automatically serves the React libraries and updates the browser when the front-end code changes. However, Vite does not include backend tooling. + +The solution is in [`index.js`](https://github.com/qbzzt/260301-gasless/blob/main/server/index.js). + +```js + app.post("/server/sponsor", async (req, res) => { + ... + }) + + // Let Vite handle everything else + const vite = await createViteServer({ + server: { middlewareMode: true } + }) + + app.use(vite.middlewares) +``` + +First we register a handler for the requests we handle ourselves (`POST` to `/server/sponsor`). Then we create and use a Vite server to handle all other URLs. + +```js + app.post("/server/sponsor", async (req, res) => { + try { + const signed = req.body + + const txHash = await sepoliaClient.writeContract({ + address: greeterAddr, + abi: greeterABI, + functionName: 'sponsoredSetGreeting', + args: [signed.req, signed.v, signed.r, signed.s], + }) + } ... + }) +``` + +This is just a standard [viem](https://viem.sh/) blockchain call. + +### The smart contract {#smart-contract} + +Finally, [`Greeter.sol`](https://github.com/qbzzt/260301-gasless/blob/main/contracts/src/Greeter.sol) needs to verify the signature. + +```solidity + constructor(string memory _greeting) { + greeting = _greeting; + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("Greeter")), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } +``` + +The constructor creates the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator), similar to the user interface code above. Blockchain execution is much more expensive, so we only calculate it once. + +```solidity + struct GreetingRequest { + string greeting; + } +``` + +This is the structure that gets signed. Here we have just one field. + +```solidity + bytes32 private constant GREETING_TYPEHASH = + keccak256("GreetingRequest(string greeting)"); +``` + +This is the [structure identifier](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct). It is calculated each time in the user interface. + +```solidity + function sponsoredSetGreeting( + GreetingRequest calldata req, + uint8 v, + bytes32 r, + bytes32 s + ) external { +``` + +This function receives a signed request and updates the greeting. + +```solidity + // Compute EIP-712 digest + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + GREETING_TYPEHASH, + keccak256(bytes(req.greeting)) + ) + ) + ) + ); +``` + +Create the digest in accordance with [EIP 712](https://eips.ethereum.org/EIPS/eip-712). + +```solidity + // Recover signer + address signer = ecrecover(digest, v, r, s); + require(signer != address(0), "Invalid signature"); +``` + +Use [`ecrecover`](https://www.evm.codes/precompiled?fork=osaka#0x01) to get the signer address. Note that a bad signature can still result in a valid address, just a random one. + +```solidity + // Apply greeting as if signer called it + greeting = req.greeting; + emit SetGreeting(signer, req.greeting); + } +``` + +Update the greeting. + +## Vulnerabilities {#vulnerabilities} + +This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Here are some, along with how to solve them. + +To see some of these attacks, click the buttons under the _Attacks_ heading and see what happens. For the **Invalid signature** button, check the server console to see the transaction response. + +### Denial of service on the server {#dos-on-server} + +The easiest attack is a [denial-of-service](https://en.wikipedia.org/wiki/Denial-of-service_attack) attack on the server. The server receives requests from anywhere on the Internet and based on those requests sends transactions. There is absolutely nothing preventing an attacker from issuing a bunch of signatures, valid or invalid. Each will cause a transaction. Eventually the server will run out of ETH to pay for gas. + +One solution to this problem is to limit the rate to one transaction per block. If the purpose is to show greetings to [externally owned accounts](/developers/docs/accounts/#key-differences), it does not matter what the greeting is in the middle of the block anyway. + +Another solution is to keep track of addresses and only allow signatures from valid customers. + +### Wrong greeting signatures {#wrong-greeting-sigs} + +When you click **Signature for wrong greeting**, you submit a valid signature for a specific address (`0xaA92c5d426430D4769c9E878C1333BDe3d689b3e`) and greeting (`Hello`). But it submits it with a different greeting. This confuses `ecrecover`, which changes the greeting but has the wrong address. + +To solve this problem, add the address to the [signed structure](https://github.com/qbzzt/260301-gasless/blob/main/server/src/Greeter.jsx#L122-L124). This way, the `ecrecover` random address won't match the address in the signature, and the smart contract will reject the message. + +### Replay attacks {#replay-attack} + +When you click **Replay attack**, you submit the same "I'm 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, and I'd like the greeting to be `Hello`" signature, but with the correct greeting. As a result, the smart contract believes that the address (which isn't yours) changed the greeting back to `Hello`. The information to do this is publicly available in the [transaction information](https://eth-sepolia.blockscout.com/tx/0xa66afe4bbf886f59533e677a798c802ceab1ac0f9db6e83a4d4b59a45cf7c1b1). + +If this is a problem, one solution is to add a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). Have a [mapping](https://docs.soliditylang.org/en/latest/types.html#mapping-types) between addresses and numbers, and add a nonce field to the signature. If the nonce field matches the mapping for the address, accept the signature and increment the mapping for next time. If it doesn't, reject the transaction. + +Another solution is to add a timestamp to the signed data and accept the signature as valid only for a few seconds after that timestamp. This is simpler and cheaper, but we risk replay attacks within the time window, and the failure of legitimate transactions if the time window is exceeded. + +## Other missing features {#other-missing-features} + +There are additional features we would add in a production setting. + +### Access from other servers {#other-servers} + +Currently, we allow any address to submit a `sponsorSetGreeting`. This may be exactly what we want, in the interest of decentralization. Or maybe we want to ensure that sponsored transactions go through _our_ server, in which case we'd check `msg.sender` in the smart contract. + +Either way, this should be a conscious design decision, not just the result of not thinking about the issue. + +### Error handling {#error-handling} + +A user submits a greeting. Maybe it gets updated at the next block. Maybe it doesn't. Errors are invisible. On a production system, the user should be able to distinguish between these cases: + +- The new greeting has not been submitted yet +- The new greeting has been submitted, and it's in process +- The new greeting has been rejected + +## Hitimisho {#conclusion} + +At this point, you should be able to create a gasless experience for your dapp users, at the cost of some centralization. + +However, this only works with smart contracts that support ERC-712. To transfer an ERC-20 token, for example, it is necessary to have the transaction signed by the owner rather than just a message. The solution is [account abstraction (ERC-4337)](https://docs.erc4337.io/index.html). I hope to write a future tutorial about it. + +[Tazama hapa kwa kazi zangu zaidi](https://cryptodocguy.pro/). diff --git a/public/content/translations/ta/developers/tutorials/gasless/index.md b/public/content/translations/ta/developers/tutorials/gasless/index.md new file mode 100644 index 00000000000..a8ea69445e1 --- /dev/null +++ b/public/content/translations/ta/developers/tutorials/gasless/index.md @@ -0,0 +1,362 @@ +--- +title: "Sponsoring gas fees: How to cover transaction costs for your users" +description: It is easy to create a private key and an address; it's just a matter of running the right software. But there are many places in the world where getting the ETH to send transactions is much harder. In this tutorial you learn how to cover the onchain gas costs for executing user-signed, offchain structured data in your smart contract. You have the user sign a structure containing the transaction information, which your offchain code then submits to the blockchain as a transaction. +author: Ori Pomerantz +tags: [ "gasless", "திட்பம்", "eip-712", "meta-transactions" ] +skill: intermediate +lang: ta +published: 2026-02-27 +--- + +## அறிமுகம் {#introduction} + +If we want Ethereum to serve [a billion more people](https://blog.ethereum.org/category/next-billion), we need to remove friction and make it as easy to use as possible. One source of this friction is the need for ETH to pay gas fees. + +If you have a dapp that makes money from users, it might make sense to let users submit transactions through your server and pay the transaction fees yourself. Because users still sign an [EIP-712 authorization message](https://eips.ethereum.org/EIPS/eip-712) in their wallets, they retain Ethereum's guarantees of integrity. Availability depends on the server that relays transactions, so it is more limited. However, you can set things up so users can also access the smart contract directly (if they get ETH), and let others set up their own servers if they want to sponsor transactions. + +The technique in this tutorial only works when you control the smart contract. There are other techniques, including [account abstraction](https://eips.ethereum.org/EIPS/eip-4337) that let you sponsor transactions to other smart contracts, which I hope to cover in a future tutorial. + +Note: This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Learn more in the [vulnerabilities section of this guide](#vulnerabilities). + +### முன்னேற்றக் கட்டுரை {#prerequisites} + +To understand this tutorial you need to already be familiar with: + +- Solidity +- ஜாவாஸ்கிரிப்ட் +- React and WAGMI. If you are not familiar with these user interface tools, [we have a tutorial for that](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +## The sample application {#sample-app} + +The sample application here is a variant on Hardhat's `Greeter` contract. You can see it [on GitHub](https://github.com/qbzzt/260301-gasless). The smart contract is already deployed on the [Sepolia](https://sepolia.dev/), at address [`0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA`](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA). + +To see it in action, follow these steps. + +1. Clone the repository and install the necessary software. + + ```sh + git clone https://github.com/qbzzt/260301-gasless.git + cd 260301-gasless/server + npm install + ``` + +2. Edit `.env` to set `PRIVATE_KEY` to a wallet that has ETH on Sepolia. If you need Sepolia ETH, [use a faucet](/developers/docs/networks/#sepolia). Ideally, this private key should be different from the one you have in your browser wallet. + +3. சேவையகத்தைத் தொடங்கவும். + + ```sh + npm run dev + ``` + +4. Browse to the application at URL [`http://localhost:5173`](http://localhost:5173). + +5. Click **Connect with Injected** to connect to a wallet. Approve in the wallet, and approve the change to Sepolia if necessary. + +6. Write a new greeting and click **Update greeting via sponsor**. + +7. Sign the message. + +8. Wait about 12 seconds (the block time on Sepolia). While waiting you can look at the URL in the server's console to see the transaction. + +9. See that the greeting changed, and that the last updated by address value is now the address of your browser wallet. + +To understand how this works, we need to look at how the message gets created in the user interface, how it is relayed by the server, and how the smart contract processes it. + +### The user interface {#ui-changes} + +The user interface is based on [WAGMI](https://wagmi.sh/); you can read about it [in this tutorial](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +Here is how we sign the message: + +```js +const signGreeting = useCallback( +``` + +The React hook [`useCallback`](https://react.dev/reference/react/useCallback) lets us improve performance by reusing the same function when the component is redrawn. + +```js + async (greeting) => { + if (!account) throw new Error("Wallet not connected") +``` + +If there is no account, raise an error. This should never happen because the UI button that starts the process that calls `signGreeting` is disabled in that case. However, future programmers may remove that safeguard, so it's a good idea to check this condition here as well. + +```js + const domain = { + name: "Greeter", + version: "1", + chainId, + verifyingContract: contractAddr, + } +``` + +Parameters for the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator). This value is constant, so in a better-optimized implementation, we might calculate it once rather than recalculate it each time the function is called. + +- `name` is a user-readable name, such as the name of the dapp for which we are producing signatures. +- `version` is the version. Different versions are not compatible. +- `chainId` is the chain we are using, as provided [by WAGMI](https://wagmi.sh/react/api/hooks/useChainId). +- `verifyingContract` is the contract address that will verify this signature. We do not want the same signature to apply to multiple contracts, in case there are several `Greeter` contracts and we want them to have different greetings. + +```js + + const types = { + GreetingRequest: [ + { name: "greeting", type: "string" }, + ], + } +``` + +The data type we sign. Here, we have a single parameter, `greeting`, but real-life systems typically have more. + +```js + const message = { greeting } +``` + +The actual message we want to sign and send. `greeting` is both the field name and the name of the variable that fills it. + +```js + const signature = await signTypedDataAsync({ + domain, + types, + primaryType: "GreetingRequest", + message, + }) +``` + +Actually get the signature. This function is asynchronous because users take a long time (from a computer's perspective) to sign data. + +```js + const r = `0x${signature.slice(2, 66)}` + const s = `0x${signature.slice(66, 130)}` + const v = parseInt(signature.slice(130, 132), 16) + + return { + req: { greeting }, + v, + r, + s, + } + }, +``` + +The function returns a single hexadecimal value. Here we divide it into fields. + +```js + [account, chainId, contractAddr, signTypedDataAsync], +) +``` + +If any of these variables change, create a new instance of the function. The `account` and `chainId` parameters can be changed by the user in the wallet. `contractAddr` is a function of the chain Id. `signTypedDataAsync` should not change, but we import it from [a hook](https://wagmi.sh/react/api/hooks/useSignTypedData), so we can't be sure, and it's best to add it here. + +Now that the new greeting is signed, we need to send it to the server. + +```js + const sponsoredGreeting = async () => { + try { +``` + +This function takes a signature and sends it to the server. + +```js + const signedMessage = await signGreeting(newGreeting) + const response = await fetch("/server/sponsor", { +``` + +Send to the path `/server/sponsor` in the server we came from. + +```js + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signedMessage), + }) +``` + +Use `POST` to send the information JSON-encoded. + +```js + const data = await response.json() + console.log("Server response:", data) + } catch (err) { + console.error("Error:", err) + } + } +``` + +Output the response. On a production system we'd also show the response to the user. + +### The server {#server} + +I like using [Vite](https://vite.dev/) as my front-end. It automatically serves the React libraries and updates the browser when the front-end code changes. However, Vite does not include backend tooling. + +The solution is in [`index.js`](https://github.com/qbzzt/260301-gasless/blob/main/server/index.js). + +```js + app.post("/server/sponsor", async (req, res) => { + ... + }) + + // Let Vite handle everything else + const vite = await createViteServer({ + server: { middlewareMode: true } + }) + + app.use(vite.middlewares) +``` + +First we register a handler for the requests we handle ourselves (`POST` to `/server/sponsor`). Then we create and use a Vite server to handle all other URLs. + +```js + app.post("/server/sponsor", async (req, res) => { + try { + const signed = req.body + + const txHash = await sepoliaClient.writeContract({ + address: greeterAddr, + abi: greeterABI, + functionName: 'sponsoredSetGreeting', + args: [signed.req, signed.v, signed.r, signed.s], + }) + } ... + }) +``` + +This is just a standard [viem](https://viem.sh/) blockchain call. + +### The smart contract {#smart-contract} + +Finally, [`Greeter.sol`](https://github.com/qbzzt/260301-gasless/blob/main/contracts/src/Greeter.sol) needs to verify the signature. + +```solidity + constructor(string memory _greeting) { + greeting = _greeting; + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("Greeter")), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } +``` + +The constructor creates the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator), similar to the user interface code above. Blockchain execution is much more expensive, so we only calculate it once. + +```solidity + struct GreetingRequest { + string greeting; + } +``` + +This is the structure that gets signed. Here we have just one field. + +```solidity + bytes32 private constant GREETING_TYPEHASH = + keccak256("GreetingRequest(string greeting)"); +``` + +This is the [structure identifier](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct). It is calculated each time in the user interface. + +```solidity + function sponsoredSetGreeting( + GreetingRequest calldata req, + uint8 v, + bytes32 r, + bytes32 s + ) external { +``` + +This function receives a signed request and updates the greeting. + +```solidity + // Compute EIP-712 digest + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + GREETING_TYPEHASH, + keccak256(bytes(req.greeting)) + ) + ) + ) + ); +``` + +Create the digest in accordance with [EIP 712](https://eips.ethereum.org/EIPS/eip-712). + +```solidity + // Recover signer + address signer = ecrecover(digest, v, r, s); + require(signer != address(0), "Invalid signature"); +``` + +Use [`ecrecover`](https://www.evm.codes/precompiled?fork=osaka#0x01) to get the signer address. Note that a bad signature can still result in a valid address, just a random one. + +```solidity + // Apply greeting as if signer called it + greeting = req.greeting; + emit SetGreeting(signer, req.greeting); + } +``` + +Update the greeting. + +## Vulnerabilities {#vulnerabilities} + +This is _not_ production-level code. இது குறிப்பிடத்தக்க தாக்குதல்களுக்கு ஆளாகக்கூடியது மற்றும் முக்கிய அம்சங்களைக் கொண்டிருக்கவில்லை. Here are some, along with how to solve them. + +To see some of these attacks, click the buttons under the _Attacks_ heading and see what happens. For the **Invalid signature** button, check the server console to see the transaction response. + +### Denial of service on the server {#dos-on-server} + +The easiest attack is a [denial-of-service](https://en.wikipedia.org/wiki/Denial-of-service_attack) attack on the server. The server receives requests from anywhere on the Internet and based on those requests sends transactions. There is absolutely nothing preventing an attacker from issuing a bunch of signatures, valid or invalid. Each will cause a transaction. Eventually the server will run out of ETH to pay for gas. + +One solution to this problem is to limit the rate to one transaction per block. If the purpose is to show greetings to [externally owned accounts](/developers/docs/accounts/#key-differences), it does not matter what the greeting is in the middle of the block anyway. + +Another solution is to keep track of addresses and only allow signatures from valid customers. + +### Wrong greeting signatures {#wrong-greeting-sigs} + +When you click **Signature for wrong greeting**, you submit a valid signature for a specific address (`0xaA92c5d426430D4769c9E878C1333BDe3d689b3e`) and greeting (`Hello`). But it submits it with a different greeting. This confuses `ecrecover`, which changes the greeting but has the wrong address. + +To solve this problem, add the address to the [signed structure](https://github.com/qbzzt/260301-gasless/blob/main/server/src/Greeter.jsx#L122-L124). This way, the `ecrecover` random address won't match the address in the signature, and the smart contract will reject the message. + +### Replay attacks {#replay-attack} + +When you click **Replay attack**, you submit the same "I'm 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, and I'd like the greeting to be `Hello`" signature, but with the correct greeting. As a result, the smart contract believes that the address (which isn't yours) changed the greeting back to `Hello`. The information to do this is publicly available in the [transaction information](https://eth-sepolia.blockscout.com/tx/0xa66afe4bbf886f59533e677a798c802ceab1ac0f9db6e83a4d4b59a45cf7c1b1). + +If this is a problem, one solution is to add a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). Have a [mapping](https://docs.soliditylang.org/en/latest/types.html#mapping-types) between addresses and numbers, and add a nonce field to the signature. If the nonce field matches the mapping for the address, accept the signature and increment the mapping for next time. If it doesn't, reject the transaction. + +Another solution is to add a timestamp to the signed data and accept the signature as valid only for a few seconds after that timestamp. This is simpler and cheaper, but we risk replay attacks within the time window, and the failure of legitimate transactions if the time window is exceeded. + +## Other missing features {#other-missing-features} + +There are additional features we would add in a production setting. + +### Access from other servers {#other-servers} + +Currently, we allow any address to submit a `sponsorSetGreeting`. This may be exactly what we want, in the interest of decentralization. Or maybe we want to ensure that sponsored transactions go through _our_ server, in which case we'd check `msg.sender` in the smart contract. + +Either way, this should be a conscious design decision, not just the result of not thinking about the issue. + +### Error handling {#error-handling} + +A user submits a greeting. Maybe it gets updated at the next block. Maybe it doesn't. Errors are invisible. On a production system, the user should be able to distinguish between these cases: + +- The new greeting has not been submitted yet +- The new greeting has been submitted, and it's in process +- The new greeting has been rejected + +## முடிவுரை {#conclusion} + +At this point, you should be able to create a gasless experience for your dapp users, at the cost of some centralization. + +However, this only works with smart contracts that support ERC-712. To transfer an ERC-20 token, for example, it is necessary to have the transaction signed by the owner rather than just a message. The solution is [account abstraction (ERC-4337)](https://docs.erc4337.io/index.html). I hope to write a future tutorial about it. + +[எனது மேலும் பணிகளை இங்கே பார்க்கவும்](https://cryptodocguy.pro/). diff --git a/public/content/translations/te/developers/tutorials/gasless/index.md b/public/content/translations/te/developers/tutorials/gasless/index.md new file mode 100644 index 00000000000..0a27c714f23 --- /dev/null +++ b/public/content/translations/te/developers/tutorials/gasless/index.md @@ -0,0 +1,362 @@ +--- +title: "Sponsoring gas fees: How to cover transaction costs for your users" +description: It is easy to create a private key and an address; it's just a matter of running the right software. But there are many places in the world where getting the ETH to send transactions is much harder. In this tutorial you learn how to cover the onchain gas costs for executing user-signed, offchain structured data in your smart contract. You have the user sign a structure containing the transaction information, which your offchain code then submits to the blockchain as a transaction. +author: "ఓరి పోమెరాంట్జ్" +tags: [ "gasless", "దృఢత్వం", "eip-712", "meta-transactions" ] +skill: "మధ్యస్థ" +lang: te +published: 2026-02-27 +--- + +## పరిచయం {#introduction} + +If we want Ethereum to serve [a billion more people](https://blog.ethereum.org/category/next-billion), we need to remove friction and make it as easy to use as possible. One source of this friction is the need for ETH to pay gas fees. + +If you have a dapp that makes money from users, it might make sense to let users submit transactions through your server and pay the transaction fees yourself. Because users still sign an [EIP-712 authorization message](https://eips.ethereum.org/EIPS/eip-712) in their wallets, they retain Ethereum's guarantees of integrity. Availability depends on the server that relays transactions, so it is more limited. However, you can set things up so users can also access the smart contract directly (if they get ETH), and let others set up their own servers if they want to sponsor transactions. + +The technique in this tutorial only works when you control the smart contract. There are other techniques, including [account abstraction](https://eips.ethereum.org/EIPS/eip-4337) that let you sponsor transactions to other smart contracts, which I hope to cover in a future tutorial. + +Note: This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Learn more in the [vulnerabilities section of this guide](#vulnerabilities). + +### అవసరాలు {#prerequisites} + +To understand this tutorial you need to already be familiar with: + +- Solidity +- జావాస్క్రిప్ట్ +- React and WAGMI. If you are not familiar with these user interface tools, [we have a tutorial for that](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +## The sample application {#sample-app} + +The sample application here is a variant on Hardhat's `Greeter` contract. You can see it [on GitHub](https://github.com/qbzzt/260301-gasless). The smart contract is already deployed on the [Sepolia](https://sepolia.dev/), at address [`0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA`](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA). + +To see it in action, follow these steps. + +1. Clone the repository and install the necessary software. + + ```sh + git clone https://github.com/qbzzt/260301-gasless.git + cd 260301-gasless/server + npm install + ``` + +2. Edit `.env` to set `PRIVATE_KEY` to a wallet that has ETH on Sepolia. If you need Sepolia ETH, [use a faucet](/developers/docs/networks/#sepolia). Ideally, this private key should be different from the one you have in your browser wallet. + +3. సర్వర్‌ను ప్రారంభించండి. + + ```sh + npm run dev + ``` + +4. Browse to the application at URL [`http://localhost:5173`](http://localhost:5173). + +5. Click **Connect with Injected** to connect to a wallet. Approve in the wallet, and approve the change to Sepolia if necessary. + +6. Write a new greeting and click **Update greeting via sponsor**. + +7. Sign the message. + +8. Wait about 12 seconds (the block time on Sepolia). While waiting you can look at the URL in the server's console to see the transaction. + +9. See that the greeting changed, and that the last updated by address value is now the address of your browser wallet. + +To understand how this works, we need to look at how the message gets created in the user interface, how it is relayed by the server, and how the smart contract processes it. + +### The user interface {#ui-changes} + +The user interface is based on [WAGMI](https://wagmi.sh/); you can read about it [in this tutorial](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +Here is how we sign the message: + +```js +const signGreeting = useCallback( +``` + +The React hook [`useCallback`](https://react.dev/reference/react/useCallback) lets us improve performance by reusing the same function when the component is redrawn. + +```js + async (greeting) => { + if (!account) throw new Error("Wallet not connected") +``` + +If there is no account, raise an error. This should never happen because the UI button that starts the process that calls `signGreeting` is disabled in that case. However, future programmers may remove that safeguard, so it's a good idea to check this condition here as well. + +```js + const domain = { + name: "Greeter", + version: "1", + chainId, + verifyingContract: contractAddr, + } +``` + +Parameters for the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator). This value is constant, so in a better-optimized implementation, we might calculate it once rather than recalculate it each time the function is called. + +- `name` is a user-readable name, such as the name of the dapp for which we are producing signatures. +- `version` is the version. Different versions are not compatible. +- `chainId` is the chain we are using, as provided [by WAGMI](https://wagmi.sh/react/api/hooks/useChainId). +- `verifyingContract` is the contract address that will verify this signature. We do not want the same signature to apply to multiple contracts, in case there are several `Greeter` contracts and we want them to have different greetings. + +```js + + const types = { + GreetingRequest: [ + { name: "greeting", type: "string" }, + ], + } +``` + +The data type we sign. Here, we have a single parameter, `greeting`, but real-life systems typically have more. + +```js + const message = { greeting } +``` + +The actual message we want to sign and send. `greeting` is both the field name and the name of the variable that fills it. + +```js + const signature = await signTypedDataAsync({ + domain, + types, + primaryType: "GreetingRequest", + message, + }) +``` + +Actually get the signature. This function is asynchronous because users take a long time (from a computer's perspective) to sign data. + +```js + const r = `0x${signature.slice(2, 66)}` + const s = `0x${signature.slice(66, 130)}` + const v = parseInt(signature.slice(130, 132), 16) + + return { + req: { greeting }, + v, + r, + s, + } + }, +``` + +The function returns a single hexadecimal value. Here we divide it into fields. + +```js + [account, chainId, contractAddr, signTypedDataAsync], +) +``` + +If any of these variables change, create a new instance of the function. The `account` and `chainId` parameters can be changed by the user in the wallet. `contractAddr` is a function of the chain Id. `signTypedDataAsync` should not change, but we import it from [a hook](https://wagmi.sh/react/api/hooks/useSignTypedData), so we can't be sure, and it's best to add it here. + +Now that the new greeting is signed, we need to send it to the server. + +```js + const sponsoredGreeting = async () => { + try { +``` + +This function takes a signature and sends it to the server. + +```js + const signedMessage = await signGreeting(newGreeting) + const response = await fetch("/server/sponsor", { +``` + +Send to the path `/server/sponsor` in the server we came from. + +```js + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signedMessage), + }) +``` + +Use `POST` to send the information JSON-encoded. + +```js + const data = await response.json() + console.log("Server response:", data) + } catch (err) { + console.error("Error:", err) + } + } +``` + +Output the response. On a production system we'd also show the response to the user. + +### The server {#server} + +I like using [Vite](https://vite.dev/) as my front-end. It automatically serves the React libraries and updates the browser when the front-end code changes. However, Vite does not include backend tooling. + +The solution is in [`index.js`](https://github.com/qbzzt/260301-gasless/blob/main/server/index.js). + +```js + app.post("/server/sponsor", async (req, res) => { + ... + }) + + // Let Vite handle everything else + const vite = await createViteServer({ + server: { middlewareMode: true } + }) + + app.use(vite.middlewares) +``` + +First we register a handler for the requests we handle ourselves (`POST` to `/server/sponsor`). Then we create and use a Vite server to handle all other URLs. + +```js + app.post("/server/sponsor", async (req, res) => { + try { + const signed = req.body + + const txHash = await sepoliaClient.writeContract({ + address: greeterAddr, + abi: greeterABI, + functionName: 'sponsoredSetGreeting', + args: [signed.req, signed.v, signed.r, signed.s], + }) + } ... + }) +``` + +This is just a standard [viem](https://viem.sh/) blockchain call. + +### The smart contract {#smart-contract} + +Finally, [`Greeter.sol`](https://github.com/qbzzt/260301-gasless/blob/main/contracts/src/Greeter.sol) needs to verify the signature. + +```solidity + constructor(string memory _greeting) { + greeting = _greeting; + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("Greeter")), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } +``` + +The constructor creates the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator), similar to the user interface code above. Blockchain execution is much more expensive, so we only calculate it once. + +```solidity + struct GreetingRequest { + string greeting; + } +``` + +This is the structure that gets signed. Here we have just one field. + +```solidity + bytes32 private constant GREETING_TYPEHASH = + keccak256("GreetingRequest(string greeting)"); +``` + +This is the [structure identifier](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct). It is calculated each time in the user interface. + +```solidity + function sponsoredSetGreeting( + GreetingRequest calldata req, + uint8 v, + bytes32 r, + bytes32 s + ) external { +``` + +This function receives a signed request and updates the greeting. + +```solidity + // Compute EIP-712 digest + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + GREETING_TYPEHASH, + keccak256(bytes(req.greeting)) + ) + ) + ) + ); +``` + +Create the digest in accordance with [EIP 712](https://eips.ethereum.org/EIPS/eip-712). + +```solidity + // Recover signer + address signer = ecrecover(digest, v, r, s); + require(signer != address(0), "Invalid signature"); +``` + +Use [`ecrecover`](https://www.evm.codes/precompiled?fork=osaka#0x01) to get the signer address. Note that a bad signature can still result in a valid address, just a random one. + +```solidity + // Apply greeting as if signer called it + greeting = req.greeting; + emit SetGreeting(signer, req.greeting); + } +``` + +Update the greeting. + +## Vulnerabilities {#vulnerabilities} + +This is _not_ production-level code. ఇది ముఖ్యమైన దాడులకు గురయ్యే అవకాశం ఉంది మరియు ప్రధాన ఫీచర్లు లేవు. Here are some, along with how to solve them. + +To see some of these attacks, click the buttons under the _Attacks_ heading and see what happens. For the **Invalid signature** button, check the server console to see the transaction response. + +### Denial of service on the server {#dos-on-server} + +The easiest attack is a [denial-of-service](https://en.wikipedia.org/wiki/Denial-of-service_attack) attack on the server. The server receives requests from anywhere on the Internet and based on those requests sends transactions. There is absolutely nothing preventing an attacker from issuing a bunch of signatures, valid or invalid. Each will cause a transaction. Eventually the server will run out of ETH to pay for gas. + +One solution to this problem is to limit the rate to one transaction per block. If the purpose is to show greetings to [externally owned accounts](/developers/docs/accounts/#key-differences), it does not matter what the greeting is in the middle of the block anyway. + +Another solution is to keep track of addresses and only allow signatures from valid customers. + +### Wrong greeting signatures {#wrong-greeting-sigs} + +When you click **Signature for wrong greeting**, you submit a valid signature for a specific address (`0xaA92c5d426430D4769c9E878C1333BDe3d689b3e`) and greeting (`Hello`). But it submits it with a different greeting. This confuses `ecrecover`, which changes the greeting but has the wrong address. + +To solve this problem, add the address to the [signed structure](https://github.com/qbzzt/260301-gasless/blob/main/server/src/Greeter.jsx#L122-L124). This way, the `ecrecover` random address won't match the address in the signature, and the smart contract will reject the message. + +### Replay attacks {#replay-attack} + +When you click **Replay attack**, you submit the same "I'm 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, and I'd like the greeting to be `Hello`" signature, but with the correct greeting. As a result, the smart contract believes that the address (which isn't yours) changed the greeting back to `Hello`. The information to do this is publicly available in the [transaction information](https://eth-sepolia.blockscout.com/tx/0xa66afe4bbf886f59533e677a798c802ceab1ac0f9db6e83a4d4b59a45cf7c1b1). + +If this is a problem, one solution is to add a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). Have a [mapping](https://docs.soliditylang.org/en/latest/types.html#mapping-types) between addresses and numbers, and add a nonce field to the signature. If the nonce field matches the mapping for the address, accept the signature and increment the mapping for next time. If it doesn't, reject the transaction. + +Another solution is to add a timestamp to the signed data and accept the signature as valid only for a few seconds after that timestamp. This is simpler and cheaper, but we risk replay attacks within the time window, and the failure of legitimate transactions if the time window is exceeded. + +## Other missing features {#other-missing-features} + +There are additional features we would add in a production setting. + +### Access from other servers {#other-servers} + +Currently, we allow any address to submit a `sponsorSetGreeting`. This may be exactly what we want, in the interest of decentralization. Or maybe we want to ensure that sponsored transactions go through _our_ server, in which case we'd check `msg.sender` in the smart contract. + +Either way, this should be a conscious design decision, not just the result of not thinking about the issue. + +### Error handling {#error-handling} + +A user submits a greeting. Maybe it gets updated at the next block. Maybe it doesn't. Errors are invisible. On a production system, the user should be able to distinguish between these cases: + +- The new greeting has not been submitted yet +- The new greeting has been submitted, and it's in process +- The new greeting has been rejected + +## ముగింపు {#conclusion} + +At this point, you should be able to create a gasless experience for your dapp users, at the cost of some centralization. + +However, this only works with smart contracts that support ERC-712. To transfer an ERC-20 token, for example, it is necessary to have the transaction signed by the owner rather than just a message. The solution is [account abstraction (ERC-4337)](https://docs.erc4337.io/index.html). I hope to write a future tutorial about it. + +[నా మరిన్ని పనుల కోసం ఇక్కడ చూడండి](https://cryptodocguy.pro/). diff --git a/public/content/translations/tr/developers/tutorials/gasless/index.md b/public/content/translations/tr/developers/tutorials/gasless/index.md new file mode 100644 index 00000000000..058d380196c --- /dev/null +++ b/public/content/translations/tr/developers/tutorials/gasless/index.md @@ -0,0 +1,362 @@ +--- +title: "Sponsoring gas fees: How to cover transaction costs for your users" +description: It is easy to create a private key and an address; it's just a matter of running the right software. But there are many places in the world where getting the ETH to send transactions is much harder. In this tutorial you learn how to cover the onchain gas costs for executing user-signed, offchain structured data in your smart contract. You have the user sign a structure containing the transaction information, which your offchain code then submits to the blockchain as a transaction. +author: Ori Pomerantz +tags: [ "gasless", "katılık", "eip-712", "meta-transactions" ] +skill: intermediate +lang: tr +published: 2026-02-27 +--- + +## Giriş {#introduction} + +If we want Ethereum to serve [a billion more people](https://blog.ethereum.org/category/next-billion), we need to remove friction and make it as easy to use as possible. One source of this friction is the need for ETH to pay gas fees. + +If you have a dapp that makes money from users, it might make sense to let users submit transactions through your server and pay the transaction fees yourself. Because users still sign an [EIP-712 authorization message](https://eips.ethereum.org/EIPS/eip-712) in their wallets, they retain Ethereum's guarantees of integrity. Availability depends on the server that relays transactions, so it is more limited. However, you can set things up so users can also access the smart contract directly (if they get ETH), and let others set up their own servers if they want to sponsor transactions. + +The technique in this tutorial only works when you control the smart contract. There are other techniques, including [account abstraction](https://eips.ethereum.org/EIPS/eip-4337) that let you sponsor transactions to other smart contracts, which I hope to cover in a future tutorial. + +Note: This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Learn more in the [vulnerabilities section of this guide](#vulnerabilities). + +### Ön Koşullar {#prerequisites} + +To understand this tutorial you need to already be familiar with: + +- Solidity +- JavaScript +- React and WAGMI. If you are not familiar with these user interface tools, [we have a tutorial for that](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +## The sample application {#sample-app} + +The sample application here is a variant on Hardhat's `Greeter` contract. You can see it [on GitHub](https://github.com/qbzzt/260301-gasless). The smart contract is already deployed on the [Sepolia](https://sepolia.dev/), at address [`0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA`](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA). + +To see it in action, follow these steps. + +1. Clone the repository and install the necessary software. + + ```sh + git clone https://github.com/qbzzt/260301-gasless.git + cd 260301-gasless/server + npm install + ``` + +2. Edit `.env` to set `PRIVATE_KEY` to a wallet that has ETH on Sepolia. If you need Sepolia ETH, [use a faucet](/developers/docs/networks/#sepolia). Ideally, this private key should be different from the one you have in your browser wallet. + +3. Sunucuyu başlatın. + + ```sh + npm run dev + ``` + +4. Browse to the application at URL [`http://localhost:5173`](http://localhost:5173). + +5. Click **Connect with Injected** to connect to a wallet. Approve in the wallet, and approve the change to Sepolia if necessary. + +6. Write a new greeting and click **Update greeting via sponsor**. + +7. Sign the message. + +8. Wait about 12 seconds (the block time on Sepolia). While waiting you can look at the URL in the server's console to see the transaction. + +9. See that the greeting changed, and that the last updated by address value is now the address of your browser wallet. + +To understand how this works, we need to look at how the message gets created in the user interface, how it is relayed by the server, and how the smart contract processes it. + +### The user interface {#ui-changes} + +The user interface is based on [WAGMI](https://wagmi.sh/); you can read about it [in this tutorial](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +Here is how we sign the message: + +```js +const signGreeting = useCallback( +``` + +The React hook [`useCallback`](https://react.dev/reference/react/useCallback) lets us improve performance by reusing the same function when the component is redrawn. + +```js + async (greeting) => { + if (!account) throw new Error("Wallet not connected") +``` + +If there is no account, raise an error. This should never happen because the UI button that starts the process that calls `signGreeting` is disabled in that case. However, future programmers may remove that safeguard, so it's a good idea to check this condition here as well. + +```js + const domain = { + name: "Greeter", + version: "1", + chainId, + verifyingContract: contractAddr, + } +``` + +Parameters for the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator). This value is constant, so in a better-optimized implementation, we might calculate it once rather than recalculate it each time the function is called. + +- `name` is a user-readable name, such as the name of the dapp for which we are producing signatures. +- `version` is the version. Different versions are not compatible. +- `chainId` is the chain we are using, as provided [by WAGMI](https://wagmi.sh/react/api/hooks/useChainId). +- `verifyingContract` is the contract address that will verify this signature. We do not want the same signature to apply to multiple contracts, in case there are several `Greeter` contracts and we want them to have different greetings. + +```js + + const types = { + GreetingRequest: [ + { name: "greeting", type: "string" }, + ], + } +``` + +The data type we sign. Here, we have a single parameter, `greeting`, but real-life systems typically have more. + +```js + const message = { greeting } +``` + +The actual message we want to sign and send. `greeting` is both the field name and the name of the variable that fills it. + +```js + const signature = await signTypedDataAsync({ + domain, + types, + primaryType: "GreetingRequest", + message, + }) +``` + +Actually get the signature. This function is asynchronous because users take a long time (from a computer's perspective) to sign data. + +```js + const r = `0x${signature.slice(2, 66)}` + const s = `0x${signature.slice(66, 130)}` + const v = parseInt(signature.slice(130, 132), 16) + + return { + req: { greeting }, + v, + r, + s, + } + }, +``` + +The function returns a single hexadecimal value. Here we divide it into fields. + +```js + [account, chainId, contractAddr, signTypedDataAsync], +) +``` + +If any of these variables change, create a new instance of the function. The `account` and `chainId` parameters can be changed by the user in the wallet. `contractAddr` is a function of the chain Id. `signTypedDataAsync` should not change, but we import it from [a hook](https://wagmi.sh/react/api/hooks/useSignTypedData), so we can't be sure, and it's best to add it here. + +Now that the new greeting is signed, we need to send it to the server. + +```js + const sponsoredGreeting = async () => { + try { +``` + +This function takes a signature and sends it to the server. + +```js + const signedMessage = await signGreeting(newGreeting) + const response = await fetch("/server/sponsor", { +``` + +Send to the path `/server/sponsor` in the server we came from. + +```js + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signedMessage), + }) +``` + +Use `POST` to send the information JSON-encoded. + +```js + const data = await response.json() + console.log("Server response:", data) + } catch (err) { + console.error("Error:", err) + } + } +``` + +Output the response. On a production system we'd also show the response to the user. + +### The server {#server} + +I like using [Vite](https://vite.dev/) as my front-end. It automatically serves the React libraries and updates the browser when the front-end code changes. However, Vite does not include backend tooling. + +The solution is in [`index.js`](https://github.com/qbzzt/260301-gasless/blob/main/server/index.js). + +```js + app.post("/server/sponsor", async (req, res) => { + ... + }) + + // Let Vite handle everything else + const vite = await createViteServer({ + server: { middlewareMode: true } + }) + + app.use(vite.middlewares) +``` + +First we register a handler for the requests we handle ourselves (`POST` to `/server/sponsor`). Then we create and use a Vite server to handle all other URLs. + +```js + app.post("/server/sponsor", async (req, res) => { + try { + const signed = req.body + + const txHash = await sepoliaClient.writeContract({ + address: greeterAddr, + abi: greeterABI, + functionName: 'sponsoredSetGreeting', + args: [signed.req, signed.v, signed.r, signed.s], + }) + } ... + }) +``` + +This is just a standard [viem](https://viem.sh/) blockchain call. + +### The smart contract {#smart-contract} + +Finally, [`Greeter.sol`](https://github.com/qbzzt/260301-gasless/blob/main/contracts/src/Greeter.sol) needs to verify the signature. + +```solidity + constructor(string memory _greeting) { + greeting = _greeting; + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("Greeter")), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } +``` + +The constructor creates the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator), similar to the user interface code above. Blockchain execution is much more expensive, so we only calculate it once. + +```solidity + struct GreetingRequest { + string greeting; + } +``` + +This is the structure that gets signed. Here we have just one field. + +```solidity + bytes32 private constant GREETING_TYPEHASH = + keccak256("GreetingRequest(string greeting)"); +``` + +This is the [structure identifier](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct). It is calculated each time in the user interface. + +```solidity + function sponsoredSetGreeting( + GreetingRequest calldata req, + uint8 v, + bytes32 r, + bytes32 s + ) external { +``` + +This function receives a signed request and updates the greeting. + +```solidity + // Compute EIP-712 digest + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + GREETING_TYPEHASH, + keccak256(bytes(req.greeting)) + ) + ) + ) + ); +``` + +Create the digest in accordance with [EIP 712](https://eips.ethereum.org/EIPS/eip-712). + +```solidity + // Recover signer + address signer = ecrecover(digest, v, r, s); + require(signer != address(0), "Invalid signature"); +``` + +Use [`ecrecover`](https://www.evm.codes/precompiled?fork=osaka#0x01) to get the signer address. Note that a bad signature can still result in a valid address, just a random one. + +```solidity + // Apply greeting as if signer called it + greeting = req.greeting; + emit SetGreeting(signer, req.greeting); + } +``` + +Update the greeting. + +## Vulnerabilities {#vulnerabilities} + +This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Here are some, along with how to solve them. + +To see some of these attacks, click the buttons under the _Attacks_ heading and see what happens. For the **Invalid signature** button, check the server console to see the transaction response. + +### Denial of service on the server {#dos-on-server} + +The easiest attack is a [denial-of-service](https://en.wikipedia.org/wiki/Denial-of-service_attack) attack on the server. The server receives requests from anywhere on the Internet and based on those requests sends transactions. There is absolutely nothing preventing an attacker from issuing a bunch of signatures, valid or invalid. Each will cause a transaction. Eventually the server will run out of ETH to pay for gas. + +One solution to this problem is to limit the rate to one transaction per block. If the purpose is to show greetings to [externally owned accounts](/developers/docs/accounts/#key-differences), it does not matter what the greeting is in the middle of the block anyway. + +Another solution is to keep track of addresses and only allow signatures from valid customers. + +### Wrong greeting signatures {#wrong-greeting-sigs} + +When you click **Signature for wrong greeting**, you submit a valid signature for a specific address (`0xaA92c5d426430D4769c9E878C1333BDe3d689b3e`) and greeting (`Hello`). But it submits it with a different greeting. This confuses `ecrecover`, which changes the greeting but has the wrong address. + +To solve this problem, add the address to the [signed structure](https://github.com/qbzzt/260301-gasless/blob/main/server/src/Greeter.jsx#L122-L124). This way, the `ecrecover` random address won't match the address in the signature, and the smart contract will reject the message. + +### Replay attacks {#replay-attack} + +When you click **Replay attack**, you submit the same "I'm 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, and I'd like the greeting to be `Hello`" signature, but with the correct greeting. As a result, the smart contract believes that the address (which isn't yours) changed the greeting back to `Hello`. The information to do this is publicly available in the [transaction information](https://eth-sepolia.blockscout.com/tx/0xa66afe4bbf886f59533e677a798c802ceab1ac0f9db6e83a4d4b59a45cf7c1b1). + +If this is a problem, one solution is to add a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). Have a [mapping](https://docs.soliditylang.org/en/latest/types.html#mapping-types) between addresses and numbers, and add a nonce field to the signature. If the nonce field matches the mapping for the address, accept the signature and increment the mapping for next time. If it doesn't, reject the transaction. + +Another solution is to add a timestamp to the signed data and accept the signature as valid only for a few seconds after that timestamp. This is simpler and cheaper, but we risk replay attacks within the time window, and the failure of legitimate transactions if the time window is exceeded. + +## Other missing features {#other-missing-features} + +There are additional features we would add in a production setting. + +### Access from other servers {#other-servers} + +Currently, we allow any address to submit a `sponsorSetGreeting`. This may be exactly what we want, in the interest of decentralization. Or maybe we want to ensure that sponsored transactions go through _our_ server, in which case we'd check `msg.sender` in the smart contract. + +Either way, this should be a conscious design decision, not just the result of not thinking about the issue. + +### Error handling {#error-handling} + +A user submits a greeting. Maybe it gets updated at the next block. Maybe it doesn't. Errors are invisible. On a production system, the user should be able to distinguish between these cases: + +- The new greeting has not been submitted yet +- The new greeting has been submitted, and it's in process +- The new greeting has been rejected + +## Sonuç {#conclusion} + +At this point, you should be able to create a gasless experience for your dapp users, at the cost of some centralization. + +However, this only works with smart contracts that support ERC-712. To transfer an ERC-20 token, for example, it is necessary to have the transaction signed by the owner rather than just a message. The solution is [account abstraction (ERC-4337)](https://docs.erc4337.io/index.html). I hope to write a future tutorial about it. + +[Çalışmalarımdan daha fazlası için buraya bakın](https://cryptodocguy.pro/). diff --git a/public/content/translations/uk/developers/tutorials/gasless/index.md b/public/content/translations/uk/developers/tutorials/gasless/index.md new file mode 100644 index 00000000000..865e49ed895 --- /dev/null +++ b/public/content/translations/uk/developers/tutorials/gasless/index.md @@ -0,0 +1,368 @@ +--- +title: "Sponsoring gas fees: How to cover transaction costs for your users" +description: It is easy to create a private key and an address; it's just a matter of running the right software. But there are many places in the world where getting the ETH to send transactions is much harder. In this tutorial you learn how to cover the onchain gas costs for executing user-signed, offchain structured data in your smart contract. You have the user sign a structure containing the transaction information, which your offchain code then submits to the blockchain as a transaction. +author: "Орі Померанц" +tags: + [ + "gasless", + "мова програмування", + "eip-712", + "meta-transactions" + ] +skill: intermediate +lang: uk +published: 2026-02-27 +--- + +## Вступ {#introduction} + +If we want Ethereum to serve [a billion more people](https://blog.ethereum.org/category/next-billion), we need to remove friction and make it as easy to use as possible. One source of this friction is the need for ETH to pay gas fees. + +If you have a dapp that makes money from users, it might make sense to let users submit transactions through your server and pay the transaction fees yourself. Because users still sign an [EIP-712 authorization message](https://eips.ethereum.org/EIPS/eip-712) in their wallets, they retain Ethereum's guarantees of integrity. Availability depends on the server that relays transactions, so it is more limited. However, you can set things up so users can also access the smart contract directly (if they get ETH), and let others set up their own servers if they want to sponsor transactions. + +The technique in this tutorial only works when you control the smart contract. There are other techniques, including [account abstraction](https://eips.ethereum.org/EIPS/eip-4337) that let you sponsor transactions to other smart contracts, which I hope to cover in a future tutorial. + +Note: This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Learn more in the [vulnerabilities section of this guide](#vulnerabilities). + +### Передумови {#prerequisites} + +To understand this tutorial you need to already be familiar with: + +- Мова програмування Solidity +- JavaScript +- React and WAGMI. If you are not familiar with these user interface tools, [we have a tutorial for that](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +## The sample application {#sample-app} + +The sample application here is a variant on Hardhat's `Greeter` contract. You can see it [on GitHub](https://github.com/qbzzt/260301-gasless). The smart contract is already deployed on the [Sepolia](https://sepolia.dev/), at address [`0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA`](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA). + +To see it in action, follow these steps. + +1. Clone the repository and install the necessary software. + + ```sh + git clone https://github.com/qbzzt/260301-gasless.git + cd 260301-gasless/server + npm install + ``` + +2. Edit `.env` to set `PRIVATE_KEY` to a wallet that has ETH on Sepolia. If you need Sepolia ETH, [use a faucet](/developers/docs/networks/#sepolia). Ideally, this private key should be different from the one you have in your browser wallet. + +3. Запустіть сервер. + + ```sh + npm run dev + ``` + +4. Browse to the application at URL [`http://localhost:5173`](http://localhost:5173). + +5. Click **Connect with Injected** to connect to a wallet. Approve in the wallet, and approve the change to Sepolia if necessary. + +6. Write a new greeting and click **Update greeting via sponsor**. + +7. Sign the message. + +8. Wait about 12 seconds (the block time on Sepolia). While waiting you can look at the URL in the server's console to see the transaction. + +9. See that the greeting changed, and that the last updated by address value is now the address of your browser wallet. + +To understand how this works, we need to look at how the message gets created in the user interface, how it is relayed by the server, and how the smart contract processes it. + +### The user interface {#ui-changes} + +The user interface is based on [WAGMI](https://wagmi.sh/); you can read about it [in this tutorial](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +Here is how we sign the message: + +```js +const signGreeting = useCallback( +``` + +The React hook [`useCallback`](https://react.dev/reference/react/useCallback) lets us improve performance by reusing the same function when the component is redrawn. + +```js + async (greeting) => { + if (!account) throw new Error("Wallet not connected") +``` + +If there is no account, raise an error. This should never happen because the UI button that starts the process that calls `signGreeting` is disabled in that case. However, future programmers may remove that safeguard, so it's a good idea to check this condition here as well. + +```js + const domain = { + name: "Greeter", + version: "1", + chainId, + verifyingContract: contractAddr, + } +``` + +Parameters for the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator). This value is constant, so in a better-optimized implementation, we might calculate it once rather than recalculate it each time the function is called. + +- `name` is a user-readable name, such as the name of the dapp for which we are producing signatures. +- `version` is the version. Different versions are not compatible. +- `chainId` is the chain we are using, as provided [by WAGMI](https://wagmi.sh/react/api/hooks/useChainId). +- `verifyingContract` is the contract address that will verify this signature. We do not want the same signature to apply to multiple contracts, in case there are several `Greeter` contracts and we want them to have different greetings. + +```js + + const types = { + GreetingRequest: [ + { name: "greeting", type: "string" }, + ], + } +``` + +The data type we sign. Here, we have a single parameter, `greeting`, but real-life systems typically have more. + +```js + const message = { greeting } +``` + +The actual message we want to sign and send. `greeting` is both the field name and the name of the variable that fills it. + +```js + const signature = await signTypedDataAsync({ + domain, + types, + primaryType: "GreetingRequest", + message, + }) +``` + +Actually get the signature. This function is asynchronous because users take a long time (from a computer's perspective) to sign data. + +```js + const r = `0x${signature.slice(2, 66)}` + const s = `0x${signature.slice(66, 130)}` + const v = parseInt(signature.slice(130, 132), 16) + + return { + req: { greeting }, + v, + r, + s, + } + }, +``` + +The function returns a single hexadecimal value. Here we divide it into fields. + +```js + [account, chainId, contractAddr, signTypedDataAsync], +) +``` + +If any of these variables change, create a new instance of the function. The `account` and `chainId` parameters can be changed by the user in the wallet. `contractAddr` is a function of the chain Id. `signTypedDataAsync` should not change, but we import it from [a hook](https://wagmi.sh/react/api/hooks/useSignTypedData), so we can't be sure, and it's best to add it here. + +Now that the new greeting is signed, we need to send it to the server. + +```js + const sponsoredGreeting = async () => { + try { +``` + +This function takes a signature and sends it to the server. + +```js + const signedMessage = await signGreeting(newGreeting) + const response = await fetch("/server/sponsor", { +``` + +Send to the path `/server/sponsor` in the server we came from. + +```js + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signedMessage), + }) +``` + +Use `POST` to send the information JSON-encoded. + +```js + const data = await response.json() + console.log("Server response:", data) + } catch (err) { + console.error("Error:", err) + } + } +``` + +Output the response. On a production system we'd also show the response to the user. + +### The server {#server} + +I like using [Vite](https://vite.dev/) as my front-end. It automatically serves the React libraries and updates the browser when the front-end code changes. However, Vite does not include backend tooling. + +The solution is in [`index.js`](https://github.com/qbzzt/260301-gasless/blob/main/server/index.js). + +```js + app.post("/server/sponsor", async (req, res) => { + ... + }) + + // Let Vite handle everything else + const vite = await createViteServer({ + server: { middlewareMode: true } + }) + + app.use(vite.middlewares) +``` + +First we register a handler for the requests we handle ourselves (`POST` to `/server/sponsor`). Then we create and use a Vite server to handle all other URLs. + +```js + app.post("/server/sponsor", async (req, res) => { + try { + const signed = req.body + + const txHash = await sepoliaClient.writeContract({ + address: greeterAddr, + abi: greeterABI, + functionName: 'sponsoredSetGreeting', + args: [signed.req, signed.v, signed.r, signed.s], + }) + } ... + }) +``` + +This is just a standard [viem](https://viem.sh/) blockchain call. + +### The smart contract {#smart-contract} + +Finally, [`Greeter.sol`](https://github.com/qbzzt/260301-gasless/blob/main/contracts/src/Greeter.sol) needs to verify the signature. + +```solidity + constructor(string memory _greeting) { + greeting = _greeting; + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("Greeter")), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } +``` + +The constructor creates the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator), similar to the user interface code above. Blockchain execution is much more expensive, so we only calculate it once. + +```solidity + struct GreetingRequest { + string greeting; + } +``` + +This is the structure that gets signed. Here we have just one field. + +```solidity + bytes32 private constant GREETING_TYPEHASH = + keccak256("GreetingRequest(string greeting)"); +``` + +This is the [structure identifier](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct). It is calculated each time in the user interface. + +```solidity + function sponsoredSetGreeting( + GreetingRequest calldata req, + uint8 v, + bytes32 r, + bytes32 s + ) external { +``` + +This function receives a signed request and updates the greeting. + +```solidity + // Compute EIP-712 digest + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + GREETING_TYPEHASH, + keccak256(bytes(req.greeting)) + ) + ) + ) + ); +``` + +Create the digest in accordance with [EIP 712](https://eips.ethereum.org/EIPS/eip-712). + +```solidity + // Recover signer + address signer = ecrecover(digest, v, r, s); + require(signer != address(0), "Invalid signature"); +``` + +Use [`ecrecover`](https://www.evm.codes/precompiled?fork=osaka#0x01) to get the signer address. Note that a bad signature can still result in a valid address, just a random one. + +```solidity + // Apply greeting as if signer called it + greeting = req.greeting; + emit SetGreeting(signer, req.greeting); + } +``` + +Update the greeting. + +## Vulnerabilities {#vulnerabilities} + +This is _not_ production-level code. Він вразливий до серйозних атак і не має ключових функцій. Here are some, along with how to solve them. + +To see some of these attacks, click the buttons under the _Attacks_ heading and see what happens. For the **Invalid signature** button, check the server console to see the transaction response. + +### Denial of service on the server {#dos-on-server} + +The easiest attack is a [denial-of-service](https://en.wikipedia.org/wiki/Denial-of-service_attack) attack on the server. The server receives requests from anywhere on the Internet and based on those requests sends transactions. There is absolutely nothing preventing an attacker from issuing a bunch of signatures, valid or invalid. Each will cause a transaction. Eventually the server will run out of ETH to pay for gas. + +One solution to this problem is to limit the rate to one transaction per block. If the purpose is to show greetings to [externally owned accounts](/developers/docs/accounts/#key-differences), it does not matter what the greeting is in the middle of the block anyway. + +Another solution is to keep track of addresses and only allow signatures from valid customers. + +### Wrong greeting signatures {#wrong-greeting-sigs} + +When you click **Signature for wrong greeting**, you submit a valid signature for a specific address (`0xaA92c5d426430D4769c9E878C1333BDe3d689b3e`) and greeting (`Hello`). But it submits it with a different greeting. This confuses `ecrecover`, which changes the greeting but has the wrong address. + +To solve this problem, add the address to the [signed structure](https://github.com/qbzzt/260301-gasless/blob/main/server/src/Greeter.jsx#L122-L124). This way, the `ecrecover` random address won't match the address in the signature, and the smart contract will reject the message. + +### Replay attacks {#replay-attack} + +When you click **Replay attack**, you submit the same "I'm 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, and I'd like the greeting to be `Hello`" signature, but with the correct greeting. As a result, the smart contract believes that the address (which isn't yours) changed the greeting back to `Hello`. The information to do this is publicly available in the [transaction information](https://eth-sepolia.blockscout.com/tx/0xa66afe4bbf886f59533e677a798c802ceab1ac0f9db6e83a4d4b59a45cf7c1b1). + +If this is a problem, one solution is to add a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). Have a [mapping](https://docs.soliditylang.org/en/latest/types.html#mapping-types) between addresses and numbers, and add a nonce field to the signature. If the nonce field matches the mapping for the address, accept the signature and increment the mapping for next time. If it doesn't, reject the transaction. + +Another solution is to add a timestamp to the signed data and accept the signature as valid only for a few seconds after that timestamp. This is simpler and cheaper, but we risk replay attacks within the time window, and the failure of legitimate transactions if the time window is exceeded. + +## Other missing features {#other-missing-features} + +There are additional features we would add in a production setting. + +### Access from other servers {#other-servers} + +Currently, we allow any address to submit a `sponsorSetGreeting`. This may be exactly what we want, in the interest of decentralization. Or maybe we want to ensure that sponsored transactions go through _our_ server, in which case we'd check `msg.sender` in the smart contract. + +Either way, this should be a conscious design decision, not just the result of not thinking about the issue. + +### Error handling {#error-handling} + +A user submits a greeting. Maybe it gets updated at the next block. Maybe it doesn't. Errors are invisible. On a production system, the user should be able to distinguish between these cases: + +- The new greeting has not been submitted yet +- The new greeting has been submitted, and it's in process +- The new greeting has been rejected + +## Висновок {#conclusion} + +At this point, you should be able to create a gasless experience for your dapp users, at the cost of some centralization. + +However, this only works with smart contracts that support ERC-712. To transfer an ERC-20 token, for example, it is necessary to have the transaction signed by the owner rather than just a message. The solution is [account abstraction (ERC-4337)](https://docs.erc4337.io/index.html). I hope to write a future tutorial about it. + +[Більше моїх робіт дивіться тут](https://cryptodocguy.pro/). diff --git a/public/content/translations/ur/developers/tutorials/gasless/index.md b/public/content/translations/ur/developers/tutorials/gasless/index.md new file mode 100644 index 00000000000..adfd50ba7da --- /dev/null +++ b/public/content/translations/ur/developers/tutorials/gasless/index.md @@ -0,0 +1,362 @@ +--- +title: "Sponsoring gas fees: How to cover transaction costs for your users" +description: It is easy to create a private key and an address; it's just a matter of running the right software. But there are many places in the world where getting the ETH to send transactions is much harder. In this tutorial you learn how to cover the onchain gas costs for executing user-signed, offchain structured data in your smart contract. You have the user sign a structure containing the transaction information, which your offchain code then submits to the blockchain as a transaction. +author: "اوری پومیرانٹز" +tags: [ "gasless", "solidity", "eip-712", "meta-transactions" ] +skill: intermediate +lang: ur-in +published: 2026-02-27 +--- + +## تعارف {#introduction} + +If we want Ethereum to serve [a billion more people](https://blog.ethereum.org/category/next-billion), we need to remove friction and make it as easy to use as possible. One source of this friction is the need for ETH to pay gas fees. + +If you have a dapp that makes money from users, it might make sense to let users submit transactions through your server and pay the transaction fees yourself. Because users still sign an [EIP-712 authorization message](https://eips.ethereum.org/EIPS/eip-712) in their wallets, they retain Ethereum's guarantees of integrity. Availability depends on the server that relays transactions, so it is more limited. However, you can set things up so users can also access the smart contract directly (if they get ETH), and let others set up their own servers if they want to sponsor transactions. + +The technique in this tutorial only works when you control the smart contract. There are other techniques, including [account abstraction](https://eips.ethereum.org/EIPS/eip-4337) that let you sponsor transactions to other smart contracts, which I hope to cover in a future tutorial. + +Note: This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Learn more in the [vulnerabilities section of this guide](#vulnerabilities). + +### شرائط {#prerequisites} + +To understand this tutorial you need to already be familiar with: + +- Solidity +- JavaScript +- React and WAGMI. If you are not familiar with these user interface tools, [we have a tutorial for that](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +## The sample application {#sample-app} + +The sample application here is a variant on Hardhat's `Greeter` contract. You can see it [on GitHub](https://github.com/qbzzt/260301-gasless). The smart contract is already deployed on the [Sepolia](https://sepolia.dev/), at address [`0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA`](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA). + +To see it in action, follow these steps. + +1. Clone the repository and install the necessary software. + + ```sh + git clone https://github.com/qbzzt/260301-gasless.git + cd 260301-gasless/server + npm install + ``` + +2. Edit `.env` to set `PRIVATE_KEY` to a wallet that has ETH on Sepolia. If you need Sepolia ETH, [use a faucet](/developers/docs/networks/#sepolia). Ideally, this private key should be different from the one you have in your browser wallet. + +3. سرور شروع کریں۔ + + ```sh + npm run dev + ``` + +4. Browse to the application at URL [`http://localhost:5173`](http://localhost:5173). + +5. Click **Connect with Injected** to connect to a wallet. Approve in the wallet, and approve the change to Sepolia if necessary. + +6. Write a new greeting and click **Update greeting via sponsor**. + +7. Sign the message. + +8. Wait about 12 seconds (the block time on Sepolia). While waiting you can look at the URL in the server's console to see the transaction. + +9. See that the greeting changed, and that the last updated by address value is now the address of your browser wallet. + +To understand how this works, we need to look at how the message gets created in the user interface, how it is relayed by the server, and how the smart contract processes it. + +### The user interface {#ui-changes} + +The user interface is based on [WAGMI](https://wagmi.sh/); you can read about it [in this tutorial](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +Here is how we sign the message: + +```js +const signGreeting = useCallback( +``` + +The React hook [`useCallback`](https://react.dev/reference/react/useCallback) lets us improve performance by reusing the same function when the component is redrawn. + +```js + async (greeting) => { + if (!account) throw new Error("Wallet not connected") +``` + +If there is no account, raise an error. This should never happen because the UI button that starts the process that calls `signGreeting` is disabled in that case. However, future programmers may remove that safeguard, so it's a good idea to check this condition here as well. + +```js + const domain = { + name: "Greeter", + version: "1", + chainId, + verifyingContract: contractAddr, + } +``` + +Parameters for the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator). This value is constant, so in a better-optimized implementation, we might calculate it once rather than recalculate it each time the function is called. + +- `name` is a user-readable name, such as the name of the dapp for which we are producing signatures. +- `version` is the version. Different versions are not compatible. +- `chainId` is the chain we are using, as provided [by WAGMI](https://wagmi.sh/react/api/hooks/useChainId). +- `verifyingContract` is the contract address that will verify this signature. We do not want the same signature to apply to multiple contracts, in case there are several `Greeter` contracts and we want them to have different greetings. + +```js + + const types = { + GreetingRequest: [ + { name: "greeting", type: "string" }, + ], + } +``` + +The data type we sign. Here, we have a single parameter, `greeting`, but real-life systems typically have more. + +```js + const message = { greeting } +``` + +The actual message we want to sign and send. `greeting` is both the field name and the name of the variable that fills it. + +```js + const signature = await signTypedDataAsync({ + domain, + types, + primaryType: "GreetingRequest", + message, + }) +``` + +Actually get the signature. This function is asynchronous because users take a long time (from a computer's perspective) to sign data. + +```js + const r = `0x${signature.slice(2, 66)}` + const s = `0x${signature.slice(66, 130)}` + const v = parseInt(signature.slice(130, 132), 16) + + return { + req: { greeting }, + v, + r, + s, + } + }, +``` + +The function returns a single hexadecimal value. Here we divide it into fields. + +```js + [account, chainId, contractAddr, signTypedDataAsync], +) +``` + +If any of these variables change, create a new instance of the function. The `account` and `chainId` parameters can be changed by the user in the wallet. `contractAddr` is a function of the chain Id. `signTypedDataAsync` should not change, but we import it from [a hook](https://wagmi.sh/react/api/hooks/useSignTypedData), so we can't be sure, and it's best to add it here. + +Now that the new greeting is signed, we need to send it to the server. + +```js + const sponsoredGreeting = async () => { + try { +``` + +This function takes a signature and sends it to the server. + +```js + const signedMessage = await signGreeting(newGreeting) + const response = await fetch("/server/sponsor", { +``` + +Send to the path `/server/sponsor` in the server we came from. + +```js + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signedMessage), + }) +``` + +Use `POST` to send the information JSON-encoded. + +```js + const data = await response.json() + console.log("Server response:", data) + } catch (err) { + console.error("Error:", err) + } + } +``` + +Output the response. On a production system we'd also show the response to the user. + +### The server {#server} + +I like using [Vite](https://vite.dev/) as my front-end. It automatically serves the React libraries and updates the browser when the front-end code changes. However, Vite does not include backend tooling. + +The solution is in [`index.js`](https://github.com/qbzzt/260301-gasless/blob/main/server/index.js). + +```js + app.post("/server/sponsor", async (req, res) => { + ... + }) + + // Let Vite handle everything else + const vite = await createViteServer({ + server: { middlewareMode: true } + }) + + app.use(vite.middlewares) +``` + +First we register a handler for the requests we handle ourselves (`POST` to `/server/sponsor`). Then we create and use a Vite server to handle all other URLs. + +```js + app.post("/server/sponsor", async (req, res) => { + try { + const signed = req.body + + const txHash = await sepoliaClient.writeContract({ + address: greeterAddr, + abi: greeterABI, + functionName: 'sponsoredSetGreeting', + args: [signed.req, signed.v, signed.r, signed.s], + }) + } ... + }) +``` + +This is just a standard [viem](https://viem.sh/) blockchain call. + +### The smart contract {#smart-contract} + +Finally, [`Greeter.sol`](https://github.com/qbzzt/260301-gasless/blob/main/contracts/src/Greeter.sol) needs to verify the signature. + +```solidity + constructor(string memory _greeting) { + greeting = _greeting; + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("Greeter")), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } +``` + +The constructor creates the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator), similar to the user interface code above. Blockchain execution is much more expensive, so we only calculate it once. + +```solidity + struct GreetingRequest { + string greeting; + } +``` + +This is the structure that gets signed. Here we have just one field. + +```solidity + bytes32 private constant GREETING_TYPEHASH = + keccak256("GreetingRequest(string greeting)"); +``` + +This is the [structure identifier](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct). It is calculated each time in the user interface. + +```solidity + function sponsoredSetGreeting( + GreetingRequest calldata req, + uint8 v, + bytes32 r, + bytes32 s + ) external { +``` + +This function receives a signed request and updates the greeting. + +```solidity + // Compute EIP-712 digest + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + GREETING_TYPEHASH, + keccak256(bytes(req.greeting)) + ) + ) + ) + ); +``` + +Create the digest in accordance with [EIP 712](https://eips.ethereum.org/EIPS/eip-712). + +```solidity + // Recover signer + address signer = ecrecover(digest, v, r, s); + require(signer != address(0), "Invalid signature"); +``` + +Use [`ecrecover`](https://www.evm.codes/precompiled?fork=osaka#0x01) to get the signer address. Note that a bad signature can still result in a valid address, just a random one. + +```solidity + // Apply greeting as if signer called it + greeting = req.greeting; + emit SetGreeting(signer, req.greeting); + } +``` + +Update the greeting. + +## Vulnerabilities {#vulnerabilities} + +This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Here are some, along with how to solve them. + +To see some of these attacks, click the buttons under the _Attacks_ heading and see what happens. For the **Invalid signature** button, check the server console to see the transaction response. + +### Denial of service on the server {#dos-on-server} + +The easiest attack is a [denial-of-service](https://en.wikipedia.org/wiki/Denial-of-service_attack) attack on the server. The server receives requests from anywhere on the Internet and based on those requests sends transactions. There is absolutely nothing preventing an attacker from issuing a bunch of signatures, valid or invalid. Each will cause a transaction. Eventually the server will run out of ETH to pay for gas. + +One solution to this problem is to limit the rate to one transaction per block. If the purpose is to show greetings to [externally owned accounts](/developers/docs/accounts/#key-differences), it does not matter what the greeting is in the middle of the block anyway. + +Another solution is to keep track of addresses and only allow signatures from valid customers. + +### Wrong greeting signatures {#wrong-greeting-sigs} + +When you click **Signature for wrong greeting**, you submit a valid signature for a specific address (`0xaA92c5d426430D4769c9E878C1333BDe3d689b3e`) and greeting (`Hello`). But it submits it with a different greeting. This confuses `ecrecover`, which changes the greeting but has the wrong address. + +To solve this problem, add the address to the [signed structure](https://github.com/qbzzt/260301-gasless/blob/main/server/src/Greeter.jsx#L122-L124). This way, the `ecrecover` random address won't match the address in the signature, and the smart contract will reject the message. + +### Replay attacks {#replay-attack} + +When you click **Replay attack**, you submit the same "I'm 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, and I'd like the greeting to be `Hello`" signature, but with the correct greeting. As a result, the smart contract believes that the address (which isn't yours) changed the greeting back to `Hello`. The information to do this is publicly available in the [transaction information](https://eth-sepolia.blockscout.com/tx/0xa66afe4bbf886f59533e677a798c802ceab1ac0f9db6e83a4d4b59a45cf7c1b1). + +If this is a problem, one solution is to add a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). Have a [mapping](https://docs.soliditylang.org/en/latest/types.html#mapping-types) between addresses and numbers, and add a nonce field to the signature. If the nonce field matches the mapping for the address, accept the signature and increment the mapping for next time. If it doesn't, reject the transaction. + +Another solution is to add a timestamp to the signed data and accept the signature as valid only for a few seconds after that timestamp. This is simpler and cheaper, but we risk replay attacks within the time window, and the failure of legitimate transactions if the time window is exceeded. + +## Other missing features {#other-missing-features} + +There are additional features we would add in a production setting. + +### Access from other servers {#other-servers} + +Currently, we allow any address to submit a `sponsorSetGreeting`. This may be exactly what we want, in the interest of decentralization. Or maybe we want to ensure that sponsored transactions go through _our_ server, in which case we'd check `msg.sender` in the smart contract. + +Either way, this should be a conscious design decision, not just the result of not thinking about the issue. + +### Error handling {#error-handling} + +A user submits a greeting. Maybe it gets updated at the next block. Maybe it doesn't. Errors are invisible. On a production system, the user should be able to distinguish between these cases: + +- The new greeting has not been submitted yet +- The new greeting has been submitted, and it's in process +- The new greeting has been rejected + +## نتیجہ {#conclusion} + +At this point, you should be able to create a gasless experience for your dapp users, at the cost of some centralization. + +However, this only works with smart contracts that support ERC-712. To transfer an ERC-20 token, for example, it is necessary to have the transaction signed by the owner rather than just a message. The solution is [account abstraction (ERC-4337)](https://docs.erc4337.io/index.html). I hope to write a future tutorial about it. + +[میرے مزید کام کے لیے یہاں دیکھیں](https://cryptodocguy.pro/)۔ diff --git a/public/content/translations/vi/developers/tutorials/gasless/index.md b/public/content/translations/vi/developers/tutorials/gasless/index.md new file mode 100644 index 00000000000..00d68dc55db --- /dev/null +++ b/public/content/translations/vi/developers/tutorials/gasless/index.md @@ -0,0 +1,362 @@ +--- +title: "Sponsoring gas fees: How to cover transaction costs for your users" +description: It is easy to create a private key and an address; it's just a matter of running the right software. But there are many places in the world where getting the ETH to send transactions is much harder. In this tutorial you learn how to cover the onchain gas costs for executing user-signed, offchain structured data in your smart contract. You have the user sign a structure containing the transaction information, which your offchain code then submits to the blockchain as a transaction. +author: Ori Pomerantz +tags: [ "gasless", "solidity", "eip-712", "meta-transactions" ] +skill: intermediate +lang: vi +published: 2026-02-27 +--- + +## Giới thiệu {#introduction} + +If we want Ethereum to serve [a billion more people](https://blog.ethereum.org/category/next-billion), we need to remove friction and make it as easy to use as possible. One source of this friction is the need for ETH to pay gas fees. + +If you have a dapp that makes money from users, it might make sense to let users submit transactions through your server and pay the transaction fees yourself. Because users still sign an [EIP-712 authorization message](https://eips.ethereum.org/EIPS/eip-712) in their wallets, they retain Ethereum's guarantees of integrity. Availability depends on the server that relays transactions, so it is more limited. However, you can set things up so users can also access the smart contract directly (if they get ETH), and let others set up their own servers if they want to sponsor transactions. + +The technique in this tutorial only works when you control the smart contract. There are other techniques, including [account abstraction](https://eips.ethereum.org/EIPS/eip-4337) that let you sponsor transactions to other smart contracts, which I hope to cover in a future tutorial. + +Note: This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Learn more in the [vulnerabilities section of this guide](#vulnerabilities). + +### Điều kiện tiên quyết {#prerequisites} + +To understand this tutorial you need to already be familiar with: + +- Solidity +- JavaScript +- React and WAGMI. If you are not familiar with these user interface tools, [we have a tutorial for that](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +## The sample application {#sample-app} + +The sample application here is a variant on Hardhat's `Greeter` contract. You can see it [on GitHub](https://github.com/qbzzt/260301-gasless). The smart contract is already deployed on the [Sepolia](https://sepolia.dev/), at address [`0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA`](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA). + +To see it in action, follow these steps. + +1. Clone the repository and install the necessary software. + + ```sh + git clone https://github.com/qbzzt/260301-gasless.git + cd 260301-gasless/server + npm install + ``` + +2. Edit `.env` to set `PRIVATE_KEY` to a wallet that has ETH on Sepolia. If you need Sepolia ETH, [use a faucet](/developers/docs/networks/#sepolia). Ideally, this private key should be different from the one you have in your browser wallet. + +3. Khởi động máy chủ. + + ```sh + npm run dev + ``` + +4. Browse to the application at URL [`http://localhost:5173`](http://localhost:5173). + +5. Click **Connect with Injected** to connect to a wallet. Approve in the wallet, and approve the change to Sepolia if necessary. + +6. Write a new greeting and click **Update greeting via sponsor**. + +7. Sign the message. + +8. Wait about 12 seconds (the block time on Sepolia). While waiting you can look at the URL in the server's console to see the transaction. + +9. See that the greeting changed, and that the last updated by address value is now the address of your browser wallet. + +To understand how this works, we need to look at how the message gets created in the user interface, how it is relayed by the server, and how the smart contract processes it. + +### The user interface {#ui-changes} + +The user interface is based on [WAGMI](https://wagmi.sh/); you can read about it [in this tutorial](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +Here is how we sign the message: + +```js +const signGreeting = useCallback( +``` + +The React hook [`useCallback`](https://react.dev/reference/react/useCallback) lets us improve performance by reusing the same function when the component is redrawn. + +```js + async (greeting) => { + if (!account) throw new Error("Wallet not connected") +``` + +If there is no account, raise an error. This should never happen because the UI button that starts the process that calls `signGreeting` is disabled in that case. However, future programmers may remove that safeguard, so it's a good idea to check this condition here as well. + +```js + const domain = { + name: "Greeter", + version: "1", + chainId, + verifyingContract: contractAddr, + } +``` + +Parameters for the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator). This value is constant, so in a better-optimized implementation, we might calculate it once rather than recalculate it each time the function is called. + +- `name` is a user-readable name, such as the name of the dapp for which we are producing signatures. +- `version` is the version. Different versions are not compatible. +- `chainId` is the chain we are using, as provided [by WAGMI](https://wagmi.sh/react/api/hooks/useChainId). +- `verifyingContract` is the contract address that will verify this signature. We do not want the same signature to apply to multiple contracts, in case there are several `Greeter` contracts and we want them to have different greetings. + +```js + + const types = { + GreetingRequest: [ + { name: "greeting", type: "string" }, + ], + } +``` + +The data type we sign. Here, we have a single parameter, `greeting`, but real-life systems typically have more. + +```js + const message = { greeting } +``` + +The actual message we want to sign and send. `greeting` is both the field name and the name of the variable that fills it. + +```js + const signature = await signTypedDataAsync({ + domain, + types, + primaryType: "GreetingRequest", + message, + }) +``` + +Actually get the signature. This function is asynchronous because users take a long time (from a computer's perspective) to sign data. + +```js + const r = `0x${signature.slice(2, 66)}` + const s = `0x${signature.slice(66, 130)}` + const v = parseInt(signature.slice(130, 132), 16) + + return { + req: { greeting }, + v, + r, + s, + } + }, +``` + +The function returns a single hexadecimal value. Here we divide it into fields. + +```js + [account, chainId, contractAddr, signTypedDataAsync], +) +``` + +If any of these variables change, create a new instance of the function. The `account` and `chainId` parameters can be changed by the user in the wallet. `contractAddr` is a function of the chain Id. `signTypedDataAsync` should not change, but we import it from [a hook](https://wagmi.sh/react/api/hooks/useSignTypedData), so we can't be sure, and it's best to add it here. + +Now that the new greeting is signed, we need to send it to the server. + +```js + const sponsoredGreeting = async () => { + try { +``` + +This function takes a signature and sends it to the server. + +```js + const signedMessage = await signGreeting(newGreeting) + const response = await fetch("/server/sponsor", { +``` + +Send to the path `/server/sponsor` in the server we came from. + +```js + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signedMessage), + }) +``` + +Use `POST` to send the information JSON-encoded. + +```js + const data = await response.json() + console.log("Server response:", data) + } catch (err) { + console.error("Error:", err) + } + } +``` + +Output the response. On a production system we'd also show the response to the user. + +### The server {#server} + +I like using [Vite](https://vite.dev/) as my front-end. It automatically serves the React libraries and updates the browser when the front-end code changes. However, Vite does not include backend tooling. + +The solution is in [`index.js`](https://github.com/qbzzt/260301-gasless/blob/main/server/index.js). + +```js + app.post("/server/sponsor", async (req, res) => { + ... + }) + + // Let Vite handle everything else + const vite = await createViteServer({ + server: { middlewareMode: true } + }) + + app.use(vite.middlewares) +``` + +First we register a handler for the requests we handle ourselves (`POST` to `/server/sponsor`). Then we create and use a Vite server to handle all other URLs. + +```js + app.post("/server/sponsor", async (req, res) => { + try { + const signed = req.body + + const txHash = await sepoliaClient.writeContract({ + address: greeterAddr, + abi: greeterABI, + functionName: 'sponsoredSetGreeting', + args: [signed.req, signed.v, signed.r, signed.s], + }) + } ... + }) +``` + +This is just a standard [viem](https://viem.sh/) blockchain call. + +### The smart contract {#smart-contract} + +Finally, [`Greeter.sol`](https://github.com/qbzzt/260301-gasless/blob/main/contracts/src/Greeter.sol) needs to verify the signature. + +```solidity + constructor(string memory _greeting) { + greeting = _greeting; + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("Greeter")), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } +``` + +The constructor creates the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator), similar to the user interface code above. Blockchain execution is much more expensive, so we only calculate it once. + +```solidity + struct GreetingRequest { + string greeting; + } +``` + +This is the structure that gets signed. Here we have just one field. + +```solidity + bytes32 private constant GREETING_TYPEHASH = + keccak256("GreetingRequest(string greeting)"); +``` + +This is the [structure identifier](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct). It is calculated each time in the user interface. + +```solidity + function sponsoredSetGreeting( + GreetingRequest calldata req, + uint8 v, + bytes32 r, + bytes32 s + ) external { +``` + +This function receives a signed request and updates the greeting. + +```solidity + // Compute EIP-712 digest + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + GREETING_TYPEHASH, + keccak256(bytes(req.greeting)) + ) + ) + ) + ); +``` + +Create the digest in accordance with [EIP 712](https://eips.ethereum.org/EIPS/eip-712). + +```solidity + // Recover signer + address signer = ecrecover(digest, v, r, s); + require(signer != address(0), "Invalid signature"); +``` + +Use [`ecrecover`](https://www.evm.codes/precompiled?fork=osaka#0x01) to get the signer address. Note that a bad signature can still result in a valid address, just a random one. + +```solidity + // Apply greeting as if signer called it + greeting = req.greeting; + emit SetGreeting(signer, req.greeting); + } +``` + +Update the greeting. + +## Vulnerabilities {#vulnerabilities} + +This is _not_ production-level code. Nó dễ bị tấn công đáng kể và thiếu các tính năng chính. Here are some, along with how to solve them. + +To see some of these attacks, click the buttons under the _Attacks_ heading and see what happens. For the **Invalid signature** button, check the server console to see the transaction response. + +### Denial of service on the server {#dos-on-server} + +The easiest attack is a [denial-of-service](https://en.wikipedia.org/wiki/Denial-of-service_attack) attack on the server. The server receives requests from anywhere on the Internet and based on those requests sends transactions. There is absolutely nothing preventing an attacker from issuing a bunch of signatures, valid or invalid. Each will cause a transaction. Eventually the server will run out of ETH to pay for gas. + +One solution to this problem is to limit the rate to one transaction per block. If the purpose is to show greetings to [externally owned accounts](/developers/docs/accounts/#key-differences), it does not matter what the greeting is in the middle of the block anyway. + +Another solution is to keep track of addresses and only allow signatures from valid customers. + +### Wrong greeting signatures {#wrong-greeting-sigs} + +When you click **Signature for wrong greeting**, you submit a valid signature for a specific address (`0xaA92c5d426430D4769c9E878C1333BDe3d689b3e`) and greeting (`Hello`). But it submits it with a different greeting. This confuses `ecrecover`, which changes the greeting but has the wrong address. + +To solve this problem, add the address to the [signed structure](https://github.com/qbzzt/260301-gasless/blob/main/server/src/Greeter.jsx#L122-L124). This way, the `ecrecover` random address won't match the address in the signature, and the smart contract will reject the message. + +### Replay attacks {#replay-attack} + +When you click **Replay attack**, you submit the same "I'm 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, and I'd like the greeting to be `Hello`" signature, but with the correct greeting. As a result, the smart contract believes that the address (which isn't yours) changed the greeting back to `Hello`. The information to do this is publicly available in the [transaction information](https://eth-sepolia.blockscout.com/tx/0xa66afe4bbf886f59533e677a798c802ceab1ac0f9db6e83a4d4b59a45cf7c1b1). + +If this is a problem, one solution is to add a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). Have a [mapping](https://docs.soliditylang.org/en/latest/types.html#mapping-types) between addresses and numbers, and add a nonce field to the signature. If the nonce field matches the mapping for the address, accept the signature and increment the mapping for next time. If it doesn't, reject the transaction. + +Another solution is to add a timestamp to the signed data and accept the signature as valid only for a few seconds after that timestamp. This is simpler and cheaper, but we risk replay attacks within the time window, and the failure of legitimate transactions if the time window is exceeded. + +## Other missing features {#other-missing-features} + +There are additional features we would add in a production setting. + +### Access from other servers {#other-servers} + +Currently, we allow any address to submit a `sponsorSetGreeting`. This may be exactly what we want, in the interest of decentralization. Or maybe we want to ensure that sponsored transactions go through _our_ server, in which case we'd check `msg.sender` in the smart contract. + +Either way, this should be a conscious design decision, not just the result of not thinking about the issue. + +### Error handling {#error-handling} + +A user submits a greeting. Maybe it gets updated at the next block. Maybe it doesn't. Errors are invisible. On a production system, the user should be able to distinguish between these cases: + +- The new greeting has not been submitted yet +- The new greeting has been submitted, and it's in process +- The new greeting has been rejected + +## Kết luận {#conclusion} + +At this point, you should be able to create a gasless experience for your dapp users, at the cost of some centralization. + +However, this only works with smart contracts that support ERC-712. To transfer an ERC-20 token, for example, it is necessary to have the transaction signed by the owner rather than just a message. The solution is [account abstraction (ERC-4337)](https://docs.erc4337.io/index.html). I hope to write a future tutorial about it. + +[Xem thêm công việc của tôi tại đây](https://cryptodocguy.pro/). diff --git a/public/content/translations/zh-tw/developers/tutorials/gasless/index.md b/public/content/translations/zh-tw/developers/tutorials/gasless/index.md new file mode 100644 index 00000000000..81662435534 --- /dev/null +++ b/public/content/translations/zh-tw/developers/tutorials/gasless/index.md @@ -0,0 +1,362 @@ +--- +title: "Sponsoring gas fees: How to cover transaction costs for your users" +description: It is easy to create a private key and an address; it's just a matter of running the right software. But there are many places in the world where getting the ETH to send transactions is much harder. In this tutorial you learn how to cover the onchain gas costs for executing user-signed, offchain structured data in your smart contract. You have the user sign a structure containing the transaction information, which your offchain code then submits to the blockchain as a transaction. +author: "作者:Ori Pomerantz" +tags: [ "gasless", "穩固", "eip-712", "meta-transactions" ] +skill: intermediate +lang: zh-tw +published: 2026-02-27 +--- + +## 介紹 {#introduction} + +If we want Ethereum to serve [a billion more people](https://blog.ethereum.org/category/next-billion), we need to remove friction and make it as easy to use as possible. One source of this friction is the need for ETH to pay gas fees. + +If you have a dapp that makes money from users, it might make sense to let users submit transactions through your server and pay the transaction fees yourself. Because users still sign an [EIP-712 authorization message](https://eips.ethereum.org/EIPS/eip-712) in their wallets, they retain Ethereum's guarantees of integrity. Availability depends on the server that relays transactions, so it is more limited. However, you can set things up so users can also access the smart contract directly (if they get ETH), and let others set up their own servers if they want to sponsor transactions. + +The technique in this tutorial only works when you control the smart contract. There are other techniques, including [account abstraction](https://eips.ethereum.org/EIPS/eip-4337) that let you sponsor transactions to other smart contracts, which I hope to cover in a future tutorial. + +Note: This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Learn more in the [vulnerabilities section of this guide](#vulnerabilities). + +### 先決條件 {#prerequisites} + +To understand this tutorial you need to already be familiar with: + +- Solidity +- JavaScript +- React and WAGMI. If you are not familiar with these user interface tools, [we have a tutorial for that](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +## The sample application {#sample-app} + +The sample application here is a variant on Hardhat's `Greeter` contract. You can see it [on GitHub](https://github.com/qbzzt/260301-gasless). The smart contract is already deployed on the [Sepolia](https://sepolia.dev/), at address [`0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA`](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA). + +To see it in action, follow these steps. + +1. Clone the repository and install the necessary software. + + ```sh + git clone https://github.com/qbzzt/260301-gasless.git + cd 260301-gasless/server + npm install + ``` + +2. Edit `.env` to set `PRIVATE_KEY` to a wallet that has ETH on Sepolia. If you need Sepolia ETH, [use a faucet](/developers/docs/networks/#sepolia). Ideally, this private key should be different from the one you have in your browser wallet. + +3. 啟動伺服器。 + + ```sh + npm run dev + ``` + +4. Browse to the application at URL [`http://localhost:5173`](http://localhost:5173). + +5. Click **Connect with Injected** to connect to a wallet. Approve in the wallet, and approve the change to Sepolia if necessary. + +6. Write a new greeting and click **Update greeting via sponsor**. + +7. Sign the message. + +8. Wait about 12 seconds (the block time on Sepolia). While waiting you can look at the URL in the server's console to see the transaction. + +9. See that the greeting changed, and that the last updated by address value is now the address of your browser wallet. + +To understand how this works, we need to look at how the message gets created in the user interface, how it is relayed by the server, and how the smart contract processes it. + +### The user interface {#ui-changes} + +The user interface is based on [WAGMI](https://wagmi.sh/); you can read about it [in this tutorial](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +Here is how we sign the message: + +```js +const signGreeting = useCallback( +``` + +The React hook [`useCallback`](https://react.dev/reference/react/useCallback) lets us improve performance by reusing the same function when the component is redrawn. + +```js + async (greeting) => { + if (!account) throw new Error("Wallet not connected") +``` + +If there is no account, raise an error. This should never happen because the UI button that starts the process that calls `signGreeting` is disabled in that case. However, future programmers may remove that safeguard, so it's a good idea to check this condition here as well. + +```js + const domain = { + name: "Greeter", + version: "1", + chainId, + verifyingContract: contractAddr, + } +``` + +Parameters for the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator). This value is constant, so in a better-optimized implementation, we might calculate it once rather than recalculate it each time the function is called. + +- `name` is a user-readable name, such as the name of the dapp for which we are producing signatures. +- `version` is the version. Different versions are not compatible. +- `chainId` is the chain we are using, as provided [by WAGMI](https://wagmi.sh/react/api/hooks/useChainId). +- `verifyingContract` is the contract address that will verify this signature. We do not want the same signature to apply to multiple contracts, in case there are several `Greeter` contracts and we want them to have different greetings. + +```js + + const types = { + GreetingRequest: [ + { name: "greeting", type: "string" }, + ], + } +``` + +The data type we sign. Here, we have a single parameter, `greeting`, but real-life systems typically have more. + +```js + const message = { greeting } +``` + +The actual message we want to sign and send. `greeting` is both the field name and the name of the variable that fills it. + +```js + const signature = await signTypedDataAsync({ + domain, + types, + primaryType: "GreetingRequest", + message, + }) +``` + +Actually get the signature. This function is asynchronous because users take a long time (from a computer's perspective) to sign data. + +```js + const r = `0x${signature.slice(2, 66)}` + const s = `0x${signature.slice(66, 130)}` + const v = parseInt(signature.slice(130, 132), 16) + + return { + req: { greeting }, + v, + r, + s, + } + }, +``` + +The function returns a single hexadecimal value. Here we divide it into fields. + +```js + [account, chainId, contractAddr, signTypedDataAsync], +) +``` + +If any of these variables change, create a new instance of the function. The `account` and `chainId` parameters can be changed by the user in the wallet. `contractAddr` is a function of the chain Id. `signTypedDataAsync` should not change, but we import it from [a hook](https://wagmi.sh/react/api/hooks/useSignTypedData), so we can't be sure, and it's best to add it here. + +Now that the new greeting is signed, we need to send it to the server. + +```js + const sponsoredGreeting = async () => { + try { +``` + +This function takes a signature and sends it to the server. + +```js + const signedMessage = await signGreeting(newGreeting) + const response = await fetch("/server/sponsor", { +``` + +Send to the path `/server/sponsor` in the server we came from. + +```js + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signedMessage), + }) +``` + +Use `POST` to send the information JSON-encoded. + +```js + const data = await response.json() + console.log("Server response:", data) + } catch (err) { + console.error("Error:", err) + } + } +``` + +Output the response. On a production system we'd also show the response to the user. + +### The server {#server} + +I like using [Vite](https://vite.dev/) as my front-end. It automatically serves the React libraries and updates the browser when the front-end code changes. However, Vite does not include backend tooling. + +The solution is in [`index.js`](https://github.com/qbzzt/260301-gasless/blob/main/server/index.js). + +```js + app.post("/server/sponsor", async (req, res) => { + ... + }) + + // Let Vite handle everything else + const vite = await createViteServer({ + server: { middlewareMode: true } + }) + + app.use(vite.middlewares) +``` + +First we register a handler for the requests we handle ourselves (`POST` to `/server/sponsor`). Then we create and use a Vite server to handle all other URLs. + +```js + app.post("/server/sponsor", async (req, res) => { + try { + const signed = req.body + + const txHash = await sepoliaClient.writeContract({ + address: greeterAddr, + abi: greeterABI, + functionName: 'sponsoredSetGreeting', + args: [signed.req, signed.v, signed.r, signed.s], + }) + } ... + }) +``` + +This is just a standard [viem](https://viem.sh/) blockchain call. + +### The smart contract {#smart-contract} + +Finally, [`Greeter.sol`](https://github.com/qbzzt/260301-gasless/blob/main/contracts/src/Greeter.sol) needs to verify the signature. + +```solidity + constructor(string memory _greeting) { + greeting = _greeting; + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("Greeter")), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } +``` + +The constructor creates the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator), similar to the user interface code above. Blockchain execution is much more expensive, so we only calculate it once. + +```solidity + struct GreetingRequest { + string greeting; + } +``` + +This is the structure that gets signed. Here we have just one field. + +```solidity + bytes32 private constant GREETING_TYPEHASH = + keccak256("GreetingRequest(string greeting)"); +``` + +This is the [structure identifier](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct). It is calculated each time in the user interface. + +```solidity + function sponsoredSetGreeting( + GreetingRequest calldata req, + uint8 v, + bytes32 r, + bytes32 s + ) external { +``` + +This function receives a signed request and updates the greeting. + +```solidity + // Compute EIP-712 digest + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + GREETING_TYPEHASH, + keccak256(bytes(req.greeting)) + ) + ) + ) + ); +``` + +Create the digest in accordance with [EIP 712](https://eips.ethereum.org/EIPS/eip-712). + +```solidity + // Recover signer + address signer = ecrecover(digest, v, r, s); + require(signer != address(0), "Invalid signature"); +``` + +Use [`ecrecover`](https://www.evm.codes/precompiled?fork=osaka#0x01) to get the signer address. Note that a bad signature can still result in a valid address, just a random one. + +```solidity + // Apply greeting as if signer called it + greeting = req.greeting; + emit SetGreeting(signer, req.greeting); + } +``` + +Update the greeting. + +## Vulnerabilities {#vulnerabilities} + +This is _not_ production-level code. 它很容易受到重大攻擊,也缺乏主要功能。 Here are some, along with how to solve them. + +To see some of these attacks, click the buttons under the _Attacks_ heading and see what happens. For the **Invalid signature** button, check the server console to see the transaction response. + +### Denial of service on the server {#dos-on-server} + +The easiest attack is a [denial-of-service](https://en.wikipedia.org/wiki/Denial-of-service_attack) attack on the server. The server receives requests from anywhere on the Internet and based on those requests sends transactions. There is absolutely nothing preventing an attacker from issuing a bunch of signatures, valid or invalid. Each will cause a transaction. Eventually the server will run out of ETH to pay for gas. + +One solution to this problem is to limit the rate to one transaction per block. If the purpose is to show greetings to [externally owned accounts](/developers/docs/accounts/#key-differences), it does not matter what the greeting is in the middle of the block anyway. + +Another solution is to keep track of addresses and only allow signatures from valid customers. + +### Wrong greeting signatures {#wrong-greeting-sigs} + +When you click **Signature for wrong greeting**, you submit a valid signature for a specific address (`0xaA92c5d426430D4769c9E878C1333BDe3d689b3e`) and greeting (`Hello`). But it submits it with a different greeting. This confuses `ecrecover`, which changes the greeting but has the wrong address. + +To solve this problem, add the address to the [signed structure](https://github.com/qbzzt/260301-gasless/blob/main/server/src/Greeter.jsx#L122-L124). This way, the `ecrecover` random address won't match the address in the signature, and the smart contract will reject the message. + +### Replay attacks {#replay-attack} + +When you click **Replay attack**, you submit the same "I'm 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, and I'd like the greeting to be `Hello`" signature, but with the correct greeting. As a result, the smart contract believes that the address (which isn't yours) changed the greeting back to `Hello`. The information to do this is publicly available in the [transaction information](https://eth-sepolia.blockscout.com/tx/0xa66afe4bbf886f59533e677a798c802ceab1ac0f9db6e83a4d4b59a45cf7c1b1). + +If this is a problem, one solution is to add a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). Have a [mapping](https://docs.soliditylang.org/en/latest/types.html#mapping-types) between addresses and numbers, and add a nonce field to the signature. If the nonce field matches the mapping for the address, accept the signature and increment the mapping for next time. If it doesn't, reject the transaction. + +Another solution is to add a timestamp to the signed data and accept the signature as valid only for a few seconds after that timestamp. This is simpler and cheaper, but we risk replay attacks within the time window, and the failure of legitimate transactions if the time window is exceeded. + +## Other missing features {#other-missing-features} + +There are additional features we would add in a production setting. + +### Access from other servers {#other-servers} + +Currently, we allow any address to submit a `sponsorSetGreeting`. This may be exactly what we want, in the interest of decentralization. Or maybe we want to ensure that sponsored transactions go through _our_ server, in which case we'd check `msg.sender` in the smart contract. + +Either way, this should be a conscious design decision, not just the result of not thinking about the issue. + +### Error handling {#error-handling} + +A user submits a greeting. Maybe it gets updated at the next block. Maybe it doesn't. Errors are invisible. On a production system, the user should be able to distinguish between these cases: + +- The new greeting has not been submitted yet +- The new greeting has been submitted, and it's in process +- The new greeting has been rejected + +## 結論 {#conclusion} + +At this point, you should be able to create a gasless experience for your dapp users, at the cost of some centralization. + +However, this only works with smart contracts that support ERC-712. To transfer an ERC-20 token, for example, it is necessary to have the transaction signed by the owner rather than just a message. The solution is [account abstraction (ERC-4337)](https://docs.erc4337.io/index.html). I hope to write a future tutorial about it. + +[在此查看我的更多作品](https://cryptodocguy.pro/)。 diff --git a/public/content/translations/zh/developers/tutorials/gasless/index.md b/public/content/translations/zh/developers/tutorials/gasless/index.md new file mode 100644 index 00000000000..83e99b74483 --- /dev/null +++ b/public/content/translations/zh/developers/tutorials/gasless/index.md @@ -0,0 +1,362 @@ +--- +title: "Sponsoring gas fees: How to cover transaction costs for your users" +description: It is easy to create a private key and an address; it's just a matter of running the right software. But there are many places in the world where getting the ETH to send transactions is much harder. In this tutorial you learn how to cover the onchain gas costs for executing user-signed, offchain structured data in your smart contract. You have the user sign a structure containing the transaction information, which your offchain code then submits to the blockchain as a transaction. +author: Ori Pomerantz +tags: [ "gasless", "Solidity", "eip-712", "meta-transactions" ] +skill: intermediate +lang: zh +published: 2026-02-27 +--- + +## 简介 {#introduction} + +If we want Ethereum to serve [a billion more people](https://blog.ethereum.org/category/next-billion), we need to remove friction and make it as easy to use as possible. One source of this friction is the need for ETH to pay gas fees. + +If you have a dapp that makes money from users, it might make sense to let users submit transactions through your server and pay the transaction fees yourself. Because users still sign an [EIP-712 authorization message](https://eips.ethereum.org/EIPS/eip-712) in their wallets, they retain Ethereum's guarantees of integrity. Availability depends on the server that relays transactions, so it is more limited. However, you can set things up so users can also access the smart contract directly (if they get ETH), and let others set up their own servers if they want to sponsor transactions. + +The technique in this tutorial only works when you control the smart contract. There are other techniques, including [account abstraction](https://eips.ethereum.org/EIPS/eip-4337) that let you sponsor transactions to other smart contracts, which I hope to cover in a future tutorial. + +Note: This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Learn more in the [vulnerabilities section of this guide](#vulnerabilities). + +### 前提条件 {#prerequisites} + +To understand this tutorial you need to already be familiar with: + +- Solidity +- JavaScript +- React and WAGMI. If you are not familiar with these user interface tools, [we have a tutorial for that](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +## The sample application {#sample-app} + +The sample application here is a variant on Hardhat's `Greeter` contract. You can see it [on GitHub](https://github.com/qbzzt/260301-gasless). The smart contract is already deployed on the [Sepolia](https://sepolia.dev/), at address [`0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA`](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA). + +To see it in action, follow these steps. + +1. Clone the repository and install the necessary software. + + ```sh + git clone https://github.com/qbzzt/260301-gasless.git + cd 260301-gasless/server + npm install + ``` + +2. Edit `.env` to set `PRIVATE_KEY` to a wallet that has ETH on Sepolia. If you need Sepolia ETH, [use a faucet](/developers/docs/networks/#sepolia). Ideally, this private key should be different from the one you have in your browser wallet. + +3. 启动服务器。 + + ```sh + npm run dev + ``` + +4. Browse to the application at URL [`http://localhost:5173`](http://localhost:5173). + +5. Click **Connect with Injected** to connect to a wallet. Approve in the wallet, and approve the change to Sepolia if necessary. + +6. Write a new greeting and click **Update greeting via sponsor**. + +7. Sign the message. + +8. Wait about 12 seconds (the block time on Sepolia). While waiting you can look at the URL in the server's console to see the transaction. + +9. See that the greeting changed, and that the last updated by address value is now the address of your browser wallet. + +To understand how this works, we need to look at how the message gets created in the user interface, how it is relayed by the server, and how the smart contract processes it. + +### The user interface {#ui-changes} + +The user interface is based on [WAGMI](https://wagmi.sh/); you can read about it [in this tutorial](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/). + +Here is how we sign the message: + +```js +const signGreeting = useCallback( +``` + +The React hook [`useCallback`](https://react.dev/reference/react/useCallback) lets us improve performance by reusing the same function when the component is redrawn. + +```js + async (greeting) => { + if (!account) throw new Error("Wallet not connected") +``` + +If there is no account, raise an error. This should never happen because the UI button that starts the process that calls `signGreeting` is disabled in that case. However, future programmers may remove that safeguard, so it's a good idea to check this condition here as well. + +```js + const domain = { + name: "Greeter", + version: "1", + chainId, + verifyingContract: contractAddr, + } +``` + +Parameters for the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator). This value is constant, so in a better-optimized implementation, we might calculate it once rather than recalculate it each time the function is called. + +- `name` is a user-readable name, such as the name of the dapp for which we are producing signatures. +- `version` is the version. Different versions are not compatible. +- `chainId` is the chain we are using, as provided [by WAGMI](https://wagmi.sh/react/api/hooks/useChainId). +- `verifyingContract` is the contract address that will verify this signature. We do not want the same signature to apply to multiple contracts, in case there are several `Greeter` contracts and we want them to have different greetings. + +```js + + const types = { + GreetingRequest: [ + { name: "greeting", type: "string" }, + ], + } +``` + +The data type we sign. Here, we have a single parameter, `greeting`, but real-life systems typically have more. + +```js + const message = { greeting } +``` + +The actual message we want to sign and send. `greeting` is both the field name and the name of the variable that fills it. + +```js + const signature = await signTypedDataAsync({ + domain, + types, + primaryType: "GreetingRequest", + message, + }) +``` + +Actually get the signature. This function is asynchronous because users take a long time (from a computer's perspective) to sign data. + +```js + const r = `0x${signature.slice(2, 66)}` + const s = `0x${signature.slice(66, 130)}` + const v = parseInt(signature.slice(130, 132), 16) + + return { + req: { greeting }, + v, + r, + s, + } + }, +``` + +The function returns a single hexadecimal value. Here we divide it into fields. + +```js + [account, chainId, contractAddr, signTypedDataAsync], +) +``` + +If any of these variables change, create a new instance of the function. The `account` and `chainId` parameters can be changed by the user in the wallet. `contractAddr` is a function of the chain Id. `signTypedDataAsync` should not change, but we import it from [a hook](https://wagmi.sh/react/api/hooks/useSignTypedData), so we can't be sure, and it's best to add it here. + +Now that the new greeting is signed, we need to send it to the server. + +```js + const sponsoredGreeting = async () => { + try { +``` + +This function takes a signature and sends it to the server. + +```js + const signedMessage = await signGreeting(newGreeting) + const response = await fetch("/server/sponsor", { +``` + +Send to the path `/server/sponsor` in the server we came from. + +```js + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signedMessage), + }) +``` + +Use `POST` to send the information JSON-encoded. + +```js + const data = await response.json() + console.log("Server response:", data) + } catch (err) { + console.error("Error:", err) + } + } +``` + +Output the response. On a production system we'd also show the response to the user. + +### The server {#server} + +I like using [Vite](https://vite.dev/) as my front-end. It automatically serves the React libraries and updates the browser when the front-end code changes. However, Vite does not include backend tooling. + +The solution is in [`index.js`](https://github.com/qbzzt/260301-gasless/blob/main/server/index.js). + +```js + app.post("/server/sponsor", async (req, res) => { + ... + }) + + // Let Vite handle everything else + const vite = await createViteServer({ + server: { middlewareMode: true } + }) + + app.use(vite.middlewares) +``` + +First we register a handler for the requests we handle ourselves (`POST` to `/server/sponsor`). Then we create and use a Vite server to handle all other URLs. + +```js + app.post("/server/sponsor", async (req, res) => { + try { + const signed = req.body + + const txHash = await sepoliaClient.writeContract({ + address: greeterAddr, + abi: greeterABI, + functionName: 'sponsoredSetGreeting', + args: [signed.req, signed.v, signed.r, signed.s], + }) + } ... + }) +``` + +This is just a standard [viem](https://viem.sh/) blockchain call. + +### The smart contract {#smart-contract} + +Finally, [`Greeter.sol`](https://github.com/qbzzt/260301-gasless/blob/main/contracts/src/Greeter.sol) needs to verify the signature. + +```solidity + constructor(string memory _greeting) { + greeting = _greeting; + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("Greeter")), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } +``` + +The constructor creates the [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator), similar to the user interface code above. Blockchain execution is much more expensive, so we only calculate it once. + +```solidity + struct GreetingRequest { + string greeting; + } +``` + +This is the structure that gets signed. Here we have just one field. + +```solidity + bytes32 private constant GREETING_TYPEHASH = + keccak256("GreetingRequest(string greeting)"); +``` + +This is the [structure identifier](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct). It is calculated each time in the user interface. + +```solidity + function sponsoredSetGreeting( + GreetingRequest calldata req, + uint8 v, + bytes32 r, + bytes32 s + ) external { +``` + +This function receives a signed request and updates the greeting. + +```solidity + // Compute EIP-712 digest + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + GREETING_TYPEHASH, + keccak256(bytes(req.greeting)) + ) + ) + ) + ); +``` + +Create the digest in accordance with [EIP 712](https://eips.ethereum.org/EIPS/eip-712). + +```solidity + // Recover signer + address signer = ecrecover(digest, v, r, s); + require(signer != address(0), "Invalid signature"); +``` + +Use [`ecrecover`](https://www.evm.codes/precompiled?fork=osaka#0x01) to get the signer address. Note that a bad signature can still result in a valid address, just a random one. + +```solidity + // Apply greeting as if signer called it + greeting = req.greeting; + emit SetGreeting(signer, req.greeting); + } +``` + +Update the greeting. + +## Vulnerabilities {#vulnerabilities} + +This is _not_ production-level code. It is vulnerable to significant attacks and lacks major features. Here are some, along with how to solve them. + +To see some of these attacks, click the buttons under the _Attacks_ heading and see what happens. For the **Invalid signature** button, check the server console to see the transaction response. + +### Denial of service on the server {#dos-on-server} + +The easiest attack is a [denial-of-service](https://en.wikipedia.org/wiki/Denial-of-service_attack) attack on the server. The server receives requests from anywhere on the Internet and based on those requests sends transactions. There is absolutely nothing preventing an attacker from issuing a bunch of signatures, valid or invalid. Each will cause a transaction. Eventually the server will run out of ETH to pay for gas. + +One solution to this problem is to limit the rate to one transaction per block. If the purpose is to show greetings to [externally owned accounts](/developers/docs/accounts/#key-differences), it does not matter what the greeting is in the middle of the block anyway. + +Another solution is to keep track of addresses and only allow signatures from valid customers. + +### Wrong greeting signatures {#wrong-greeting-sigs} + +When you click **Signature for wrong greeting**, you submit a valid signature for a specific address (`0xaA92c5d426430D4769c9E878C1333BDe3d689b3e`) and greeting (`Hello`). But it submits it with a different greeting. This confuses `ecrecover`, which changes the greeting but has the wrong address. + +To solve this problem, add the address to the [signed structure](https://github.com/qbzzt/260301-gasless/blob/main/server/src/Greeter.jsx#L122-L124). This way, the `ecrecover` random address won't match the address in the signature, and the smart contract will reject the message. + +### Replay attacks {#replay-attack} + +When you click **Replay attack**, you submit the same "I'm 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, and I'd like the greeting to be `Hello`" signature, but with the correct greeting. As a result, the smart contract believes that the address (which isn't yours) changed the greeting back to `Hello`. The information to do this is publicly available in the [transaction information](https://eth-sepolia.blockscout.com/tx/0xa66afe4bbf886f59533e677a798c802ceab1ac0f9db6e83a4d4b59a45cf7c1b1). + +If this is a problem, one solution is to add a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). Have a [mapping](https://docs.soliditylang.org/en/latest/types.html#mapping-types) between addresses and numbers, and add a nonce field to the signature. If the nonce field matches the mapping for the address, accept the signature and increment the mapping for next time. If it doesn't, reject the transaction. + +Another solution is to add a timestamp to the signed data and accept the signature as valid only for a few seconds after that timestamp. This is simpler and cheaper, but we risk replay attacks within the time window, and the failure of legitimate transactions if the time window is exceeded. + +## Other missing features {#other-missing-features} + +There are additional features we would add in a production setting. + +### Access from other servers {#other-servers} + +Currently, we allow any address to submit a `sponsorSetGreeting`. This may be exactly what we want, in the interest of decentralization. Or maybe we want to ensure that sponsored transactions go through _our_ server, in which case we'd check `msg.sender` in the smart contract. + +Either way, this should be a conscious design decision, not just the result of not thinking about the issue. + +### Error handling {#error-handling} + +A user submits a greeting. Maybe it gets updated at the next block. Maybe it doesn't. Errors are invisible. On a production system, the user should be able to distinguish between these cases: + +- The new greeting has not been submitted yet +- The new greeting has been submitted, and it's in process +- The new greeting has been rejected + +## 结论 {#conclusion} + +At this point, you should be able to create a gasless experience for your dapp users, at the cost of some centralization. + +However, this only works with smart contracts that support ERC-712. To transfer an ERC-20 token, for example, it is necessary to have the transaction signed by the owner rather than just a message. The solution is [account abstraction (ERC-4337)](https://docs.erc4337.io/index.html). I hope to write a future tutorial about it. + +[点击此处查看我的更多作品](https://cryptodocguy.pro/)。