Ana içeriğe atla

Gaz ücretlerine sponsor olmak: Kullanıcılarınızın işlem maliyetlerini nasıl karşılayabilirsiniz

gazsız
Solidity
EIP-712
meta işlemler
Orta düzey
Ori Pomerantz
27 Şubat 2026
9 dakikalık okuma

Giriş

Ethereum'un bir milyar insana daha (opens in a new tab) hizmet etmesini istiyorsak, sürtünmeyi ortadan kaldırmalı ve kullanımını olabildiğince kolaylaştırmalıyız. Bu sürtünmenin bir kaynağı, gaz ücretlerini ödemek için ETH'ye duyulan ihtiyaçtır.

Kullanıcılardan para kazanan bir merkeziyetsiz uygulamanız (dapp) varsa, kullanıcıların sunucunuz üzerinden işlem göndermesine izin vermek ve işlem ücretlerini kendiniz ödemek mantıklı olabilir. Kullanıcılar cüzdanlarında hala bir EIP-712 yetkilendirme mesajı (opens in a new tab) imzaladıkları için Ethereum'un bütünlük garantilerini korurlar. Kullanılabilirlik, işlemleri ileten sunucuya bağlıdır, bu nedenle daha sınırlıdır. Ancak, kullanıcıların akıllı sözleşmeye doğrudan erişebilmeleri (eğer ETH alırlarsa) ve işlemlere sponsor olmak isteyen başkalarının kendi sunucularını kurabilmeleri için her şeyi ayarlayabilirsiniz.

Bu eğitimdeki teknik yalnızca akıllı sözleşmeyi siz kontrol ettiğinizde işe yarar. Gelecekteki bir eğitimde ele almayı umduğum, diğer akıllı sözleşmelere yönelik işlemlere sponsor olmanızı sağlayan hesap soyutlama (opens in a new tab) da dahil olmak üzere başka teknikler de vardır.

Not: Bu, üretim düzeyinde bir kod değildir. Önemli saldırılara karşı savunmasızdır ve temel özelliklerden yoksundur. Bu kılavuzun güvenlik açıkları bölümünde daha fazla bilgi edinebilirsiniz.

Ön koşullar

Bu eğitimi anlamak için şunlara zaten aşina olmanız gerekir:

Örnek uygulama

Buradaki örnek uygulama, Hardhat'in Greeter sözleşmesinin bir varyantıdır. Bunu GitHub'da (opens in a new tab) görebilirsiniz. Akıllı sözleşme halihazırda Sepolia (opens in a new tab) üzerinde, 0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA (opens in a new tab) adresinde dağıtılmıştır.

