Membiarkan pengguna tanpa gas Anda menyimpan token dan memanggil kontrak
Pengantar
Artikel sebelumnya membahas penggunaan akses tanpa gas ke aplikasi Anda sendiri menggunakan tanda tangan EIP-712, tetapi ini terbatas pada kontrak pintar Anda sendiri. Menggunakan abstraksi akun, kita dapat membuat dompet kontrak pintar yang menerima dua jenis transaksi dan meneruskannya ke tujuan yang diminta:
- Transaksi yang dikirim oleh EOA tertentu (yang mewajibkan EOA tersebut memiliki ETH)
- Transaksi yang dikirim dari mana saja, tetapi ditandatangani oleh EOA yang sama.
Dengan cara ini, kita dapat menyediakan cara tanpa gas bagi sebuah akun untuk menyimpan aset (token, dll.) dan melakukan semua fungsi yang dapat dilakukan oleh EOA dengan gas.
Mengapa kita tidak bisa sekadar meneruskan permintaan tersebut?
Dalam ERC-20 dan standar terkait, pemilik akun adalah msg.sender (opens in a new tab), alamat yang memanggil kontrak token, yang belum tentu merupakan pembuat transaksi, tx.origin (opens in a new tab). Ini diwajibkan karena alasan keamanan (opens in a new tab). Ini berarti bahwa jika kita meneruskan permintaan transfer token, permintaan tersebut akan mencoba mentransfer token dari alamat penerus (relayer) alih-alih dari alamat yang dikendalikan oleh pengguna.
Ada solusi yang memungkinkan Anda menggunakan alamat EOA melalui EIP-7702 (opens in a new tab), tetapi ini mewajibkan penandatanganan pendelegasian yang berpotensi berbahaya, sehingga Anda hanya dapat menggunakannya untuk mendelegasikan ke kontrak pintar yang disetujui oleh penyedia dompet. Untuk tutorial ini, saya lebih memilih metode yang jauh lebih sederhana yaitu membuat kontrak pintar sebagai proksi bagi pengguna.
Melihatnya beraksi
-
Pastikan Anda memiliki Node (opens in a new tab) dan Foundry (opens in a new tab).
-
Kloning aplikasi dan instal perangkat lunak yang diperlukan.
git clone https://github.com/qbzzt/260315-gasless-tokens.git cd 260315-gasless-tokens forge build cd server npm install -
Edit
.envuntuk mengaturSEPOLIA_PRIVATE_KEYke dompet yang memiliki ETH di Sepolia. Jika Anda membutuhkan ETH Sepolia, gunakan faucet untuk mendapatkannya. Idealnya, kunci privat ini harus berbeda dari yang Anda miliki di dompet peramban Anda. -
Mulai server.
npm run dev -
Buka aplikasi di URL
http://localhost:5173(opens in a new tab). -
Klik Connect with Injected untuk terhubung ke dompet. Setujui di dompet, dan setujui perubahan ke Sepolia jika perlu.
-
Gulir ke bawah dan klik Deploy UserProxy (slow process).
-
Anda dapat melihat kapan proksi pengguna disebarkan karena ada alamat di sebelah UserProxy access. Jika Anda menunggu 24 detik (2 blok) dan itu masih belum terjadi, mungkin ada masalah dengan pendeteksian perubahan.
Jika demikian, buka Penjelajah Sepolia (opens in a new tab) dan masukkan hash transaksi penyebaran yang Anda lihat di keluaran server pada
npm run dev. Klik kontrak yang dibuat untuk melihat alamatnya, lalu salin. Tempelkan alamat di bidang Or enter existing proxy address, lalu klik Set proxy address. -
Klik Request more tokens for proxy untuk mengirimkan panggilan ke fungsi
faucet(opens in a new tab) dari kontrak ERC-20 untuk mendapatkan token. Konfirmasi tanda tangan di dompet. Tentu saja, token tersebut masuk ke alamat proksi, bukan alamat pengguna. -
Gulir ke bawah dan klik tautan di bawah Last transaction:. Ini akan membuka peramban untuk menunjukkan kepada Anda transaksi
faucet. -
Di bagian amount to transfer, masukkan angka antara satu dan seribu. Klik Transfer untuk mentransfer token ke alamat Anda sendiri. Sebelum Anda mengeklik Konfirmasi untuk permintaan tersebut, perhatikan bahwa data yang ditandatangani tidak jelas (opaque). Pengguna akan kesulitan memahami apa yang mereka tandatangani. Ingatlah bahwa kita akan membahasnya di bawah.
-
Setelah transaksi dikonfirmasi, tunggu untuk melihat perubahan pada your balance dan proxy balance. Perhatikan bahwa ini juga akan memakan waktu, karena Sepolia memiliki waktu blok 12 detik.
Cara kerjanya
Untuk pengalaman tanpa gas, kita memerlukan antarmuka pengguna untuk pengguna, server untuk merutekan pesan dari antarmuka pengguna ke rantai, dan kontrak pintar untuk menerima dan memverifikasinya.
Kontrak pintar dompet
Ini adalah kontrak pintar (opens in a new tab). Tujuannya adalah untuk melakukan apa pun yang diminta oleh pemilik asli, terlepas dari saluran yang digunakan untuk memintanya, dan mengabaikan hal lainnya. Untuk melakukan ini, fungsi-fungsinya menerima alamat target untuk dipanggil dan data untuk digunakan memanggilnya.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract UserProxy {
address immutable OWNER;
uint public nonce = 0;
Identitas pemilik dan sebuah nonce (opens in a new tab) untuk mencegah pesan diulang. Karena nonce adalah variabel public, kompiler Solidity juga membuat fungsi view, nonce() (opens in a new tab), yang memungkinkan kode offchain untuk membaca nilainya.
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;
Informasi yang diperlukan untuk memverifikasi tanda tangan EIP-712 (opens in a new tab).
constructor(address owner_) {
OWNER = owner_;
Sebuah UserProxy terikat pada satu alamat pemilik. Ini diperlukan karena ia dapat memiliki aset (token ERC-20, NFT, dll.). Kita tidak ingin mencampuradukkan aset milik pemilik yang berbeda.
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)
)
);
}
Pemisah domain (opens in a new tab). Ini tidak dapat dihitung pada waktu kompilasi, karena bergantung pada ID rantai dan alamat kontrak. Hal ini membuat UserProxy tidak mungkin tertipu oleh pesan yang disiapkan untuk yang lain.
event CallResult(address target, bytes returnData);
Log hasil panggilan.
function directAccess(address target, bytes calldata data)
external returns (bytes memory) {
Fungsi ini dapat dipanggil secara langsung oleh pemilik. Jika tidak ada penerus (relay) yang tersedia, pemilik masih dapat mengakses aset secara langsung di rantai blok (jika pengguna memiliki ETH).
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;
}
Jika kita dipanggil secara langsung oleh pemilik, panggil target dengan data panggilan (calldata) yang disediakan.
function signedAccess(
address target,
bytes calldata data,
uint8 v,
bytes32 r,
bytes32 s)
Ini adalah fungsi utama UserProxy. Fungsi ini mendapatkan target dan data, serta sebuah tanda tangan.
external returns (bytes memory) {
// Hitung digest EIP-712
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(
abi.encode(
SIGNED_ACCESS_TYPEHASH,
target,
keccak256(data),
nonce
)
)
)
);
Intisari (digest) juga menyertakan nonce, tetapi kita tidak perlu menerimanya dari transaksi; kita sudah mengetahui nilai yang benar. Tanda tangan dengan nonce yang salah akan ditolak.
// Pulihkan penandatangan
address signer = ecrecover(digest, v, r, s);
require(signer == OWNER, "Signature invalid or not by owner");
Jika tanda tangan tidak valid, ecrecover biasanya akan mengembalikan alamat yang berbeda, dan itu tidak akan diterima.
(bool success, bytes memory returnData) = target.call(data);
require(success, "Call failed");
Panggil kontrak yang diperintahkan pengguna untuk kita panggil, dan kembalikan (revert) jika tidak berhasil.
emit CallResult(target, returnData);
nonce++; // Tingkatkan nonce untuk mencegah replay
return returnData;
}
Jika berhasil, pancarkan peristiwa log dan tingkatkan nonce.
function directAccessPayable(address target, uint value, bytes calldata data)
external payable returns (bytes memory) {
.
.
.
}
function signedAccessPayable(
.
.
.
}
}
Ini adalah varian yang hampir identik yang memungkinkan Anda juga mentransfer ETH keluar dari kontrak.
Penerus (relayer)
Penerus (relayer) adalah komponen server. Ini ditulis dalam JavaScript; Anda dapat melihat kode sumbernya di sini (opens in a new tab).
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'
Pustaka yang kita butuhkan. Ini adalah server Express (opens in a new tab), yang menggunakan Vite (opens in a new tab) untuk menyajikan kode antarmuka pengguna. Kita menggunakan Viem (opens in a new tab) untuk berkomunikasi dengan rantai blok, dan dotenv (opens in a new tab) untuk membaca kunci privat untuk alamat yang mengirimkan transaksi.
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
const UserProxy = require('../contracts/out/UserProxy.sol/UserProxy.json')
Ini adalah cara sederhana untuk membaca UserProxy yang telah dikompilasi. Kita memerlukan ABI untuk dapat memanggil UserProxy, dan kode yang dikompilasi untuk dapat menyebarkannya bagi pengguna.
dotenv.config()
const sepoliaAccount = privateKeyToAccount(process.env.SEPOLIA_PRIVATE_KEY)
console.log("Using account:", sepoliaAccount.address)
Baca file .env, ekstrak alamatnya, dan cetak ke konsol.
const sepoliaClient = createWalletClient({
account: sepoliaAccount,
chain: sepolia,
transport: http("https://rpc.sentio.xyz/sepolia"),
})
const publicClient = createPublicClient({
chain: sepolia,
transport: http(),
})
Klien Viem yang berbicara dengan rantai blok.
const start = async () => {
const app = express()
Jalankan server Express.
app.use(express.json())
Beri tahu Express untuk membaca isi permintaan, dan jika itu JSON, urai (parse) isinya.
app.post("/server/deploy", async (req, res) => {
Ini adalah kode yang menangani permintaan untuk menyebarkan proksi. Perhatikan bahwa kita rentan terhadap serangan penolakan layanan (denial-of-service) (opens in a new tab) di sini karena penyerang dapat mengirimkan spam permintaan kepada kita untuk menyebarkan proksi hingga ETH kita habis. Pada sistem produksi, kita mungkin akan mewajibkan agar permintaan untuk menyebarkan proksi ditandatangani dan penandatangannya adalah pelanggan yang sudah ada.
try {
const ownerAddress = req.body.ownerAddress
Dapatkan alamat pemilik dari permintaan.
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,
})
Sebarkan kontrak (opens in a new tab) dan tunggu hingga disebarkan (opens in a new tab).
res.json({ contractAddress: receipt.contractAddress })
Jika semuanya baik-baik saja, kembalikan alamat proksi ke antarmuka pengguna.
} catch (err) {
console.error(err)
res.status(500).json({ error: err.message })
}
})
Jika ada masalah, laporkan.
app.post("/server/message", async (req, res) => {
Ini adalah kode yang memproses pesan pengguna untuk kontrak UserProxy. Ini adalah titik lain yang rentan terhadap serangan penolakan layanan.
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,
})
Dapatkan data permintaan dan gunakan untuk memanggil signedAccess pada proksi.
console.log("Message transaction hash:", txHash)
res.json({ txHash })
Laporkan kembali hash transaksi. Ini memungkinkan UI menampilkan URL bagi pengguna untuk memeriksa transaksi.
} catch (err) {
console.error(err)
res.status(500).json({ error: err.message })
}
})
Sekali lagi, jika ada masalah, laporkan.
// Biarkan Vite menangani sisanya
const vite = await createViteServer({
server: { middlewareMode: true }
})
app.use(vite.middlewares)
app.listen(5173, () => {
console.log("Dev server running on http://localhost:5173");
})
}
start()
Untuk hal lainnya, gunakan Vite, yang menangani penyajian antarmuka pengguna untuk kita.
Antarmuka pengguna
Ini adalah kode antarmuka pengguna (opens in a new tab). Sebagian besar kode hampir identik dengan yang didokumentasikan dalam artikel ini, dengan pengecualian Token.jsx (opens in a new tab).
Bagian dari Token.jsx (opens in a new tab) mirip dengan Greeter.jsx (opens in a new tab) dalam artikel ini. Berikut adalah bagian-bagian barunya.
import {
encodeFunctionData
} from 'viem'
Fungsi ini (opens in a new tab) membuat data panggilan (calldata) untuk panggilan fungsi EVM. Ini diperlukan agar pengguna dapat menandatangani data panggilan tersebut.
import UserProxy from '../../contracts/out/UserProxy.sol/UserProxy.json'
UserProxy, dijelaskan di atas.
import Erc20 from '../../contracts/out/Faucet.sol/FaucetToken.json'
Kontrak ini (opens in a new tab) sebagian besar adalah kontrak ERC-20 normal, dengan penambahan satu fungsi penting, faucet(). Fungsi ini memberikan token kepada siapa saja yang memintanya untuk tujuan pengujian.
const erc20Addrs = {
// Sepolia
11155111: '0x4cBedDEDA88fDd9e116618a5cD71BB0E440C2A78'
}
Alamat untuk FaucetToken.
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>
)
}
Komponen ini mengeluarkan alamat dengan tautan ke kontrak di penjelajah blok.
const Token = () => {
...
Ini adalah komponen utama yang melakukan sebagian besar pekerjaan.
const [ balanceAmount, setBalanceAmount ] = useState("Loading...")
Saldo token dari alamat pengguna.
const [ proxyAddr, setProxyAddr ] = useState(null)
Alamat proksi yang dimiliki oleh pengguna.
const [ proxyBalanceAmount, setProxyBalanceAmount ] = useState("Loading...")
Saldo token proksi.
const [ newProxyAddr, setNewProxyAddr ] = useState("")
Bidang ini digunakan saat pengguna mengatur alamat proksi secara manual. Memiliki kemampuan untuk mengatur alamat proksi secara manual memungkinkan pengguna menggunakan proksi yang ada alih-alih menyebarkan yang baru setiap saat (dan kehilangan semua token yang dimiliki oleh proksi lama).
const [ txHash, setTxHash ] = useState(null)
Hash dari transaksi terakhir, digunakan untuk menampilkan tautan ke penjelajah sehingga pengguna dapat memeriksa transaksi tersebut.
const [ transferToken, setTransferToken ] = useState("")
const [ transferAmount, setTransferAmount ] = useState("")
const [ transferTo, setTransferTo ] = useState("")
Semua bidang ini digunakan untuk mengirim perintah transfer token ke kontrak ERC-20. Ini mungkin FaucetToken, tetapi tidak harus demikian. Fungsi transfer adalah bagian dari standar ERC-20.
const balance = useReadContract({
...
})
const proxyBalance = useReadContract({
...
})
Baca dua saldo token yang kita minati, berapa banyak yang dimiliki pengguna, dan berapa banyak yang dimiliki proksi.
const nonce = useReadContract({
address: proxyAddr,
abi: UserProxy.abi,
functionName: 'nonce',
args: [],
})
Untuk mencegah serangan pemutaran ulang (replay attack) (misalnya, penjual memutar ulang transaksi yang memberi mereka uang), kita menggunakan sebuah nonce (opens in a new tab). Kita perlu mengetahui nilai saat ini untuk menambahkannya ke data yang kita tandatangani.
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])
Gunakan useEffect (opens in a new tab) untuk memperbarui saldo yang ditampilkan kepada pengguna saat informasi yang dibaca dari rantai blok berubah.
useEffect(() => {
setTransferToken(faucetAddr)
}, [faucetAddr])
useEffect(() => {
setTransferTo(account.address)
}, [account.address])
Standarnya adalah mentransfer token FaucetToken ke akun pengguna sendiri. Di sini kita mengatur nilai-nilai ini saat kita menerimanya dari Viem.
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)
Penangan peristiwa (event handler) untuk saat bidang teks berubah.
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)
}
}
Minta server untuk menyebarkan proksi bagi pengguna ini.
const signMessage = async(proxyAddr, target, calldata) => {
Tandatangani pesan sebelum mengirimkannya ke server untuk dikirim ke UserProxy secara onchain. Ini dijelaskan di sini. Kita perlu menandatangani pesan dengan alamat target (alamat token yang kita panggil) dan data panggilan (calldata) untuk dikirim.
const domain = {
.
.
.
return {v, r, s}
}
const messageUserProxy = async (proxy, target, data, v, r, s) => {
Kirim pesan yang ditandatangani ke UserProxy, yang akan memverifikasi tanda tangan dan kemudian mengirimkannya ke target.
try {
const response = await fetch("/server/message", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
proxy, target, // kedua alamat
data, // data panggilan untuk dikirim ke target
v, r, s // tanda tangan
})
})
const serverResponse = await response.json()
setTxHash(serverResponse.txHash)
} catch (err) {
console.error("Error:", err)
}
}
Kirim permintaan ke server, dan saat Anda menerima respons, dapatkan hash transaksi.
const faucetSimulation = useSimulateContract({
address: faucetAddr,
abi: Erc20.abi,
functionName: 'faucet',
account: account.address
})
Simulasikan pemanggilan fungsi faucet. Kita hanya mengaktifkan tombol faucet jika ini berhasil.
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)
}
Untuk memanggil fungsi melalui server dan UserProxy, kita mengikuti tiga langkah:
-
Buat data panggilan (calldata) untuk ditandatangani dan dikirim menggunakan
encodeFunctionData(opens in a new tab). -
Tandatangani pesan (alamat target, data panggilan, dan nonce).
-
Kirim pesan ke server.
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 />
Bagian komponen ini memungkinkan Anda menggunakan FaucetToken secara langsung dari peramban. Tujuan utamanya adalah untuk memfasilitasi proses debug.
<h4>UserProxy access <Address address={proxyAddr} /></h4>
<button onClick={deployUserProxy}>
Deploy UserProxy (slow process)
</button>
Biarkan pengguna menyebarkan UserProxy baru.
<br /><br />
<input type="text" placeholder="Atau masukkan alamat proxy yang ada" value={newProxyAddr} onChange={proxyAddressChange} />
<br /><br />
<button
onClick={() => setProxyAddr(newProxyAddr)}
disabled={newProxyAddr.match(/^0x[a-fA-F0-9]{40}$/) === null}
>
Set proxy address
</button>
Hanya biarkan pengguna mengeklik Set proxy address saat mereka memasukkan alamat yang sah. Perhatikan bahwa ini tidak memastikan bahwa alamat yang dimaksud memang merupakan kontrak UserProxy. Dimungkinkan untuk menambahkan pemeriksaan semacam itu, tetapi akan jauh lebih lambat (pengalaman pengguna yang lebih buruk) dan tidak meningkatkan keamanan (penyerang selalu dapat menggunakan kode mereka sendiri untuk antarmuka pengguna).
<br /><br />
{ proxyAddr && (
Tampilkan sisanya hanya jika ada alamat proksi yang sah.
<>
Proxy balance: {proxyBalanceAmount}
<br />
Proxy nonce: {nonce?.data?.toString() ?? "Loading..."}
Pengguna tidak perlu mengetahui nonce; ini hanya untuk tujuan debug.
<br />
<button disabled={!proxyAddr || proxyAddr === "Loading..." || nonce?.status !== 'success'}
onClick={proxyFaucet}
>
Request more tokens for proxy
</button>
Kita tidak dapat menyimulasikan panggilan ke faucet() melalui proksi. Namun, setidaknya kita dapat memastikan bahwa kita memiliki proksi dan bahwa proksi tersebut melaporkan nonce kepada kita.
<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>
</>
)}
Biarkan pengguna menerbitkan transaksi transfer ERC-20.
<hr />
{ txHash && (
<>
<h4>Last transaction:</h4>
<a href={`https://eth-sepolia.blockscout.com/tx/${txHash}`} target="_blank">
{txHash}
</a>
</>
)}
Jika ada hash transaksi terakhir, tampilkan tautan agar pengguna dapat melihatnya di penjelajah blok.
</div>
</>
)
}
export {Token}
Ini hanyalah boilerplate React.
Kerentanan
Server kita rentan terhadap serangan penolakan layanan. Serangan ini dijelaskan dalam artikel sebelumnya dari seri ini.
Selain itu, kita mendorong perilaku pengguna yang buruk. Inilah yang kita minta untuk ditandatangani oleh pengguna:
Kita tahu ini adalah transfer ERC-20 yang sah untuk token, jumlah, dan alamat tujuan yang ingin ditransfer oleh pengguna. Namun, sebagian besar pengguna tidak tahu cara menafsirkan data panggilan (calldata), dan tidak tahu apa yang mereka tandatangani. Itu adalah desain yang buruk, karena dua alasan:
- Beberapa pengguna tidak akan menggunakan kita karena mereka tidak memercayai data yang kita suruh untuk mereka tandatangani.
- Pengguna lain akan memercayai kita dan belajar bahwa mereka harus menandatangani data panggilan begitu saja tanpa memahami apa itu. Ini berarti bahwa jika Adam si Penyerang berhasil mengarahkan mereka ke situs webnya, ia dapat meminta mereka menandatangani transaksi yang memberinya semua USDC (atau DAI, atau ERC-20 lainnya) yang dimiliki pengguna.
Solusinya adalah memiliki fungsi terpisah di UserProxy untuk fungsi yang umum digunakan, seperti transfer. Kemudian pengguna dapat menandatangani sesuatu yang mereka pahami.
Catatan: Meskipun pengguna dapat menggunakan dompet apa pun yang mereka inginkan, sangat disarankan agar aplikasi yang menggunakan EIP-712 mendorong mereka untuk menggunakan dompet yang menampilkan seluruh data tanda tangan (opens in a new tab). Beberapa dompet memotong alamat, yang mana tidak aman. Penyerang dapat membuat alamat yang memiliki karakter awal dan akhir yang sama, tetapi berbeda di bagian tengah.
Kesimpulan
Selain kerentanan di atas, solusi dalam tutorial ini memiliki beberapa kelemahan yang dapat dibantu atasi oleh Ethereum.
- Ketahanan sensor. Saat ini, pengguna dapat menggunakan server Anda, server pesaing yang disiapkan oleh orang lain, atau terhubung ke Ethereum secara langsung, yang menimbulkan biaya gas. Menggunakan ERC-4337 (opens in a new tab) memungkinkan pengguna menawarkan transaksi mereka ke kumpulan server yang besar, sehingga mengurangi kemungkinan transaksi mereka akan disensor.
- Aset yang dimiliki EOA. Seperti yang dicatat di atas, EIP-7702 (opens in a new tab) dapat digunakan untuk mengelola aset yang sudah dimiliki oleh alamat EOA. Ini memiliki kesulitannya sendiri, tetapi terkadang hal ini diperlukan.
Saya berharap dapat menerbitkan tutorial tentang penambahan fitur-fitur ini dalam waktu dekat.
Lihat di sini untuk karya saya yang lain (opens in a new tab).


