Přejít na hlavní obsah

Sponzorování poplatků za gas: Jak pokrýt transakční náklady vašich uživatelů

bez gasu
Solidity
EIP-712
meta-transakce
Středně pokročilý
Ori Pomerantz
27. února 2026
10 minuta čtení

Úvod

Pokud chceme, aby Ethereum sloužilo další miliardě lidí (opens in a new tab), musíme odstranit překážky a zajistit, aby se používalo co nejsnadněji. Jedním ze zdrojů těchto překážek je nutnost mít ETH na placení poplatků za gas.

Pokud máte decentralizovanou aplikaci (dapp), která vydělává na uživatelích, může dávat smysl nechat uživatele odesílat transakce přes váš server a platit transakční poplatky sami. Protože uživatelé stále podepisují autorizační zprávu EIP-712 (opens in a new tab) ve svých peněženkách, zachovávají si záruky integrity Etherea. Dostupnost závisí na serveru, který transakce přeposílá, takže je omezenější. Můžete to však nastavit tak, aby uživatelé mohli k chytrému kontraktu přistupovat i přímo (pokud získají ETH), a umožnit ostatním nastavit si vlastní servery, pokud chtějí sponzorovat transakce.

Technika v tomto tutoriálu funguje pouze tehdy, když ovládáte chytrý kontrakt. Existují i další techniky, včetně abstrakce účtu (opens in a new tab), které vám umožní sponzorovat transakce do jiných chytrých kontraktů, a kterým se snad budu věnovat v některém z budoucích tutoriálů.

Poznámka: Toto není kód připravený pro produkci. Je zranitelný vůči významným útokům a chybí mu důležité funkce. Více se dozvíte v části tohoto průvodce věnované zranitelnostem.

Předpoklady

K porozumění tomuto tutoriálu byste již měli znát:

  • Solidity
  • JavaScript
  • React a WAGMI. Pokud tyto nástroje pro uživatelské rozhraní neznáte, máme pro to tutoriál.

Ukázková aplikace

Zdejší ukázková aplikace je variantou kontraktu Greeter z nástroje Hardhat. Můžete si ji prohlédnout na GitHubu (opens in a new tab). Chytrý kontrakt je již nasazen v síti Sepolia (opens in a new tab) na adrese 0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA (opens in a new tab).

Chcete-li ji vidět v akci, postupujte podle těchto kroků.

  1. Naklonujte repozitář a nainstalujte potřebný software.

    1git clone https://github.com/qbzzt/260301-gasless.git
    2cd 260301-gasless/server
    3npm install
  2. Upravte .env a nastavte PRIVATE_KEY na peněženku, která má ETH v síti Sepolia. Pokud potřebujete Sepolia ETH, použijte faucet. V ideálním případě by se tento soukromý klíč měl lišit od toho, který máte v peněžence v prohlížeči.

  3. Spusťte server.

    1npm run dev
  4. Přejděte do aplikace na URL http://localhost:5173 (opens in a new tab).

  5. Klikněte na Connect with Injected pro připojení k peněžence. Schvalte to v peněžence a v případě potřeby schvalte změnu na síť Sepolia.

  6. Napište nový pozdrav a klikněte na Update greeting via sponsor.

  7. Podepište zprávu.

  8. Počkejte asi 12 sekund (čas bloku v síti Sepolia). Během čekání se můžete podívat na URL v konzoli serveru a prohlédnout si transakci.

  9. Podívejte se, že se pozdrav změnil a že hodnota adresy, která jej naposledy aktualizovala, je nyní adresa vaší peněženky v prohlížeči.

Abychom pochopili, jak to funguje, musíme se podívat na to, jak se zpráva vytváří v uživatelském rozhraní, jak ji server přeposílá a jak ji chytrý kontrakt zpracovává.

Uživatelské rozhraní

Uživatelské rozhraní je založeno na WAGMI (opens in a new tab); můžete si o něm přečíst v tomto tutoriálu.

