Ruka hadi kwenye maudhui makuu

Kuruhusu watumiaji wako wasio na gesi kushikilia tokeni na kuita mikataba

bila gesi
erc-20
udhanifu wa akaunti
Kati
Ori Pomerantz
1 Aprili 2026
16 dakika za kusoma

Utangulizi

Makala iliyopita ilijadili kutumia ufikiaji usio na gesi kwenye programu yako mwenyewe kwa kutumia sahihi za EIP-712, lakini inakomea kwenye mikataba yako mahiri pekee. Kwa kutumia udhanifu wa akaunti, tunaweza kuunda mikoba ya mkataba mahiri inayokubali aina mbili za miamala na kuipeleka kwenye kituo kilichoombwa:

  • Miamala iliyotumwa na EOA mahususi (ambayo inahitaji EOA hiyo kuwa na ETH)
  • Miamala iliyotumwa kutoka popote, lakini iliyosainiwa na EOA hiyo hiyo.

Kwa njia hii, tunaweza kutoa njia isiyo na gesi kwa akaunti kushikilia rasilimali (tokeni, n.k.) na kufanya kazi zote ambazo EOA yenye gesi inaweza kufanya.

Kwa nini hatuwezi tu kupeleka ombi?

Katika viwango vya ERC-20 na vinavyohusiana, mmiliki wa akaunti ni msg.sender (opens in a new tab), anwani iliyoita mkataba wa tokeni, ambayo si lazima iwe mwanzilishi wa muamala, tx.origin (opens in a new tab). Hili linahitajika kwa sababu za kiusalama (opens in a new tab). Hii inamaanisha kuwa ikiwa tutapeleka maombi ya hamisho la tokeni, yatajaribu kuhamisha tokeni kutoka kwenye anwani ya mpelekaji badala ya anwani inayodhibitiwa na mtumiaji.

Kuna suluhisho linalokuruhusu kutumia anwani ya EOA kupitia EIP-7702 (opens in a new tab), lakini inahitaji kusaini ukaimishaji unaoweza kuwa hatari, kwa hivyo unaweza kuitumia tu kukaimisha kwa mkataba mahiri ambao mtoa huduma wa mkoba anauidhinisha. Kwa mafunzo haya ninapendelea njia rahisi zaidi ya kuunda mkataba mahiri kama proksi kwa mtumiaji.

Kuiona ikifanya kazi

  1. Hakikisha una Node (opens in a new tab) na Foundry (opens in a new tab).

  2. Nakili programu na usakinishe programu muhimu.

    git clone https://github.com/qbzzt/260315-gasless-tokens.git
    cd 260315-gasless-tokens
    forge build
    cd server
    npm install
    
  3. Hariri .env ili kuweka SEPOLIA_PRIVATE_KEY kwenye mkoba ulio na ETH kwenye Sepolia. Ikiwa unahitaji ETH ya Sepolia, tumia bomba kuipata. Kimsingi, ufunguo wa siri huu unapaswa kuwa tofauti na ule ulio nao kwenye mkoba wa kivinjari chako.

  4. Anzisha seva.

    npm run dev
    
  5. Vinjari kwenye programu kwenye URL http://localhost:5173 (opens in a new tab).

  6. Bofya Connect with Injected ili kuunganisha kwenye mkoba. Idhinisha kwenye mkoba, na uidhinishe mabadiliko kwenda Sepolia ikiwa ni lazima.

  7. Shuka chini na ubofye Deploy UserProxy (slow process).

  8. Unaweza kuona wakati proksi ya mtumiaji inaposambazwa kwa sababu kuna anwani karibu na UserProxy access. Ikiwa ulisubiri sekunde 24 (vitalu 2) na bado haijafanyika, kunaweza kuwa na tatizo la kutambua mabadiliko.

    Ikiwa ndivyo ilivyo, nenda kwenye Kichunguzi cha Bloku cha Sepolia (opens in a new tab) na uweke heshi ya muamala wa usambazaji unayoiona kwenye matokeo ya seva kwenye npm run dev. Bofya mkataba ulioundwa ili kutazama anwani yake, kisha uinakili. Bandika anwani kwenye sehemu ya Or enter existing proxy address, kisha ubofye Set proxy address.

  9. Bofya Request more tokens for proxy ili kuwasilisha mwito kwenye kipengele cha faucet (opens in a new tab) cha mkataba wa ERC-20 ili kupata tokeni. Thibitisha sahihi kwenye mkoba. Bila shaka, tokeni zinafika kwenye anwani ya proksi, si ya mtumiaji.

  10. Shuka chini na ubofye kiungo kilicho chini ya Last transaction:. Hii itafungua kivinjari ili kukuonyesha muamala wa faucet.

  11. Kwenye amount to transfer, weka nambari kati ya moja na elfu moja. Bofya Transfer ili kuhamisha tokeni kwenye anwani yako mwenyewe. Kabla hujabofya Confirm kwa ajili ya ombi, ona kwamba data inayosainiwa haieleweki. Watumiaji wangekuwa na wakati mgumu kuelewa wanachosaini. Kumbuka kwamba tutaijadili hapa chini.

  12. Baada ya muamala kuthibitishwa, subiri kuona mabadiliko katika your balance na proxy balance. Kumbuka kwamba hii pia itachukua muda, kwa sababu Sepolia ina muda wa kitalu wa sekunde 12.