Bunu çalışırken görmek için şu adımları izleyin.

  1. Depoyu klonlayın ve gerekli yazılımı yükleyin.

    1git clone https://github.com/qbzzt/260301-gasless.git
    2cd 260301-gasless/server
    3npm install
  2. PRIVATE_KEY değerini Sepolia'da ETH'si olan bir cüzdana ayarlamak için .env dosyasını düzenleyin. Sepolia ETH'ye ihtiyacınız varsa, bir musluk kullanın. İdeal olarak, bu özel anahtar tarayıcı cüzdanınızdakinden farklı olmalıdır.

  3. Sunucuyu başlatın.

    1npm run dev
  4. http://localhost:5173 (opens in a new tab) URL'sindeki uygulamaya gidin.

  5. Bir cüzdana bağlanmak için Connect with Injected seçeneğine tıklayın. Cüzdanda onaylayın ve gerekirse Sepolia'ya geçişi onaylayın.

  6. Yeni bir selamlama yazın ve Update greeting via sponsor seçeneğine tıklayın.

  7. Mesajı imzalayın.

  8. Yaklaşık 12 saniye (Sepolia'daki blok süresi) bekleyin. Beklerken işlemi görmek için sunucu konsolundaki URL'ye bakabilirsiniz.

  9. Selamlamanın değiştiğini ve son güncelleyen adres değerinin artık tarayıcı cüzdanınızın adresi olduğunu görün.

Bunun nasıl çalıştığını anlamak için mesajın kullanıcı arayüzünde nasıl oluşturulduğuna, sunucu tarafından nasıl iletildiğine ve akıllı sözleşmenin onu nasıl işlediğine bakmamız gerekir.

Kullanıcı arayüzü

Kullanıcı arayüzü WAGMI (opens in a new tab) tabanlıdır; bunun hakkında bu eğitimde okuyabilirsiniz.

Mesajı şu şekilde imzalıyoruz:

1const signGreeting = useCallback(

React kancası (hook) useCallback (opens in a new tab), bileşen yeniden çizildiğinde aynı işlevi yeniden kullanarak performansı artırmamızı sağlar.

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

Eğer bir hesap yoksa, bir hata oluşturun. Bu asla gerçekleşmemelidir çünkü signGreeting işlevini çağıran süreci başlatan kullanıcı arayüzü düğmesi bu durumda devre dışı bırakılır. Ancak, gelecekteki programcılar bu korumayı kaldırabilir, bu nedenle bu koşulu burada da kontrol etmek iyi bir fikirdir.

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

Etki alanı ayırıcısı (domain separator) (opens in a new tab) için parametreler. Bu değer sabittir, bu nedenle daha iyi optimize edilmiş bir uygulamada, işlev her çağrıldığında yeniden hesaplamak yerine bir kez hesaplayabiliriz.

  • name, imzalar ürettiğimiz merkeziyetsiz uygulamanın (dapp) adı gibi kullanıcı tarafından okunabilir bir addır.
  • version sürümdür. Farklı sürümler uyumlu değildir.
  • chainId, WAGMI tarafından (opens in a new tab) sağlandığı şekliyle kullandığımız zincirdir.
  • verifyingContract, bu imzayı doğrulayacak sözleşme adresidir. Birden fazla Greeter sözleşmesi olması ve bunların farklı selamlamalara sahip olmasını istememiz durumunda, aynı imzanın birden fazla sözleşmeye uygulanmasını istemeyiz.
1
2 const types = {
3 GreetingRequest: [
4 { name: "greeting", type: "string" },
5 ],
6 }

İmzaladığımız veri türü. Burada tek bir parametremiz var, greeting, ancak gerçek hayattaki sistemler genellikle daha fazlasına sahiptir.

1 const message = { greeting }

İmzalamak ve göndermek istediğimiz asıl mesaj. greeting hem alan adıdır hem de onu dolduran değişkenin adıdır.

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

İmzayı gerçekten alın. Bu işlev asenkrondur çünkü kullanıcıların verileri imzalaması (bir bilgisayarın bakış açısından) uzun zaman alır.

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

İşlev tek bir onaltılık (hexadecimal) değer döndürür. Burada onu alanlara ayırıyoruz.

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

Bu değişkenlerden herhangi biri değişirse, işlevin yeni bir örneğini oluşturun. account ve chainId parametreleri kullanıcı tarafından cüzdanda değiştirilebilir. contractAddr, zincir kimliğinin (chain Id) bir işlevidir. signTypedDataAsync değişmemelidir, ancak onu bir kancadan (hook) (opens in a new tab) içe aktarıyoruz, bu yüzden emin olamayız ve onu buraya eklemek en iyisidir.

Artık yeni selamlama imzalandığına göre, onu sunucuya göndermemiz gerekiyor.

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

Bu işlev bir imza alır ve onu sunucuya gönderir.

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

Geldiğimiz sunucudaki /server/sponsor yoluna gönderin.

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

Bilgileri JSON kodlu olarak göndermek için POST kullanın.

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

Yanıtın çıktısını alın. Bir üretim sisteminde yanıtı kullanıcıya da gösterirdik.

Sunucu

Ön uç (front-end) olarak Vite (opens in a new tab) kullanmayı seviyorum. React kütüphanelerini otomatik olarak sunar ve ön uç kodu değiştiğinde tarayıcıyı günceller. Ancak Vite, arka uç (backend) araçlarını içermez.

Çözüm index.js (opens in a new tab) içindedir.

1 app.post("/server/sponsor", async (req, res) => {
2 ...
3 })
4
5 // Geri kalan her şeyi Vite halletsin
6 const vite = await createViteServer({
7 server: { middlewareMode: true }
8 })
9
10 app.use(vite.middlewares)

Önce kendi işlediğimiz istekler için bir işleyici kaydederiz (/server/sponsor yoluna POST). Ardından diğer tüm URL'leri işlemek için bir Vite sunucusu oluşturur ve kullanırız.

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

Bu sadece standart bir viem (opens in a new tab) blokzincir çağrısıdır.

Akıllı sözleşme

Son olarak, Greeter.sol (opens in a new tab) dosyasının imzayı doğrulaması gerekir.

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 }

Kurucu (constructor), yukarıdaki kullanıcı arayüzü koduna benzer şekilde etki alanı ayırıcısını (opens in a new tab) oluşturur. Blokzincir yürütmesi çok daha pahalıdır, bu yüzden onu yalnızca bir kez hesaplarız.

1 struct GreetingRequest {
2 string greeting;
3 }

İmzalanan yapı budur. Burada sadece bir alanımız var.

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

Bu, yapı tanımlayıcısıdır (opens in a new tab). Kullanıcı arayüzünde her seferinde hesaplanır.

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

Bu işlev imzalı bir istek alır ve selamlamayı günceller.

1 // EIP-712 özetini hesapla
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 );