Zde je ukázka, jak zprávu podepisujeme:

1const signGreeting = useCallback(

React hook useCallback (opens in a new tab) nám umožňuje zlepšit výkon opětovným použitím stejné funkce při překreslení komponenty.

1 async (greeting) => {
2 if (!account) throw new Error("Wallet not connected")

Pokud neexistuje žádný účet, vyvolejte chybu. To by se nikdy nemělo stát, protože tlačítko uživatelského rozhraní, které spouští proces volající signGreeting, je v takovém případě zakázáno. Budoucí programátoři však mohou tuto pojistku odstranit, takže je dobré tuto podmínku zkontrolovat i zde.

1 const domain = {
2 name: "Greeter",
3 version: "1",
4 chainId,
5 verifyingContract: contractAddr,
6 }

Parametry pro oddělovač domén (domain separator) (opens in a new tab). Tato hodnota je konstantní, takže v lépe optimalizované implementaci bychom ji mohli vypočítat pouze jednou, místo abychom ji přepočítávali při každém volání funkce.

  • name je uživatelsky čitelný název, například název dapp, pro kterou vytváříme podpisy.
  • version je verze. Různé verze nejsou kompatibilní.
  • chainId je řetězec, který používáme, jak jej poskytuje WAGMI (opens in a new tab).
  • verifyingContract je adresa kontraktu, který tento podpis ověří. Nechceme, aby stejný podpis platil pro více kontraktů, pro případ, že existuje několik kontraktů Greeter a my chceme, aby měly různé pozdravy.
1
2 const types = {
3 GreetingRequest: [
4 { name: "greeting", type: "string" },
5 ],
6 }

Datový typ, který podepisujeme. Zde máme jediný parametr, greeting, ale systémy v reálném světě jich obvykle mají více.

1 const message = { greeting }

Samotná zpráva, kterou chceme podepsat a odeslat. greeting je název pole i název proměnné, která jej vyplňuje.

1 const signature = await signTypedDataAsync({
2 domain,
3 types,
4 primaryType: "GreetingRequest",
5 message,
6 })

Samotné získání podpisu. Tato funkce je asynchronní, protože uživatelům trvá dlouho (z pohledu počítače), než data podepíší.

1 const r = `0x${signature.slice(2, 66)}`
2 const s = `0x${signature.slice(66, 130)}`
3 const v = parseInt(signature.slice(130, 132), 16)
4
5 return {
6 req: { greeting },
7 v,
8 r,
9 s,
10 }
11 },

Funkce vrací jedinou hexadecimální hodnotu. Zde ji rozdělíme do polí.

1 [account, chainId, contractAddr, signTypedDataAsync],
2)

Pokud se některá z těchto proměnných změní, vytvořte novou instanci funkce. Parametry account a chainId může uživatel změnit v peněžence. contractAddr je funkcí ID řetězce. signTypedDataAsync by se nemělo měnit, ale importujeme jej z hooku (opens in a new tab), takže si nemůžeme být jisti, a je nejlepší jej sem přidat.

Nyní, když je nový pozdrav podepsán, musíme jej odeslat na server.

1 const sponsoredGreeting = async () => {
2 try {

Tato funkce převezme podpis a odešle jej na server.

1 const signedMessage = await signGreeting(newGreeting)
2 const response = await fetch("/server/sponsor", {

Odešlete na cestu /server/sponsor na serveru, ze kterého jsme přišli.

1 method: "POST",
2 headers: { "Content-Type": "application/json" },
3 body: JSON.stringify(signedMessage),
4 })

Použijte POST k odeslání informací zakódovaných ve formátu JSON.

1 const data = await response.json()
2 console.log("Server response:", data)
3 } catch (err) {
4 console.error("Error:", err)
5 }
6 }

Vypište odpověď. V produkčním systému bychom odpověď také zobrazili uživateli.

Server

Rád používám Vite (opens in a new tab) jako svůj front-end. Automaticky servíruje knihovny Reactu a aktualizuje prohlížeč při změně front-endového kódu. Vite však neobsahuje nástroje pro backend.

