Zum Hauptinhalt springen

Sponsoring von Gasgebühren: Wie Sie Transaktionskosten für Ihre Nutzer übernehmen

gaslos
Solidity
EIP-712
Meta-Transaktionen
Fortgeschritten
Ori Pomerantz
27. Februar 2026
11 Minuten Lesezeit

Einführung

Wenn wir wollen, dass Ethereum einer Milliarde weiterer Menschen (opens in a new tab) dient, müssen wir Reibungspunkte beseitigen und die Nutzung so einfach wie möglich machen. Eine Quelle dieser Reibung ist die Notwendigkeit von ETH, um Gasgebühren zu bezahlen.

Wenn Sie eine Dapp haben, die mit Nutzern Geld verdient, könnte es sinnvoll sein, Nutzer Transaktionen über Ihren Server einreichen zu lassen und die Transaktionsgebühren selbst zu bezahlen. Da die Nutzer weiterhin eine EIP-712-Autorisierungsnachricht (opens in a new tab) in ihren Wallets signieren, behalten sie die Integritätsgarantien von Ethereum. Die Verfügbarkeit hängt von dem Server ab, der die Transaktionen weiterleitet, und ist daher eingeschränkter. Sie können es jedoch so einrichten, dass Nutzer auch direkt auf den Smart Contract zugreifen können (wenn sie ETH erhalten), und andere ihre eigenen Server einrichten lassen, wenn sie Transaktionen sponsern möchten.

Die Technik in diesem Tutorial funktioniert nur, wenn Sie den Smart Contract kontrollieren. Es gibt andere Techniken, einschließlich der Kontoabstraktion (opens in a new tab), mit denen Sie Transaktionen an andere Smart Contracts sponsern können, was ich hoffentlich in einem zukünftigen Tutorial behandeln werde.

Hinweis: Dies ist kein produktionsreifer Code. Er ist anfällig für erhebliche Angriffe und es fehlen wichtige Funktionen. Erfahren Sie mehr im Abschnitt über Schwachstellen in diesem Leitfaden.

Voraussetzungen

Um dieses Tutorial zu verstehen, sollten Sie bereits vertraut sein mit:

Die Beispielanwendung

Die hier gezeigte Beispielanwendung ist eine Variante des Greeter-Vertrags von Hardhat. Sie können sie auf GitHub (opens in a new tab) ansehen. Der Smart Contract ist bereits auf Sepolia (opens in a new tab) unter der Adresse 0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA (opens in a new tab) bereitgestellt.

Um sie in Aktion zu sehen, befolgen Sie diese Schritte.

  1. Klonen Sie das Repository und installieren Sie die erforderliche Software.

    1git clone https://github.com/qbzzt/260301-gasless.git
    2cd 260301-gasless/server
    3npm install