Özeti (digest) EIP 712 (opens in a new tab)'ye uygun olarak oluşturun.

1 // İmzalayanı kurtar
2 address signer = ecrecover(digest, v, r, s);
3 require(signer != address(0), "Invalid signature");

İmzalayanın adresini almak için ecrecover (opens in a new tab) kullanın. Kötü bir imzanın yine de geçerli bir adresle, sadece rastgele bir adresle sonuçlanabileceğini unutmayın.

1 // Selamlamayı sanki imzalayan çağırmış gibi uygula
2 greeting = req.greeting;
3 emit SetGreeting(signer, req.greeting);
4 }

Selamlamayı güncelleyin.

Güvenlik açıkları

Bu, üretim düzeyinde bir kod değildir. Önemli saldırılara karşı savunmasızdır ve temel özelliklerden yoksundur. İşte bazıları ve bunların nasıl çözüleceği.

Bu saldırılardan bazılarını görmek için Attacks başlığı altındaki düğmelere tıklayın ve ne olduğunu görün. Invalid signature düğmesi için, işlem yanıtını görmek üzere sunucu konsolunu kontrol edin.

Sunucuda hizmet reddi (Denial of service)

En kolay saldırı, sunucuya yönelik bir hizmet reddi (denial-of-service) (opens in a new tab) saldırısıdır. Sunucu, İnternet'in herhangi bir yerinden istekler alır ve bu isteklere dayanarak işlemler gönderir. Bir saldırganın geçerli veya geçersiz bir dizi imza yayınlamasını engelleyen hiçbir şey yoktur. Her biri bir işleme neden olacaktır. Sonunda sunucunun gaz ödemek için ETH'si tükenecektir.

Bu sorunun bir çözümü, oranı blok başına bir işlemle sınırlamaktır. Amaç harici olarak sahip olunan hesaplara (externally owned accounts) selamlamalar göstermekse, bloğun ortasındaki selamlamanın ne olduğunun zaten bir önemi yoktur.

Başka bir çözüm de adresleri takip etmek ve yalnızca geçerli müşterilerden gelen imzalara izin vermektir.

Yanlış selamlama imzaları

