Gazsız kullanıcılarınızın token tutmasına ve sözleşmeleri çağırmasına izin vermek
Giriş
Önceki bir makale, EIP-712 imzalarını kullanarak kendi uygulamanıza gazsız erişim sağlamayı tartışmıştı, ancak bu yalnızca kendi akıllı sözleşmelerinizle sınırlıdır. Hesap soyutlama kullanarak, iki tür işlemi kabul eden ve bunları istenen bir hedefe ileten akıllı sözleşme cüzdanları oluşturabiliriz:
- Belirli bir Harici Sahipli Hesap (EOA) tarafından gönderilen işlemler (bu, söz konusu EOA'nın ETH'ye sahip olmasını gerektirir)
- Herhangi bir yerden gönderilen, ancak aynı EOA tarafından imzalanan işlemler.
Bu şekilde, bir hesabın varlıkları (token'lar vb.) tutması ve gazı olan bir EOA'nın yapabileceği tüm işlevleri yerine getirmesi için gazsız bir yol sağlayabiliriz.
Neden isteği sadece iletemiyoruz?
ERC-20 ve ilgili standartlarda hesap sahibi, token sözleşmesini çağıran adres olan msg.sender (opens in a new tab)'dir ve bu adres her zaman işlemin başlatıcısı olan tx.origin (opens in a new tab) olmak zorunda değildir. Bu, güvenlik nedenleriyle (opens in a new tab) gereklidir. Bu, token transfer isteklerini iletirsek, token'ları kullanıcı tarafından kontrol edilen bir adres yerine ileticinin adresinden transfer etmeye çalışacakları anlamına gelir.
EOA adresini EIP-7702 (opens in a new tab) aracılığıyla kullanmanıza izin veren bir çözüm vardır, ancak bu potansiyel olarak tehlikeli bir yetki devri imzalamayı gerektirir, bu nedenle bunu yalnızca cüzdan sağlayıcısının onayladığı bir akıllı sözleşmeye yetki devretmek için kullanabilirsiniz. Bu eğitim için, kullanıcıya bir proxy (vekil) olarak bir akıllı sözleşme oluşturmanın çok daha basit olan yöntemini tercih ediyorum.
İşbaşında görmek
-
Hem Node (opens in a new tab) hem de Foundry (opens in a new tab)'ye sahip olduğunuzdan emin olun.
-
Uygulamayı klonlayın ve gerekli yazılımı yükleyin.
git clone https://github.com/qbzzt/260315-gasless-tokens.git cd 260315-gasless-tokens forge build cd server npm install -
SEPOLIA_PRIVATE_KEYdeğerini Sepolia'da ETH'si olan bir cüzdana ayarlamak için.envdosyasını düzenleyin. Sepolia ETH'ye ihtiyacınız varsa, almak için bir musluk kullanın. İdeal olarak, bu özel anahtar tarayıcı cüzdanınızdakinden farklı olmalıdır. -
Sunucuyu başlatın.
npm run dev -
http://localhost:5173(opens in a new tab) URL'sindeki uygulamaya gidin. -
Bir cüzdana bağlanmak için Connect with Injected (Enjekte Edilen ile Bağlan) seçeneğine tıklayın. Cüzdanda onaylayın ve gerekirse Sepolia'ya geçişi onaylayın.
-
Aşağı kaydırın ve Deploy UserProxy (slow process) (UserProxy'yi Dağıt (yavaş işlem)) seçeneğine tıklayın.
-
Kullanıcı proxy'sinin ne zaman dağıtıldığını görebilirsiniz çünkü UserProxy access (UserProxy erişimi) yanında bir adres belirir. 24 saniye (2 blok) beklediyseniz ve hala gerçekleşmediyse, değişiklikleri algılamada bir sorun olabilir.
Eğer durum buysa, Sepolia Gezgini (opens in a new tab)'ne gidin ve sunucu çıktısında
npm run devadresinde gördüğünüz dağıtım işlem hash'ini girin. Adresini görüntülemek için oluşturulan sözleşmeye tıklayın, ardından kopyalayın. Adresi Or enter existing proxy address (Veya mevcut proxy adresini girin) alanına yapıştırın, ardından Set proxy address (Proxy adresini ayarla) seçeneğine tıklayın. -
Token almak üzere ERC-20 sözleşmesinin
faucet(opens in a new tab) işlevine bir çağrı göndermek için Request more tokens for proxy (Proxy için daha fazla token iste) seçeneğine tıklayın. Cüzdandaki imzayı Onaylayın (Confirm). Elbette token'lar kullanıcının adresine değil, proxy'nin adresine ulaşır. -
Aşağı kaydırın ve Last transaction: (Son işlem:) altındaki bağlantıya tıklayın. Bu, size
faucetişlemini göstermek için tarayıcıyı açacaktır. -
amount to transfer (transfer edilecek miktar) kısmına bir ile bin arasında bir sayı girin. Token'ları kendi adresinize transfer etmek için Transfer'e tıklayın. İstek için Onayla'ya (Confirm) tıklamadan önce, imzalanan verinin opak (anlaşılmaz) olduğunu görün. Kullanıcılar ne imzaladıklarını anlamakta zorlanırlardı. Bunu aşağıda tartışacağımızı unutmayın.
-
İşlem onaylandıktan sonra, hem your balance (bakiyeniz) hem de proxy balance (proxy bakiyesi) içindeki değişikliği görmek için bekleyin. Sepolia'nın 12 saniyelik bir blok süresi olduğu için bunun da biraz zaman alacağını unutmayın.
Nasıl çalışır
Gazsız bir deneyim için, kullanıcıya yönelik bir kullanıcı arayüzüne, mesajları kullanıcı arayüzünden zincire yönlendirecek bir sunucuya ve bunları alıp doğrulayacak bir akıllı sözleşmeye ihtiyacımız var.
Cüzdan akıllı sözleşmesi
Bu, akıllı sözleşmedir (opens in a new tab). Amacı, talep etmek için kullanılan kanaldan bağımsız olarak gerçek sahibin talep ettiği her şeyi yapmak ve diğer her şeyi görmezden gelmektir. Bunu yapmak için, işlevleri çağrılacak bir hedef adres ve onu çağırmak için kullanılacak veriyi alır.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract UserProxy {
address immutable OWNER;
uint public nonce = 0;
Sahibinin kimliği ve mesajların tekrarlanmasını önlemek için bir nonce (opens in a new tab). Nonce bir public değişkeni olduğu için, Solidity derleyicisi ayrıca zincir dışı kodun değerini okumasına izin veren bir görünüm (view) işlevi olan nonce() (opens in a new tab) oluşturur.
bytes32 private constant SIGNED_ACCESS_TYPEHASH =
keccak256("SignedAccess(address target,bytes data,uint256 nonce)");
bytes32 private constant SIGNED_ACCESS_PAYABLE_TYPEHASH =
keccak256("SignedAccessPayable(address target,bytes data,uint256 nonce,uint256 value)");
bytes32 immutable DOMAIN_SEPARATOR;
EIP-712 imzalarını (opens in a new tab) doğrulamak için gereken bilgiler.
constructor(address owner_) {
OWNER = owner_;
Bir UserProxy, tek bir sahip adresine bağlıdır. Bu gereklidir çünkü varlıklara (ERC-20 token'ları, NFT'ler vb.) sahip olabilir. Farklı sahiplere ait varlıkları birbirine karıştırmak istemiyoruz.
DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
),
keccak256(bytes("UserProxy")),
keccak256(bytes("1")),
block.chainid,
address(this)
)
);
}
Etki alanı ayırıcısı (domain separator) (opens in a new tab). Derleme zamanında hesaplanamaz, çünkü zincir kimliğine (chain ID) ve sözleşme adresine bağlıdır. Bu, bir UserProxy'nin bir başkası için hazırlanmış bir mesajla kandırılmasını imkansız hale getirir.
event CallResult(address target, bytes returnData);
Bir çağrının sonuçlarını günlüğe kaydedin.
function directAccess(address target, bytes calldata data)
external returns (bytes memory) {
Bu işlev doğrudan sahip tarafından çağrılabilir. Hiçbir iletici mevcut değilse, sahip yine de varlıklara doğrudan blokzincir üzerinden erişebilir (kullanıcının ETH'si varsa).
require(msg.sender == OWNER, "Only owner can call");
(bool success, bytes memory returnData) = target.call(data);
require(success, "Call failed");
emit CallResult(target, returnData);
return returnData;
}
Eğer sahip tarafından doğrudan çağrılırsak, sağlanan çağrı verisi ile hedefi çağırın.
function signedAccess(
address target,
bytes calldata data,
uint8 v,
bytes32 r,
bytes32 s)
Bu, UserProxy'nin ana işlevidir. target ve data'nun yanı sıra bir imza alır.
external returns (bytes memory) {
// EIP-712 özetini hesapla
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(
abi.encode(
SIGNED_ACCESS_TYPEHASH,
target,
keccak256(data),
nonce
)
)
)
);
Özet (digest) ayrıca nonce'u da içerir, ancak bunu işlemden almamıza gerek yoktur; doğru değeri zaten biliyoruz. Yanlış nonce'a sahip bir imza reddedilecektir.
// İmzalayanı kurtar
address signer = ecrecover(digest, v, r, s);
require(signer == OWNER, "Signature invalid or not by owner");
İmza geçersizse, ecrecover genellikle farklı bir adres döndürür ve kabul edilmez.
(bool success, bytes memory returnData) = target.call(data);
require(success, "Call failed");
Kullanıcının bize çağırmamızı söylediği sözleşmeyi çağırın ve başarılı olmazsa geri alın.
emit CallResult(target, returnData);
nonce++; // Tekrarlamayı önlemek için nonce değerini artır
return returnData;
}
Başarılı olursa, bir günlük olayı yayınlayın ve nonce'u artırın.
function directAccessPayable(address target, uint value, bytes calldata data)
external payable returns (bytes memory) {
.
.
.
}
function signedAccessPayable(
.
.
.
}
}
Bunlar, sözleşmeden ETH transfer etmenize de olanak tanıyan neredeyse aynı varyantlardır.
İletici (Relayer)
İletici bir sunucu bileşenidir. JavaScript ile yazılmıştır; kaynak kodunu buradan (opens in a new tab) görebilirsiniz.
import express from "express";
import { createServer as createViteServer } from "vite";
import { createWalletClient, createPublicClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { sepolia } from 'viem/chains'
import dotenv from 'dotenv'
İhtiyacımız olan kütüphaneler. Bu, kullanıcı arayüzü kodunu sunmak için Vite (opens in a new tab) kullanan bir Express (opens in a new tab) sunucusudur. Blokzincir ile iletişim kurmak için Viem (opens in a new tab)'i ve işlemi gönderen adresin özel anahtarını okumak için dotenv (opens in a new tab)'i kullanıyoruz.
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
const UserProxy = require('../contracts/out/UserProxy.sol/UserProxy.json')
Bu, derlenmiş UserProxy'yi okumanın basit bir yoludur. UserProxy'yi çağırabilmek için ABI'ye ve onu bir kullanıcı için dağıtabilmek için derlenmiş koda ihtiyacımız var.
dotenv.config()
const sepoliaAccount = privateKeyToAccount(process.env.SEPOLIA_PRIVATE_KEY)
console.log("Using account:", sepoliaAccount.address)
.env dosyasını okuyun, adresi çıkarın ve konsola yazdırın.
const sepoliaClient = createWalletClient({
account: sepoliaAccount,
chain: sepolia,
transport: http("https://rpc.sentio.xyz/sepolia"),
})
const publicClient = createPublicClient({
chain: sepolia,
transport: http(),
})
Blokzincir ile konuşan Viem istemcileri.
const start = async () => {
const app = express()
Bir Express sunucusu çalıştırın.
app.use(express.json())
Express'e istek gövdesini okumasını ve eğer JSON ise ayrıştırmasını söyleyin.
app.post("/server/deploy", async (req, res) => {
Bu, proxy'yi dağıtma isteklerini işleyen koddur. Burada hizmet reddi (denial-of-service) (opens in a new tab) saldırılarına karşı savunmasız olduğumuzu unutmayın, çünkü bir saldırgan ETH'miz tükenene kadar proxy'yi dağıtma istekleriyle bize spam yapabilir. Bir üretim sisteminde, muhtemelen proxy'yi dağıtma isteğinin imzalanmasını ve imzalayanın mevcut bir müşteri olmasını şart koşardık.
try {
const ownerAddress = req.body.ownerAddress
İstekten sahibin adresini alın.
const txHash = await sepoliaClient.deployContract({
abi: UserProxy.abi,
bytecode: UserProxy.bytecode.object,
args: [ownerAddress],
account: sepoliaAccount,
})
console.log("Deployment transaction hash:", txHash)
const receipt = await publicClient.waitForTransactionReceipt({
hash: txHash,
})
Sözleşmeyi dağıtın (opens in a new tab) ve dağıtılana kadar bekleyin (opens in a new tab).
res.json({ contractAddress: receipt.contractAddress })
Her şey yolundaysa, proxy adresini kullanıcı arayüzüne döndürün.
} catch (err) {
console.error(err)
res.status(500).json({ error: err.message })
}
})
Bir sorun varsa, bildirin.
app.post("/server/message", async (req, res) => {
Bu, UserProxy sözleşmesi için kullanıcı mesajlarını işleyen koddur. Bu, hizmet reddi saldırısına karşı savunmasız olan başka bir noktadır.
try {
const { proxy, target, data, v, r, s } = req.body
const txHash = await sepoliaClient.writeContract({
address: proxy,
abi: UserProxy.abi,
functionName: 'signedAccess',
args: [target, data, v, r, s],
account: sepoliaAccount,
})
İstek verilerini alın ve proxy üzerinde signedAccess'i çağırmak için kullanın.
console.log("Message transaction hash:", txHash)
res.json({ txHash })
İşlem hash'ini geri bildirin. Bu, kullanıcı arayüzünün kullanıcının işlemi kontrol etmesi için bir URL görüntülemesini sağlar.
} catch (err) {
console.error(err)
res.status(500).json({ error: err.message })
}
})
Yine, bir sorun varsa bildirin.
// Geri kalan her şeyi Vite halletsin
const vite = await createViteServer({
server: { middlewareMode: true }
})
app.use(vite.middlewares)
app.listen(5173, () => {
console.log("Dev server running on http://localhost:5173");
})
}
start()
Diğer her şey için, kullanıcı arayüzünü bizim için sunmayı halleden Vite'i kullanın.
Kullanıcı arayüzü
Bu, kullanıcı arayüzü kodudur (opens in a new tab). Kodun çoğu, Token.jsx (opens in a new tab) istisnası dışında, bu makalede belgelenenle neredeyse aynıdır.
Token.jsx (opens in a new tab)'nin bazı kısımları, bu makaledeki Greeter.jsx (opens in a new tab)'e benzer. İşte yeni kısımlar.
import {
encodeFunctionData
} from 'viem'
Bu işlev (opens in a new tab), bir EVM işlev çağrısı için çağrı verisini oluşturur. Bu, kullanıcının çağrı verisini imzalayabilmesi için gereklidir.
import UserProxy from '../../contracts/out/UserProxy.sol/UserProxy.json'
Yukarıda açıklanan UserProxy.
import Erc20 from '../../contracts/out/Faucet.sol/FaucetToken.json'
Bu sözleşme (opens in a new tab), önemli bir işlev olan faucet()'nin eklenmesiyle çoğunlukla normal bir ERC-20 sözleşmesidir. Bu işlev, test amacıyla isteyen herkese token verir.
const erc20Addrs = {
// Sepolia
11155111: '0x4cBedDEDA88fDd9e116618a5cD71BB0E440C2A78'
}
FaucetToken için adres.
const Address = ({ address }) => {
if (!address) return null
return (
<a href={`https://eth-sepolia.blockscout.com/address/${address}?tab=read_write_contract`} target="_blank">{address}</a>
)
}
Bu bileşen, bir blok gezginindeki sözleşmeye giden bir bağlantıyla birlikte bir adres çıktısı verir.
const Token = () => {
...
Bu, işin çoğunu yapan ana bileşendir.
const [ balanceAmount, setBalanceAmount ] = useState("Loading...")
Kullanıcı adresinin token bakiyesi.
const [ proxyAddr, setProxyAddr ] = useState(null)
Kullanıcıya ait bir proxy'nin adresi.
const [ proxyBalanceAmount, setProxyBalanceAmount ] = useState("Loading...")
Proxy'nin token bakiyesi.
const [ newProxyAddr, setNewProxyAddr ] = useState("")
Bu alan, kullanıcı proxy adresini manuel olarak ayarladığında kullanılır. Proxy adresini manuel olarak ayarlama yeteneğine sahip olmak, kullanıcının her seferinde yeni bir tane dağıtmak (ve eski proxy'nin sahip olduğu tüm token'ları kaybetmek) yerine mevcut bir proxy'yi kullanmasına olanak tanır.
const [ txHash, setTxHash ] = useState(null)
Kullanıcının o işlemi kontrol edebilmesi için gezgine bir bağlantı göstermek amacıyla kullanılan son işlemin hash'i.
const [ transferToken, setTransferToken ] = useState("")
const [ transferAmount, setTransferAmount ] = useState("")
const [ transferTo, setTransferTo ] = useState("")
Bu alanların tümü, bir ERC-20 sözleşmesine token transfer komutları göndermek için kullanılır. Bu FaucetToken olabilir, ancak olmak zorunda değildir. transfer işlevi ERC-20 standardının bir parçasıdır.
const balance = useReadContract({
...
})
const proxyBalance = useReadContract({
...
})
İlgilendiğimiz iki token bakiyesini, kullanıcının ne kadarına sahip olduğunu ve proxy'nin ne kadarına sahip olduğunu okuyun.
const nonce = useReadContract({
address: proxyAddr,
abi: UserProxy.abi,
functionName: 'nonce',
args: [],
})
Tekrarlama saldırılarını (örneğin, bir satıcının kendisine para veren bir işlemi tekrarlaması) önlemek için bir nonce (opens in a new tab) kullanıyoruz. İmzaladığımız veriye eklemek için mevcut değeri bilmemiz gerekiyor.
useEffect(() => {
if (balance?.status === "success")
setBalanceAmount(balance.data / 10n**18n)
else
setBalanceAmount("Loading...")
}, [balance])
useEffect(() => {
if (proxyBalance?.status === "success")
setProxyBalanceAmount(proxyBalance.data / 10n**18n)
else
setProxyBalanceAmount("Loading...")
}, [proxyBalance])
Blokzincirden okunan bilgi değiştiğinde kullanıcıya gösterilen bakiyeyi güncellemek için useEffect (opens in a new tab) kullanın.
useEffect(() => {
setTransferToken(faucetAddr)
}, [faucetAddr])
useEffect(() => {
setTransferTo(account.address)
}, [account.address])
Varsayılan, FaucetToken token'larını kullanıcının kendi hesabına transfer etmektir. Burada bu değerleri Viem'den aldığımızda ayarlıyoruz.
const proxyAddressChange = (evt) => setNewProxyAddr(evt.target.value)
const transferTokenChange = (evt) => setTransferToken(evt.target.value)
const transferToChange = (evt) => setTransferTo(evt.target.value)
const transferAmountChange = (evt) => setTransferAmount(evt.target.value)
Metin alanları değiştiğinde çalışacak olay işleyicileri.
const deployUserProxy = async () => {
try {
const response = await fetch("/server/deploy", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ownerAddress: account.address })
})
const data = await response.json()
setProxyAddr(data.contractAddress)
} catch (err) {
console.error("Error:", err)
}
}
Sunucudan bu kullanıcı için bir proxy dağıtmasını isteyin.
const signMessage = async(proxyAddr, target, calldata) => {
Zincir içi UserProxy'ye göndermesi için sunucuya iletmeden önce bir mesaj imzalayın. Bu burada açıklanmıştır. Hem hedef adres (çağırdığımız token'ın adresi) hem de gönderilecek çağrı verisi ile bir mesaj imzalamamız gerekiyor.
const domain = {
.
.
.
return {v, r, s}
}
const messageUserProxy = async (proxy, target, data, v, r, s) => {
İmzayı doğrulayacak ve ardından target'a gönderecek olan UserProxy'ye imzalı bir mesaj gönderin.
try {
const response = await fetch("/server/message", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
proxy, target, // her iki adres
data, // hedefe gönderilecek çağrı verisi
v, r, s // imza
})
})
const serverResponse = await response.json()
setTxHash(serverResponse.txHash)
} catch (err) {
console.error("Error:", err)
}
}
Sunucuya bir istek gönderin ve yanıtı aldığınızda işlem hash'ini alın.
const faucetSimulation = useSimulateContract({
address: faucetAddr,
abi: Erc20.abi,
functionName: 'faucet',
account: account.address
})
faucet işlevini çağırmayı simüle edin. Musluk düğmesini yalnızca bu başarılı olursa etkinleştiririz.
const proxyFaucet = async () => {
const calldata = encodeFunctionData({
abi: Erc20.abi,
functionName: 'faucet',
args: [],
})
const {v, r, s} = await signMessage(proxyAddr, calldata)
messageUserProxy(proxyAddr, faucetAddr, calldata, v, r, s)
}
const proxyTransfer = async () => {
const calldata = encodeFunctionData({
abi: Erc20.abi,
functionName: 'transfer',
args: [transferTo, BigInt(transferAmount) * 10n**18n],
})
const {v, r, s} = await signMessage(proxyAddr, transferToken, calldata)
messageUserProxy(proxyAddr, transferToken, calldata, v, r, s)
}
Sunucu ve UserProxy aracılığıyla bir işlevi çağırmak için üç adımı izleriz:
-
encodeFunctionData(opens in a new tab) kullanarak imzalanacak ve gönderilecek çağrı verisini oluşturun. -
Mesajı imzalayın (hedef adres, çağrı verisi ve nonce).
-
Mesajı sunucuya gönderin.
return (
<>
<div align="left">
<h2>Token</h2>
<h4>Token contract address <Address address={faucetAddr} /></h4>
<hr />
<h4>Direct access (as <Address address={account?.address} />)</h4>
Your balance: {balanceAmount}
<br />
<button disabled={!faucetSimulation.data}
onClick={() => writeContract(
faucetSimulation.data.request
)}
>
Request more tokens
</button>
<hr />
Bileşenin bu kısmı, FaucetToken'yi doğrudan tarayıcıdan kullanmanıza olanak tanır. Temel amacı hata ayıklamayı kolaylaştırmaktır.
<h4>UserProxy access <Address address={proxyAddr} /></h4>
<button onClick={deployUserProxy}>
Deploy UserProxy (slow process)
</button>
Kullanıcının yeni bir UserProxy dağıtmasına izin verin.
<br /><br />
<input type="text" placeholder="Veya mevcut proxy adresini girin" value={newProxyAddr} onChange={proxyAddressChange} />
<br /><br />
<button
onClick={() => setProxyAddr(newProxyAddr)}
disabled={newProxyAddr.match(/^0x[a-fA-F0-9]{40}$/) === null}
>
Set proxy address
</button>
Kullanıcıların yalnızca meşru bir adres girdiklerinde Set proxy address (Proxy adresini ayarla) seçeneğine tıklamasına izin verin. Bunun, söz konusu adresin gerçekten bir UserProxy sözleşmesi olduğunu garanti etmediğini unutmayın. Böyle bir kontrol eklemek mümkündür, ancak çok daha yavaş olacaktır (daha kötü kullanıcı deneyimi) ve güvenliği artırmayacaktır (saldırganlar kullanıcı arayüzü için her zaman kendi kodlarını kullanabilirler).
<br /><br />
{ proxyAddr && (
Geri kalanını yalnızca meşru bir proxy adresi varsa gösterin.
<>
Proxy balance: {proxyBalanceAmount}
<br />
Proxy nonce: {nonce?.data?.toString() ?? "Loading..."}
Kullanıcının nonce'u bilmesine gerek yoktur; bu sadece hata ayıklama amaçlıdır.
<br />
<button disabled={!proxyAddr || proxyAddr === "Loading..." || nonce?.status !== 'success'}
onClick={proxyFaucet}
>
Request more tokens for proxy
</button>
Proxy üzerinden faucet()'ye yapılan bir çağrıyı simüle edemeyiz. Ancak, en azından bir proxy'miz olduğundan ve proxy'nin bize bir nonce bildirdiğinden emin olabiliriz.
<hr />
<h4>Transfer tokens from proxy</h4>
<ul>
<li> Token to transfer: <input type="text" placeholder="Token to transfer" value={transferToken} onChange={transferTokenChange} /> </li>
<li> Recipient address: <input type="text" placeholder="Recipient address" value={transferTo} onChange={transferToChange} /> </li>
<li> Amount to transfer: <input type="number" placeholder="Amount to transfer" value={transferAmount} onChange={transferAmountChange} /> </li>
</ul>
<button disabled={!proxyAddr || proxyAddr === "Loading..." || nonce?.status !== 'success'}
onClick={proxyTransfer}
>
Transfer
</button>
</>
)}
Kullanıcının ERC-20 transfer işlemleri düzenlemesine izin verin.
<hr />
{ txHash && (
<>
<h4>Last transaction:</h4>
<a href={`https://eth-sepolia.blockscout.com/tx/${txHash}`} target="_blank">
{txHash}
</a>
</>
)}
Son bir işlem hash'i varsa, kullanıcının bunu bir blok gezgininde görüntüleyebilmesi için bir bağlantı gösterin.
</div>
</>
)
}
export {Token}
Bu sadece React şablon kodudur (boilerplate).
Güvenlik Açıkları
Sunucumuz hizmet reddi saldırılarına karşı savunmasızdır. Bu saldırı serinin önceki makalesinde açıklanmıştır.
Ek olarak, kötü kullanıcı davranışını teşvik ediyoruz. Kullanıcıdan imzalamasını istediğimiz şey şudur:
Bunun, kullanıcının transfer etmek istediği token, miktar ve hedef adres için meşru bir ERC-20 transferi olduğunu biz biliyoruz. Ancak çoğu kullanıcı çağrı verisini nasıl yorumlayacağını bilmez ve ne imzaladıkları hakkında hiçbir fikirleri yoktur. Bu, iki nedenden dolayı kötü bir tasarımdır:
- Bazı kullanıcılar, imzalamalarını söylediğimiz verilere güvenmedikleri için bizi kullanmayacaktır.
- Diğer kullanıcılar bize güvenecek ve ne olduğunu anlamadan sadece çağrı verisini imzalamaları gerektiğini öğreneceklerdir. Bu, Saldırgan Adam onları kendi web sitesine yönlendirmeyi başarırsa, kullanıcının sahip olduğu tüm USDC'yi (veya DAI veya başka herhangi bir ERC-20'yi) kendisine veren bir işlemi imzalamalarını sağlayabileceği anlamına gelir.
Çözüm, UserProxy içinde transfer gibi yaygın olarak kullanılan işlevler için ayrı işlevlere sahip olmaktır. O zaman kullanıcılar anladıkları bir şeyi imzalayabilirler.
Not: Kullanıcılar istedikleri herhangi bir cüzdanı kullanabilseler de, EIP-712 kullanan uygulamaların onları tüm imza verilerini gösteren (opens in a new tab) bir cüzdan kullanmaya teşvik etmesi şiddetle tavsiye edilir. Bazı cüzdanlar adresi keser, bu da güvensizdir. Bir saldırgan, aynı başlangıç ve bitiş karakterlerine sahip olan ancak ortası farklı olan bir adres oluşturabilir.
Sonuç
Yukarıdaki güvenlik açıklarına ek olarak, bu eğitimdeki çözümün Ethereum'un ele almamıza yardımcı olabileceği birkaç dezavantajı vardır.
- Sansür direnci. Şu anda kullanıcılar sizin sunucunuzu, başkası tarafından kurulan rakip bir sunucuyu kullanabilir veya doğrudan Ethereum'a bağlanabilir, bu da gaz maliyetlerine neden olur. ERC-4337 (opens in a new tab) kullanmak, kullanıcıların işlemlerini geniş bir sunucu havuzuna sunmalarına olanak tanıyarak işlemlerinin sansürlenme olasılığını azaltır.
- EOA'nın sahip olduğu varlıklar. Yukarıda belirtildiği gibi, EIP-7702 (opens in a new tab), halihazırda bir EOA adresine ait olan varlıkları yönetmek için kullanılabilir. Bunun zorlukları vardır, ancak bazen gereklidir.
Yakın gelecekte bu özellikleri eklemekle ilgili eğitimler yayınlamayı umuyorum.
Çalışmalarımın daha fazlası için buraya bakın (opens in a new tab).