1
22. Bearbeiten Sie `.env`, um `PRIVATE_KEY` auf ein Wallet zu setzen, das ETH auf Sepolia hat. Wenn Sie Sepolia-ETH benötigen, [verwenden Sie ein Faucet](/developers/docs/networks/#sepolia). Idealerweise sollte sich dieser Private-Key von dem in Ihrem Browser-Wallet unterscheiden.
3
43. Starten Sie den Server.
5
6 ```sh
7 npm run dev
  1. Rufen Sie die Anwendung unter der URL http://localhost:5173 (opens in a new tab) auf.

  2. Klicken Sie auf Connect with Injected, um sich mit einem Wallet zu verbinden. Bestätigen Sie dies im Wallet und genehmigen Sie bei Bedarf den Wechsel zu Sepolia.

  3. Schreiben Sie eine neue Begrüßung und klicken Sie auf Update greeting via sponsor.

  4. Signieren Sie die Nachricht.

  5. Warten Sie etwa 12 Sekunden (die Blockzeit auf Sepolia). Während Sie warten, können Sie sich die URL in der Serverkonsole ansehen, um die Transaktion zu sehen.

  6. Sehen Sie, dass sich die Begrüßung geändert hat und dass der Wert der Adresse, die zuletzt aktualisiert hat, nun die Adresse Ihres Browser-Wallets ist.

Um zu verstehen, wie das funktioniert, müssen wir uns ansehen, wie die Nachricht in der Benutzeroberfläche erstellt wird, wie sie vom Server weitergeleitet wird und wie der Smart Contract sie verarbeitet.

Die Benutzeroberfläche

Die Benutzeroberfläche basiert auf WAGMI (opens in a new tab); Sie können in diesem Tutorial darüber lesen.

So signieren wir die Nachricht:

1const signGreeting = useCallback(

Der React-Hook useCallback (opens in a new tab) ermöglicht es uns, die Leistung zu verbessern, indem wir dieselbe Funktion wiederverwenden, wenn die Komponente neu gezeichnet wird.

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

Wenn es kein Konto gibt, wird ein Fehler ausgelöst. Dies sollte niemals passieren, da die UI-Schaltfläche, die den Prozess startet, der signGreeting aufruft, in diesem Fall deaktiviert ist. Zukünftige Programmierer könnten diese Schutzmaßnahme jedoch entfernen, daher ist es eine gute Idee, diese Bedingung auch hier zu überprüfen.

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

Parameter für den Domain-Separator (opens in a new tab). Dieser Wert ist konstant, daher könnten wir ihn in einer besser optimierten Implementierung einmal berechnen, anstatt ihn bei jedem Aufruf der Funktion neu zu berechnen.

  • name ist ein für den Nutzer lesbarer Name, wie z. B. der Name der Dapp, für die wir Signaturen erstellen.
  • version ist die Version. Verschiedene Versionen sind nicht kompatibel.
  • chainId ist die Chain, die wir verwenden, wie sie von WAGMI (opens in a new tab) bereitgestellt wird.
  • verifyingContract ist die Vertragsadresse, die diese Signatur verifizieren wird. Wir möchten nicht, dass dieselbe Signatur für mehrere Verträge gilt, falls es mehrere Greeter-Verträge gibt und wir möchten, dass sie unterschiedliche Begrüßungen haben.
1
2 const types = {
3 GreetingRequest: [
4 { name: "greeting", type: "string" },
5 ],
6 }

Der Datentyp, den wir signieren. Hier haben wir einen einzigen Parameter, greeting, aber reale Systeme haben typischerweise mehr.

1 const message = { greeting }

Die eigentliche Nachricht, die wir signieren und senden möchten. greeting ist sowohl der Feldname als auch der Name der Variablen, die ihn füllt.

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

Die Signatur tatsächlich abrufen. Diese Funktion ist asynchron, da Nutzer (aus Sicht eines Computers) lange brauchen, um Daten zu signieren.

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 },

Die Funktion gibt einen einzelnen hexadezimalen Wert zurück. Hier teilen wir ihn in Felder auf.

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

Wenn sich eine dieser Variablen ändert, erstellen Sie eine neue Instanz der Funktion. Die Parameter account und chainId können vom Nutzer im Wallet geändert werden. contractAddr ist eine Funktion der Chain-ID. signTypedDataAsync sollte sich nicht ändern, aber wir importieren es aus einem Hook (opens in a new tab), daher können wir nicht sicher sein, und es ist am besten, es hier hinzuzufügen.

Nachdem die neue Begrüßung nun signiert ist, müssen wir sie an den Server senden.

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

Diese Funktion nimmt eine Signatur und sendet sie an den Server.

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

Senden Sie an den Pfad /server/sponsor auf dem Server, von dem wir gekommen sind.

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

Verwenden Sie POST, um die Informationen JSON-codiert zu senden.

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

Geben Sie die Antwort aus. Auf einem Produktionssystem würden wir die Antwort auch dem Nutzer anzeigen.

Der Server

Ich verwende gerne Vite (opens in a new tab) als mein Front-End. Es stellt automatisch die React-Bibliotheken bereit und aktualisiert den Browser, wenn sich der Front-End-Code ändert. Vite enthält jedoch keine Backend-Tools.

Die Lösung befindet sich in index.js (opens in a new tab).

1 app.post("/server/sponsor", async (req, res) => {
2 ...
3 })
4
5 // Lass Vite alles andere erledigen
6 const vite = await createViteServer({
7 server: { middlewareMode: true }
8 })
9
10 app.use(vite.middlewares)

Zuerst registrieren wir einen Handler für die Anfragen, die wir selbst bearbeiten (POST an /server/sponsor). Dann erstellen und verwenden wir einen Vite-Server, um alle anderen URLs zu verarbeiten.

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 })

Dies ist nur ein standardmäßiger viem (opens in a new tab)-Blockchain-Aufruf.

Der Smart Contract

Schließlich muss Greeter.sol (opens in a new tab) die Signatur verifizieren.

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 }

Der Konstruktor erstellt den Domain-Separator (opens in a new tab), ähnlich wie der obige Benutzeroberflächen-Code. Die Ausführung auf der Blockchain ist viel teurer, daher berechnen wir ihn nur einmal.

1 struct GreetingRequest {
2 string greeting;
3 }

Dies ist die Struktur, die signiert wird. Hier haben wir nur ein Feld.

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

Dies ist der Struktur-Identifikator (opens in a new tab). Er wird jedes Mal in der Benutzeroberfläche berechnet.

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

Diese Funktion empfängt eine signierte Anfrage und aktualisiert die Begrüßung.

1 // EIP-712-Digest berechnen
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 );

Erstellen Sie den Digest in Übereinstimmung mit EIP 712 (opens in a new tab).

1 // Signer wiederherstellen
2 address signer = ecrecover(digest, v, r, s);
3 require(signer != address(0), "Invalid signature");

Verwenden Sie ecrecover (opens in a new tab), um die Adresse des Unterzeichners zu erhalten. Beachten Sie, dass eine fehlerhafte Signatur dennoch zu einer gültigen Adresse führen kann, nur eben zu einer zufälligen.

1 // Begrüßung anwenden, als hätte der Signer sie aufgerufen
2 greeting = req.greeting;
3 emit SetGreeting(signer, req.greeting);
4 }

Aktualisieren Sie die Begrüßung.

Schwachstellen

Dies ist kein produktionsreifer Code. Er ist anfällig für erhebliche Angriffe und es fehlen wichtige Funktionen. Hier sind einige davon, zusammen mit Lösungsansätzen.

Um einige dieser Angriffe zu sehen, klicken Sie auf die Schaltflächen unter der Überschrift Attacks und sehen Sie, was passiert. Für die Schaltfläche Invalid signature überprüfen Sie die Serverkonsole, um die Transaktionsantwort zu sehen.

Denial-of-Service auf dem Server

Der einfachste Angriff ist ein Denial-of-Service (opens in a new tab)-Angriff auf den Server. Der Server empfängt Anfragen von überall im Internet und sendet basierend auf diesen Anfragen Transaktionen. Es gibt absolut nichts, was einen Angreifer daran hindert, eine Reihe von Signaturen auszustellen, ob gültig oder ungültig. Jede wird eine Transaktion verursachen. Letztendlich wird dem Server das ETH ausgehen, um für Gas zu bezahlen.

Eine Lösung für dieses Problem besteht darin, die Rate auf eine Transaktion pro Block zu begrenzen. Wenn der Zweck darin besteht, Begrüßungen für Extern verwaltete Konten anzuzeigen, spielt es ohnehin keine Rolle, wie die Begrüßung in der Mitte des Blocks lautet.

Eine weitere Lösung besteht darin, Adressen nachzuverfolgen und nur Signaturen von gültigen Kunden zuzulassen.

Falsche Begrüßungssignaturen

Wenn Sie auf Signature for wrong greeting klicken, übermitteln Sie eine gültige Signatur für eine bestimmte Adresse (0xaA92c5d426430D4769c9E878C1333BDe3d689b3e) und Begrüßung (Hello). Aber sie wird mit einer anderen Begrüßung übermittelt. Dies verwirrt ecrecover, was die Begrüßung ändert, aber die falsche Adresse hat.

Um dieses Problem zu lösen, fügen Sie die Adresse zur signierten Struktur (opens in a new tab) hinzu. Auf diese Weise stimmt die zufällige Adresse von ecrecover nicht mit der Adresse in der Signatur überein, und der Smart Contract wird die Nachricht ablehnen.

Replay-Angriffe

Wenn Sie auf Replay attack klicken, übermitteln Sie dieselbe Signatur „Ich bin 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e und ich möchte, dass die Begrüßung Hello lautet“, aber mit der korrekten Begrüßung. Infolgedessen glaubt der Smart Contract, dass die Adresse (die nicht Ihre ist) die Begrüßung wieder in Hello geändert hat. Die Informationen dazu sind öffentlich in den Transaktionsinformationen (opens in a new tab) verfügbar.

Wenn dies ein Problem darstellt, besteht eine Lösung darin, eine Nonce (opens in a new tab) hinzuzufügen. Erstellen Sie ein Mapping (opens in a new tab) zwischen Adressen und Zahlen und fügen Sie der Signatur ein Nonce-Feld hinzu. Wenn das Nonce-Feld mit dem Mapping für die Adresse übereinstimmt, akzeptieren Sie die Signatur und erhöhen Sie das Mapping für das nächste Mal. Wenn nicht, lehnen Sie die Transaktion ab.

Eine weitere Lösung besteht darin, den signierten Daten einen Zeitstempel hinzuzufügen und die Signatur nur für wenige Sekunden nach diesem Zeitstempel als gültig zu akzeptieren. Dies ist einfacher und billiger, aber wir riskieren Replay-Angriffe innerhalb des Zeitfensters und das Fehlschlagen legitimer Transaktionen, wenn das Zeitfenster überschritten wird.

Weitere fehlende Funktionen

Es gibt zusätzliche Funktionen, die wir in einer Produktionsumgebung hinzufügen würden.

Zugriff von anderen Servern

Derzeit erlauben wir jeder Adresse, ein sponsorSetGreeting zu übermitteln. Dies könnte im Interesse der Dezentralisierung genau das sein, was wir wollen. Oder vielleicht möchten wir sicherstellen, dass gesponserte Transaktionen über unseren Server laufen, in welchem Fall wir msg.sender im Smart Contract überprüfen würden.

So oder so sollte dies eine bewusste Designentscheidung sein und nicht nur das Ergebnis davon, dass man nicht über das Problem nachgedacht hat.

Fehlerbehandlung

Ein Nutzer übermittelt eine Begrüßung. Vielleicht wird sie beim nächsten Block aktualisiert. Vielleicht auch nicht. Fehler sind unsichtbar. Auf einem Produktionssystem sollte der Nutzer zwischen diesen Fällen unterscheiden können:

  • Die neue Begrüßung wurde noch nicht übermittelt
  • Die neue Begrüßung wurde übermittelt und wird verarbeitet
  • Die neue Begrüßung wurde abgelehnt

Fazit

An diesem Punkt sollten Sie in der Lage sein, ein gasloses Erlebnis für die Nutzer Ihrer Dapp zu schaffen, auf Kosten einer gewissen Zentralisierung.

Dies funktioniert jedoch nur mit Smart Contracts, die ERC-712 unterstützen. Um beispielsweise einen ERC-20-Token zu übertragen, ist es erforderlich, dass die Transaktion vom Eigentümer signiert wird und nicht nur eine Nachricht. Die Lösung ist die Kontoabstraktion (ERC-4337) (opens in a new tab). Ich hoffe, in Zukunft ein Tutorial darüber zu schreiben.

Sehen Sie hier für weitere meiner Arbeiten (opens in a new tab).

Letzte Aktualisierung der Seite: 3. März 2026

War dieses Tutorial hilfreich?