Signature for wrong greeting seçeneğine tıkladığınızda, belirli bir adres (0xaA92c5d426430D4769c9E878C1333BDe3d689b3e) ve selamlama (Hello) için geçerli bir imza gönderirsiniz. Ancak bunu farklı bir selamlama ile gönderir. Bu, selamlamayı değiştiren ancak yanlış adrese sahip olan ecrecover işlevinin kafasını karıştırır.

Bu sorunu çözmek için adresi imzalı yapıya (opens in a new tab) ekleyin. Bu şekilde, ecrecover rastgele adresi imzadaki adresle eşleşmeyecek ve akıllı sözleşme mesajı reddedecektir.

Tekrar (Replay) saldırıları

Replay attack seçeneğine tıkladığınızda, aynı "Ben 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e'yim ve selamlamanın Hello olmasını istiyorum" imzasını, ancak doğru selamlama ile gönderirsiniz. Sonuç olarak, akıllı sözleşme (size ait olmayan) adresin selamlamayı tekrar Hello olarak değiştirdiğine inanır. Bunu yapmak için gereken bilgiler işlem bilgilerinde (opens in a new tab) herkese açıktır.

Eğer bu bir sorunsa, çözümlerden biri bir nonce (opens in a new tab) eklemektir. Adresler ve sayılar arasında bir eşleme (mapping) (opens in a new tab) oluşturun ve imzaya bir nonce alanı ekleyin. Nonce alanı adresin eşlemesiyle eşleşirse, imzayı kabul edin ve bir sonraki sefer için eşlemeyi artırın. Eşleşmezse, işlemi reddedin.

Başka bir çözüm de imzalanan veriye bir zaman damgası eklemek ve imzayı yalnızca o zaman damgasından sonraki birkaç saniye için geçerli kabul etmektir. Bu daha basit ve daha ucuzdur, ancak zaman penceresi içinde tekrar saldırıları riskiyle ve zaman penceresi aşılırsa meşru işlemlerin başarısız olmasıyla karşı karşıya kalırız.

Eksik olan diğer özellikler

Bir üretim ortamında ekleyeceğimiz başka özellikler de vardır.

Diğer sunuculardan erişim

Şu anda, herhangi bir adresin bir sponsorSetGreeting göndermesine izin veriyoruz. Merkeziyetsizlik adına tam olarak istediğimiz şey bu olabilir. Ya da sponsorlu işlemlerin bizim sunucumuzdan geçmesini sağlamak isteyebiliriz, bu durumda akıllı sözleşmede msg.sender kontrolü yapardık.

Her iki durumda da bu, sadece konu hakkında düşünmemenin bir sonucu değil, bilinçli bir tasarım kararı olmalıdır.

Hata yönetimi

Bir kullanıcı bir selamlama gönderir. Belki bir sonraki blokta güncellenir. Belki de güncellenmez. Hatalar görünmezdir. Bir üretim sisteminde, kullanıcı bu durumları birbirinden ayırabilmelidir:

  • Yeni selamlama henüz gönderilmedi
  • Yeni selamlama gönderildi ve işlemde
  • Yeni selamlama reddedildi

Sonuç

Bu noktada, bir miktar merkezileşme pahasına, merkeziyetsiz uygulama (dapp) kullanıcılarınız için gazsız bir deneyim yaratabilmelisiniz.

Ancak bu yalnızca ERC-712'yi destekleyen akıllı sözleşmelerle çalışır. Örneğin bir ERC-20 Token transfer etmek için, sadece bir mesajın değil, işlemin sahibi tarafından imzalanması gerekir. Çözüm hesap soyutlama (ERC-4337) (opens in a new tab)'dır. Gelecekte bunun hakkında bir eğitim yazmayı umuyorum.

Çalışmalarımın daha fazlası için buraya bakın (opens in a new tab).

Sayfanın son güncellenme tarihi: 3 Mart 2026

Bu eğitim faydalı oldu mu?