Řešení se nachází v index.js (opens in a new tab).

1 app.post("/server/sponsor", async (req, res) => {
2 ...
3 })
4
5 // Nechte Vite zpracovat vše ostatní
6 const vite = await createViteServer({
7 server: { middlewareMode: true }
8 })
9
10 app.use(vite.middlewares)

Nejprve zaregistrujeme handler pro požadavky, které zpracováváme sami (POST na /server/sponsor). Poté vytvoříme a použijeme server Vite ke zpracování všech ostatních URL.

1 app.post("/server/sponsor", async (req, res) => {
2 try {
3 const signed = req.body
4
5 const txHash = await sepoliaClient.writeContract({
6 address: greeterAddr,
7 abi: greeterABI,
8 functionName: 'sponsoredSetGreeting',
9 args: [signed.req, signed.v, signed.r, signed.s],
10 })
11 } ...
12 })

Toto je jen standardní volání blockchainu pomocí knihovny viem (opens in a new tab).

Chytrý kontrakt

Nakonec musí Greeter.sol (opens in a new tab) ověřit podpis.

1 constructor(string memory _greeting) {
2 greeting = _greeting;
3
4 DOMAIN_SEPARATOR = keccak256(
5 abi.encode(
6 keccak256(
7 "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
8 ),
9 keccak256(bytes("Greeter")),
10 keccak256(bytes("1")),
11 block.chainid,
12 address(this)
13 )
14 );
15 }

Konstruktor vytvoří oddělovač domén (opens in a new tab), podobně jako kód uživatelského rozhraní výše. Provádění na blockchainu je mnohem dražší, takže jej počítáme pouze jednou.

1 struct GreetingRequest {
2 string greeting;
3 }

Toto je struktura, která se podepisuje. Zde máme pouze jedno pole.

1 bytes32 private constant GREETING_TYPEHASH =
2 keccak256("GreetingRequest(string greeting)");

Toto je identifikátor struktury (opens in a new tab). V uživatelském rozhraní se počítá pokaždé znovu.

1 function sponsoredSetGreeting(
2 GreetingRequest calldata req,
3 uint8 v,
4 bytes32 r,
5 bytes32 s
6 ) external {

Tato funkce přijme podepsaný požadavek a aktualizuje pozdrav.

1 // Vypočítat otisk EIP-712
2 bytes32 digest = keccak256(
3 abi.encodePacked(
4 "\x19\x01",
5 DOMAIN_SEPARATOR,
6 keccak256(
7 abi.encode(
8 GREETING_TYPEHASH,
9 keccak256(bytes(req.greeting))
10 )
11 )
12 )
13 );

Vytvořte hash (digest) v souladu s EIP-712 (opens in a new tab).

1 // Obnovit podepisujícího
2 address signer = ecrecover(digest, v, r, s);
3 require(signer != address(0), "Invalid signature");

Použijte ecrecover (opens in a new tab) k získání adresy podepisujícího. Všimněte si, že špatný podpis může stále vést k platné adrese, jen k nějaké náhodné.

1 // Použít pozdrav, jako by jej volal podepisující
2 greeting = req.greeting;
3 emit SetGreeting(signer, req.greeting);
4 }

Aktualizujte pozdrav.

Zranitelnosti

Toto není kód připravený pro produkci. Je zranitelný vůči významným útokům a chybí mu důležité funkce. Zde jsou některé z nich, spolu s návodem, jak je vyřešit.

Chcete-li vidět některé z těchto útoků, klikněte na tlačítka pod nadpisem Attacks a sledujte, co se stane. U tlačítka Invalid signature zkontrolujte konzoli serveru, abyste viděli odpověď na transakci.

Odepření služby (Denial of service) na serveru