Jinsi inavyofanya kazi

Kwa matumizi yasiyo na gesi, tunahitaji kiolesura cha mtumiaji kwa ajili ya mtumiaji, seva ya kuelekeza jumbe kutoka kwenye kiolesura cha mtumiaji hadi kwenye mnyororo, na mkataba mahiri wa kuzipokea na kuzithibitisha.

Mkataba mahiri wa mkoba

Huu ni mkataba mahiri (opens in a new tab). Lengo lake ni kufanya chochote ambacho mmiliki halisi anaomba, bila kujali njia iliyotumika kukiomba, na kupuuza kila kitu kingine. Ili kufanya hivi, vipengele vyake hupokea anwani lengwa ya kuita na data ya kutumia kuiita.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

contract UserProxy {
    address immutable OWNER;
    uint public nonce = 0;

Utambulisho wa mmiliki na nonsi (opens in a new tab) ili kuzuia jumbe zisirudiwe. Kwa sababu nonsi ni kigezo cha public, kikusanyaji cha Solidity pia huunda kipengele cha kutazama, nonce() (opens in a new tab), kinachoruhusu msimbo ulio nje ya mnyororo kusoma thamani yake.

    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;

Taarifa zinazohitajika ili kuthibitisha sahihi za EIP-712 (opens in a new tab).

    constructor(address owner_) {
        OWNER = owner_;

UserProxy inafungamanishwa na anwani moja ya mmiliki. Hili ni muhimu kwa sababu inaweza kumiliki rasilimali (tokeni za ERC-20, NFT, n.k.). Hatutaki kuchanganya rasilimali zinazomilikiwa na wamiliki tofauti.

Kitenganishi cha kikoa (opens in a new tab). Hakiwezi kukokotolewa wakati wa kukusanya, kwa sababu inategemea kitambulisho cha mnyororo na anwani ya mkataba. Hii inafanya iwezekane kwa UserProxy kudanganywa na ujumbe ulioandaliwa kwa ajili ya mwingine.

    event CallResult(address target, bytes returnData);

Weka logi ya matokeo ya mwito.

    function directAccess(address target, bytes calldata data)
            external returns (bytes memory) {

Kipengele hiki kinaweza kuitwa moja kwa moja na mmiliki. Ikiwa hakuna wapelekaji wanaopatikana, mmiliki bado anaweza kufikia rasilimali moja kwa moja kwenye mnyororo wa vitalu (ikiwa mtumiaji ana 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;
    }

Ikiwa tunaitwa moja kwa moja na mmiliki, ita lengwa kwa kutumia data za mwito zilizotolewa.

    function signedAccess(
        address target,
        bytes calldata data,
        uint8 v,
        bytes32 r,
        bytes32 s)

Hiki ni kipengele kikuu cha UserProxy. Inapata target na data, pamoja na sahihi.

Muhtasari pia unajumuisha nonsi, lakini hatuhitaji kuipokea kutoka kwenye muamala; tayari tunajua thamani sahihi. Sahihi yenye nonsi isiyo sahihi itakataliwa.


    // Rejesha msaini
    address signer = ecrecover(digest, v, r, s);
    require(signer == OWNER, "Signature invalid or not by owner");

Ikiwa sahihi ni batili, ecrecover kwa kawaida itarudisha anwani tofauti, na haitakubaliwa.

    (bool success, bytes memory returnData) = target.call(data);
    require(success, "Call failed");

Ita mkataba ambao mtumiaji alituambia tuuite, na tengua ikiwa haujafanikiwa.

    emit CallResult(target, returnData);

    nonce++; // Ongeza nonsi ili kuzuia marudio

    return returnData;
}

Ikiwa imefanikiwa, toa tukio la logi na uongeze nonsi.

Hizi ni tofauti zinazokaribia kufanana ambazo zinakuruhusu pia kuhamisha ETH nje ya mkataba.

Mpelekaji

Mpelekaji ni sehemu ya seva. Imeandikwa kwa JavaScript; unaweza kuona msimbo chanzo hapa (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'

Maktaba tunazohitaji. Hii ni seva ya Express (opens in a new tab), ambayo inatumia Vite (opens in a new tab) kutoa msimbo wa kiolesura cha mtumiaji. Tunatumia Viem (opens in a new tab) kuwasiliana na mnyororo wa vitalu, na dotenv (opens in a new tab) kusoma ufunguo wa siri kwa ajili ya anwani inayotuma muamala.

import { createRequire } from 'module'
const require = createRequire(import.meta.url)
const UserProxy = require('../contracts/out/UserProxy.sol/UserProxy.json')

Hii ni njia rahisi ya kusoma UserProxy iliyokusanywa. Tunahitaji ABI ili kuweza kuita UserProxy, na msimbo uliokusanywa ili kuweza kuisambaza kwa ajili ya mtumiaji.

dotenv.config()
const sepoliaAccount = privateKeyToAccount(process.env.SEPOLIA_PRIVATE_KEY)
console.log("Using account:", sepoliaAccount.address)

Soma faili la .env, toa anwani, na uichapishe kwenye kiweko.

Wateja wa Viem wanaozungumza na mnyororo wa vitalu.

const start = async () => {
  const app = express()

Endesha seva ya Express.

  app.use(express.json())

Iambie Express isome kiini cha ombi, na ikiwa ni JSON iichanganue.

  app.post("/server/deploy", async (req, res) => {

Huu ni msimbo unaoshughulikia maombi ya kusambaza proksi. Kumbuka kwamba tuko hatarini kwa mashambulizi ya kunyimwa huduma (opens in a new tab) hapa kwa sababu mshambuliaji anaweza kututumia maombi mengi ya kusambaza proksi hadi ETH yetu iishe. Kwenye mfumo wa uzalishaji, labda tungehitaji kwamba ombi la kusambaza proksi lisainiwe na kwamba msaini awe mteja aliyepo.

    try {
      const ownerAddress = req.body.ownerAddress

Pata anwani ya mmiliki kutoka kwenye ombi.

Sambaza mkataba (opens in a new tab) na usubiri hadi usambazwe (opens in a new tab).

      res.json({ contractAddress: receipt.contractAddress })

Ikiwa kila kitu kiko sawa, rudisha anwani ya proksi kwenye kiolesura cha mtumiaji.

    } catch (err) {
      console.error(err)
      res.status(500).json({ error: err.message })
    }
  })

Ikiwa kuna tatizo, liripoti.

  app.post("/server/message", async (req, res) => {

Huu ni msimbo unaochakata jumbe za mtumiaji kwa ajili ya mkataba wa UserProxy. Hili ni eneo lingine lililo hatarini kwa shambulio la kunyimwa huduma.

Pata data ya ombi na uitumie kuita signedAccess kwenye proksi.

      console.log("Message transaction hash:", txHash)

      res.json({ txHash })

Ripoti heshi ya muamala. Hii inaruhusu UI kuonyesha URL ili mtumiaji akague muamala.

    } catch (err) {
      console.error(err)
      res.status(500).json({ error: err.message })
    }
  })

Tena, ikiwa kuna tatizo, liripoti.

Kwa kila kitu kingine, tumia Vite, ambayo inashughulikia kutoa kiolesura cha mtumiaji kwa ajili yetu.

Kiolesura cha mtumiaji

Huu ni msimbo wa kiolesura cha mtumiaji (opens in a new tab). Sehemu kubwa ya msimbo inakaribia kufanana na ule ulioandikwa kwenye makala hii, isipokuwa Token.jsx (opens in a new tab).

Sehemu za Token.jsx (opens in a new tab) zinafanana na Greeter.jsx (opens in a new tab) katika makala hii. Hizi hapa ni sehemu mpya.

import {
   encodeFunctionData
       } from 'viem'

Kipengele hiki (opens in a new tab) kinaunda data za mwito kwa ajili ya mwito wa kipengele cha EVM. Hili ni muhimu ili mtumiaji aweze kusaini data za mwito.

import UserProxy from '../../contracts/out/UserProxy.sol/UserProxy.json'

UserProxy, iliyoelezwa hapo juu.

import Erc20 from '../../contracts/out/Faucet.sol/FaucetToken.json'

Mkataba huu (opens in a new tab) kwa kiasi kikubwa ni mkataba wa kawaida wa ERC-20, pamoja na nyongeza ya kipengele kimoja muhimu, faucet(). Kipengele hiki kinatoa tokeni kwa yeyote anayeziomba kwa madhumuni ya majaribio.

const erc20Addrs = {
  // Sepolia
    11155111: '0x4cBedDEDA88fDd9e116618a5cD71BB0E440C2A78'
}

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

Kijenzi hiki kinatoa anwani yenye kiungo cha mkataba kwenye kichunguzi cha bloku.

const Token = () => {
    ...

Hiki ndicho kijenzi kikuu kinachofanya kazi kubwa.

  const [ balanceAmount, setBalanceAmount ] = useState("Loading...")

Salio la tokeni la anwani ya mtumiaji.

  const [ proxyAddr, setProxyAddr ] = useState(null)

Anwani ya proksi inayomilikiwa na mtumiaji.

  const [ proxyBalanceAmount, setProxyBalanceAmount ] = useState("Loading...")

Salio la tokeni la proksi.

  const [ newProxyAddr, setNewProxyAddr ] = useState("")

Sehemu hii inatumika wakati mtumiaji anaweka anwani ya proksi yeye mwenyewe. Kuwa na uwezo wa kuweka anwani ya proksi wewe mwenyewe kunamruhusu mtumiaji kutumia proksi iliyopo badala ya kusambaza mpya kila wakati (na kupoteza tokeni zote zinazomilikiwa na proksi ya zamani).

  const [ txHash, setTxHash ] = useState(null)

Heshi ya muamala wa mwisho, inayotumika kuonyesha kiungo cha kichunguzi ili mtumiaji aweze kukagua muamala huo.

  const [ transferToken, setTransferToken ] = useState("")
  const [ transferAmount, setTransferAmount ] = useState("")
  const [ transferTo, setTransferTo ] = useState("")

Sehemu hizi zote zinatumika kutuma amri za hamisho la tokeni kwenye mkataba wa ERC-20. Hii inaweza kuwa FaucetToken, lakini si lazima iwe hivyo. Kipengele cha transfer ni sehemu ya kiwango cha ERC-20.

  const balance = useReadContract({
    ...
  })


  const proxyBalance = useReadContract({
    ...
  })

Soma masalio mawili ya tokeni tunayovutiwa nayo, kiasi ambacho mtumiaji anamiliki, na kiasi ambacho proksi inamiliki.

  const nonce = useReadContract({
      address: proxyAddr,
      abi: UserProxy.abi,
      functionName: 'nonce',
      args: [],
  })

Ili kuzuia mashambulizi ya kurudia (kwa mfano, muuzaji kurudia muamala unaompa pesa), tunatumia nonsi (opens in a new tab). Tunahitaji kujua thamani ya sasa ili kuiongeza kwenye data tunayosaini.

Tumia useEffect (opens in a new tab) kusasisha salio linaloonyeshwa kwa mtumiaji wakati taarifa iliyosomwa kutoka kwenye mnyororo wa vitalu inapobadilika.

  useEffect(() => {
    setTransferToken(faucetAddr)
  }, [faucetAddr])

  useEffect(() => {
    setTransferTo(account.address)
  }, [account.address])

Chaguo-msingi ni kuhamisha tokeni za FaucetToken kwenye akaunti ya mtumiaji mwenyewe. Hapa tunaweka thamani hizi tunapozipokea kutoka kwa 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)

Vishughulikiaji vya matukio kwa wakati sehemu za maandishi zinapobadilika.

Omba seva isambaze proksi kwa ajili ya mtumiaji huyu.

  const signMessage = async(proxyAddr, target, calldata) => {

Saini ujumbe kabla ya kuutuma kwenye seva ili upelekwe kwenye UserProxy mnyororoni. Hili limeelezwa hapa. Tunahitaji kusaini ujumbe wenye anwani lengwa (anwani ya tokeni tunayoita na) na data za mwito za kutuma.

    const domain = {
      .
      .
      .
    return {v, r, s}
  }

  const messageUserProxy = async (proxy, target, data, v, r, s) => {

Tuma ujumbe uliosainiwa kwenye UserProxy, ambayo itathibitisha sahihi na kisha kuutuma kwenye target.

Tuma ombi kwenye seva, na unapopokea jibu, pata heshi ya muamala.

  const faucetSimulation = useSimulateContract({
    address: faucetAddr,
    abi: Erc20.abi,
    functionName: 'faucet',
    account: account.address
  })

Iga kuita kipengele cha faucet. Tunawezesha kitufe cha bomba tu ikiwa hili litafanikiwa.

Ili kuita kipengele kupitia seva na UserProxy, tunafuata hatua tatu:

  1. Unda data za mwito za kusaini na kutuma kwa kutumia encodeFunctionData (opens in a new tab).

  2. Saini ujumbe (anwani lengwa, data za mwito, na nonsi).

  3. Tuma ujumbe kwenye seva.

Sehemu hii ya kijenzi inakuruhusu kutumia FaucetToken moja kwa moja kutoka kwenye kivinjari. Lengo lake kuu ni kuwezesha utatuzi.

         <h4>UserProxy access <Address address={proxyAddr} /></h4>
         <button onClick={deployUserProxy}>
         Deploy UserProxy (slow process)
         </button>

Ruhusu mtumiaji kusambaza UserProxy mpya.

Ruhusu watumiaji kubofya Set proxy address tu wanapoingiza anwani halali. Kumbuka kwamba hii haihakikishi kwamba anwani inayohusika ni mkataba wa UserProxy kweli. Inawezekana kuongeza ukaguzi kama huo, lakini itakuwa polepole sana (uzoefu mbaya zaidi wa mtumiaji) na haitaboresha usalama (washambuliaji wanaweza kutumia msimbo wao wenyewe kwa kiolesura cha mtumiaji kila wakati).

         <br /><br />
         { proxyAddr && (

Onyesha yaliyosalia tu ikiwa kuna anwani halali ya proksi.

            <>
               Proxy balance: {proxyBalanceAmount}
               <br />
               Proxy nonce: {nonce?.data?.toString() ?? "Loading..."}

Mtumiaji hahitaji kujua nonsi; hii ni kwa madhumuni ya utatuzi tu.

               <br />
               <button disabled={!proxyAddr || proxyAddr === "Loading..." || nonce?.status !== 'success'}
                  onClick={proxyFaucet}
               >
                  Request more tokens for proxy
               </button>

Hatuwezi kuiga mwito kwenye faucet() kupitia proksi. Hata hivyo, tunaweza angalau kuhakikisha kwamba tuna proksi na kwamba proksi ilituripoti nonsi.

Ruhusu mtumiaji kutoa miamala ya hamisho la ERC-20.

Ikiwa kuna heshi ya muamala wa mwisho, onyesha kiungo ili mtumiaji aweze kuitazama kwenye kichunguzi cha bloku.

 
</div>
    </>
  )
}

export {Token}

Hii ni msimbo wa msingi tu wa React.

Udhaifu

Seva yetu iko hatarini kwa mashambulizi ya kunyimwa huduma. Shambulio hili limeelezwa katika makala iliyopita ya mfululizo huu.

Zaidi ya hayo, tunahimiza tabia mbaya ya mtumiaji. Hiki ndicho tunachomwomba mtumiaji asaini:

Screen capture with opaque calldata

Sisi tunajua hili ni hamisho halali la ERC-20 kwa ajili ya tokeni, kiasi, na anwani lengwa ambayo mtumiaji anataka kuhamisha. Lakini watumiaji wengi hawajui jinsi ya kufasiri data za mwito, na hawana wazo wanasaini nini. Huo ni muundo mbaya, kwa sababu mbili:

  • Baadhi ya watumiaji hawatatutumia kwa sababu hawaamini data tunayowaambia wasaini.
  • Watumiaji wengine watatuamini na kujifunza kwamba wanapaswa tu kusaini data za mwito bila kuelewa ni nini. Hii inamaanisha kwamba ikiwa Adam Mshambuliaji atafanikiwa kuwaelekeza kwenye tovuti yake, anaweza kuwafanya wasaini muamala unaompa USDC zote (au DAI, au ERC-20 nyingine yoyote) anazomiliki mtumiaji.

Suluhisho ni kuwa na vipengele tofauti katika UserProxy kwa ajili ya vipengele vinavyotumika sana, kama vile hamisho. Kisha watumiaji wanaweza kusaini kitu wanachokielewa.

Screen capture with transfer details

Kumbuka: Ingawa watumiaji wanaweza kutumia mkoba wowote wanaotaka, inapendekezwa sana kwamba programu zinazotumia EIP-712 ziwahimize kutumia mkoba ambao unaonyesha data yote ya sahihi (opens in a new tab). Baadhi ya mikoba hukata anwani, jambo ambalo si salama. Mshambuliaji anaweza kuunda anwani yenye herufi zinazofanana mwanzoni na mwishoni, lakini inatofautiana katikati.

Screen capture with truncated addresses

Hitimisho

Mbali na udhaifu ulio hapo juu, suluhisho katika mafunzo haya lina mapungufu kadhaa ambayo Ethereum inaweza kutusaidia kuyashughulikia.

  • Ukinzani wa udhibiti. Kwa sasa, watumiaji wanaweza kutumia seva yako, seva shindani iliyowekwa na mtu mwingine, au kuunganisha kwenye Ethereum moja kwa moja, jambo ambalo linagharimu gesi. Kutumia ERC-4337 (opens in a new tab) kunaruhusu watumiaji kutoa muamala wao kwenye kundi kubwa la seva, na kupunguza uwezekano wa miamala yao kudhibitiwa.
  • Rasilimali zinazomilikiwa na EOA. Kama ilivyoelezwa hapo juu, EIP-7702 (opens in a new tab) inaweza kutumika kudhibiti rasilimali ambazo tayari zinamilikiwa na anwani ya EOA. Hili lina ugumu wake, lakini wakati mwingine ni muhimu.

Natumai kuchapisha mafunzo kuhusu kuongeza vipengele hivi katika siku za usoni.

Tazama hapa kwa kazi zangu zaidi (opens in a new tab).