Nejjednodušším útokem je útok typu odepření služby (denial-of-service) (opens in a new tab) na server. Server přijímá požadavky odkudkoli z internetu a na základě těchto požadavků odesílá transakce. Útočníkovi absolutně nic nebrání ve vydání hromady podpisů, ať už platných, nebo neplatných. Každý z nich vyvolá transakci. Nakonec serveru dojde ETH na placení za gas.

Jedním z řešení tohoto problému je omezit rychlost na jednu transakci na blok. Pokud je účelem zobrazovat pozdravy externě vlastněným účtům (externally owned accounts), stejně nezáleží na tom, jaký je pozdrav uprostřed bloku.

Dalším řešením je sledovat adresy a povolit podpisy pouze od platných zákazníků.

Podpisy pro nesprávný pozdrav

Když kliknete na Signature for wrong greeting, odešlete platný podpis pro konkrétní adresu (0xaA92c5d426430D4769c9E878C1333BDe3d689b3e) a pozdrav (Hello). Odešle se to ale s jiným pozdravem. To zmate ecrecover, což změní pozdrav, ale s nesprávnou adresou.

Chcete-li tento problém vyřešit, přidejte adresu do podepsané struktury (opens in a new tab). Tímto způsobem se náhodná adresa z ecrecover nebude shodovat s adresou v podpisu a chytrý kontrakt zprávu odmítne.

Replay útoky

Když kliknete na Replay attack, odešlete stejný podpis „Jsem 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e a chtěl bych, aby pozdrav byl Hello“, ale se správným pozdravem. V důsledku toho se chytrý kontrakt domnívá, že adresa (která není vaše) změnila pozdrav zpět na Hello. Informace k provedení tohoto kroku jsou veřejně dostupné v informacích o transakci (opens in a new tab).

Pokud to představuje problém, jedním z řešení je přidat nonce (opens in a new tab). Vytvořte mapování (mapping) (opens in a new tab) mezi adresami a čísly a přidejte do podpisu pole nonce. Pokud se pole nonce shoduje s mapováním pro danou adresu, přijměte podpis a zvyšte hodnotu v mapování pro příště. Pokud se neshoduje, transakci odmítněte.

Dalším řešením je přidat k podepsaným datům časové razítko a přijmout podpis jako platný pouze několik sekund po tomto časovém razítku. Je to jednodušší a levnější, ale riskujeme replay útoky v rámci tohoto časového okna a selhání legitimních transakcí, pokud je časové okno překročeno.

Další chybějící funkce

Existují další funkce, které bychom v produkčním prostředí přidali.

Přístup z jiných serverů

V současné době umožňujeme jakékoli adrese odeslat sponsorSetGreeting. To může být přesně to, co chceme, v zájmu decentralizace. Nebo možná chceme zajistit, aby sponzorované transakce procházely přes náš server, v takovém případě bychom v chytrém kontraktu zkontrolovali msg.sender.

Ať tak či onak, mělo by jít o vědomé rozhodnutí při návrhu, nikoli jen o výsledek toho, že jsme o problému nepřemýšleli.

Zpracování chyb

Uživatel odešle pozdrav. Možná se aktualizuje v dalším bloku. Možná ne. Chyby jsou neviditelné. V produkčním systému by měl být uživatel schopen rozlišit mezi těmito případy:

  • Nový pozdrav ještě nebyl odeslán
  • Nový pozdrav byl odeslán a zpracovává se
  • Nový pozdrav byl odmítnut

Závěr

V tuto chvíli byste měli být schopni vytvořit pro uživatele vaší dapp zážitek bez nutnosti platit za gas, a to za cenu určité centralizace.

To však funguje pouze u chytrých kontraktů, které podporují ERC-712. Například k převodu tokenu ERC-20 je nutné, aby transakci podepsal vlastník, a ne jen zprávu. Řešením je abstrakce účtu (ERC-4337) (opens in a new tab). Doufám, že o tom napíšu budoucí tutoriál.

Zde najdete další mou práci (opens in a new tab).

Poslední aktualizace stránky: 3. března 2026

Byl tento návod užitečný?