Tutorial·8 min का पठन·Solingo द्वारा

ERC-4337 Paymasters: gasless transactions समझाई गई

Paymaster की मदद से user के बजाय कोई और gas चुका सकता है, या user native ETH के बजाय किसी ERC-20 में gas दे सकता है। यहाँ बताया गया है कि validatePaymasterUserOp और postOp असल में कैसे काम करते हैं, साथ ही एक minimal verifying paymaster जो आप खुद बना सकते हैं।

# ERC-4337 Paymasters: gasless transactions समझाई गई

किसी dapp में नए users को onboard करने में सबसे बड़ी रुकावटों में से एक है gas। कुछ भी करने से पहले, यहाँ तक कि एक free NFT mint करने या airdrop claim करने के लिए भी, user के पास native ETH होना ज़रूरी है। ERC-4337 account abstraction इसे एक component से हल करता है जिसे paymaster कहते हैं: एक smart contract जो किसी transaction का gas चुकाने को राज़ी होता है, या तो मुफ़्त में (sponsored) या किसी ERC-20 token के बदले। यह article पहले account abstraction दोहराता है, फिर ठीक-ठीक बताता है कि paymaster क्या करता है और एक minimal verifying paymaster कैसे बनाएं।

Account abstraction और UserOperation, संक्षेप में

ERC-4337 Ethereum protocol को नहीं बदलता। किसी externally owned account (EOA) से एक सामान्य transaction भेजने के बजाय, user एक UserOperation sign करता है: एक struct जो बताती है कि उसके smart contract account को क्या करना चाहिए।

UserOperations एक alternative mempool में भेजी जाती हैं, bundlers नाम के actors उन्हें उठाते हैं, और एक batch के रूप में एक ही audited contract में submit करते हैं जिसे EntryPoint कहते हैं। EntryPoint हर UserOperation को दो phases में चलाता है:

  • Validation: यह account से पूछता है कि operation valid है या नहीं (signature, nonce) और paymaster से पूछता है कि वह payment के लिए राज़ी है या नहीं।
  • Execution: यह account को असल action करने के लिए call करता है, फिर gas accounting settle करता है।
  • एक UserOperation के मुख्य fields में sender (smart account), nonce, callData (क्या execute करना है), gas limits, और paymasterAndData शामिल हैं। यही आखिरी field एक paymaster को activate करती है।

    Paymaster असल में क्या करता है

    Paymaster एक contract है जिसने EntryPoint के पास ETH stake और deposit किया होता है। जब किसी UserOperation में non-empty paymasterAndData होती है, तो EntryPoint gas का खर्च sender account के बजाय paymaster के deposit से काटता है।

    Paymaster interface में दो functions हैं जिन्हें EntryPoint call करता है:

    • validatePaymasterUserOp: validation phase के दौरान call होता है। Paymaster तय करता है कि इस operation को sponsor करना है या नहीं, और एक context blob के साथ validation data return करता है।
    • postOp: execution के बाद call होता है। यहीं paymaster असली खर्च का हिसाब मिलाता है, जैसे अभी-अभी आगे चुकाए गए gas को cover करने के लिए user से ERC-20 tokens खींचना।

    यहाँ standard के अनुसार interface है (EntryPoint v0.7 की shape, PackedUserOperation के साथ):

    // SPDX-License-Identifier: MIT
    

    pragma solidity ^0.8.23;

    struct PackedUserOperation {

    address sender;

    uint256 nonce;

    bytes initCode;

    bytes callData;

    bytes32 accountGasLimits;

    uint256 preVerificationGas;

    bytes32 gasFees;

    bytes paymasterAndData;

    bytes signature;

    }

    enum PostOpMode {

    opSucceeded, // user op succeeded

    opReverted, // user op revert हुआ, फिर भी gas चुकाना है

    postOpReverted // पिछला postOp revert हुआ; केवल internal उपयोग

    }

    interface IPaymaster {

    function validatePaymasterUserOp(

    PackedUserOperation calldata userOp,

    bytes32 userOpHash,

    uint256 maxCost

    ) external returns (bytes memory context, uint256 validationData);

    function postOp(

    PostOpMode mode,

    bytes calldata context,

    uint256 actualGasCost,

    uint256 actualUserOpGasPrice

    ) external;

    }

    validatePaymasterUserOp विस्तार से

    यह function सस्ता होना चाहिए और किसी ऐसे mutable external state पर निर्भर नहीं होना चाहिए जिसे कोई दूसरी UserOperation बदल सके, क्योंकि bundlers इसे off chain simulate करते हैं और validation rules तोड़ने वाली operations को reject कर देते हैं।

    यह दो चीज़ें return करता है:

    • context: arbitrary bytes जो postOp को आगे भेजे जाते हैं। इसे छोटा रखें। आमतौर पर इसमें user का address और वह token price होती है जिसे आपने lock किया।
    • validationData: एक packed value जिसमें एक aggregator address (या "signature failed" के लिए special value) होती है, साथ में एक validUntil और validAfter time window। 0 return करने का मतलब है "हमेशा valid, कोई aggregator नहीं"।

    maxCost argument वह अधिकतम राशि है जो operation में wei में लग सकती है। Paymaster इसका उपयोग यह तय करने में करता है कि वह इतनी राशि आगे चुकाने को तैयार है या नहीं।

    postOp विस्तार से

    Account का callData execute होने के बाद, EntryPoint postOp को असली gas cost के साथ call करता है। mode बताता है कि user operation succeed हुआ या revert। अहम बात: paymaster gas तब भी चुकाता है जब user का call revert हो जाए, इसलिए charging की logic यहीं होनी चाहिए, execution में नहीं।

    एक सूक्ष्म पेच: EntryPoint v0.7 में, अगर postOp खुद revert हो जाए, तो पूरी user op revert होकर फिर से चलती है, पर paymaster पर फिर भी charge लगता है। इसलिए postOp मज़बूत होना चाहिए।

    Paymaster के दो आम designs हैं।

    Sponsored (verifying) paymaster. User के लिए gas मुफ़्त है। dapp का एक off chain service एक approval sign करता है जो कहता है "मैं इस खास UserOperation का payment इस timestamp तक करूँगा"। On chain paymaster सिर्फ़ उस signature को check करता है। इसी तरह आप नए users को एक gasless पहला अनुभव देते हैं। खर्च dapp उठाती है।

    ERC-20 (token) paymaster. User के पास ETH नहीं है पर मान लीजिए USDC है। Paymaster EntryPoint को ETH gas आगे चुका देता है, फिर postOp में transferFrom call करके user से उतनी ही राशि का USDC (साथ में थोड़ा markup) खींच लेता है। User को पहले ही paymaster को approve करना होता है। exchange rate ही tricky हिस्सा है: या तो आप किसी oracle पर भरोसा करते हैं, या signed data में एक price pin कर देते हैं।

    एक त्वरित तुलना:

    • कौन चुकाता है: sponsored = dapp, ERC-20 = user (tokens में)।
    • User को क्या चाहिए: sponsored = कुछ नहीं, ERC-20 = token balance और एक approval।
    • मुख्य risk: sponsored = आपके funds का abuse और griefing, ERC-20 = validation और postOp के बीच price slippage।

    एक minimal verifying paymaster बनाना

    आइए sponsored variant बनाएं। Logic: paymaster एक verifyingSigner (एक off chain key) पर भरोसा करता है। dapp backend UserOperation के एक hash और एक time window को sign करता है। On chain, paymaster signer को recover करके approve करता है।

    // SPDX-License-Identifier: MIT
    

    pragma solidity ^0.8.23;

    import {IPaymaster, PackedUserOperation, PostOpMode} from "./IPaymaster.sol";

    import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

    import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";

    contract MinimalVerifyingPaymaster is IPaymaster {

    using ECDSA for bytes32;

    address public immutable entryPoint;

    address public immutable verifyingSigner;

    // validationData के success / failure markers (ERC-4337 के अनुसार)

    uint256 private constant SIG_VALIDATION_FAILED = 1;

    uint256 private constant SIG_VALIDATION_SUCCESS = 0;

    constructor(address _entryPoint, address _verifyingSigner) {

    entryPoint = _entryPoint;

    verifyingSigner = _verifyingSigner;

    }

    modifier onlyEntryPoint() {

    require(msg.sender == entryPoint, "not from EntryPoint");

    _;

    }

    // off chain signer इस hash को sign करता है। इसे chain + paymaster से

    // bind करें ताकि signature कहीं और replay न हो सके।

    function getHash(

    PackedUserOperation calldata userOp,

    uint48 validUntil,

    uint48 validAfter

    ) public view returns (bytes32) {

    return keccak256(

    abi.encode(

    userOp.sender,

    userOp.nonce,

    keccak256(userOp.callData),

    block.chainid,

    address(this),

    validUntil,

    validAfter

    )

    );

    }

    function validatePaymasterUserOp(

    PackedUserOperation calldata userOp,

    bytes32, // userOpHash, यहाँ अप्रयुक्त

    uint256 // maxCost, इस सरल variant में अप्रयुक्त

    ) external view onlyEntryPoint returns (bytes memory context, uint256 validationData) {

    // paymasterAndData layout (v0.7):

    // [0:20] paymaster address

    // [20:36] paymaster verification gas limit

    // [36:52] paymaster postOp gas limit

    // [52:] हमारा custom data: validUntil | validAfter | signature

    bytes calldata data = userOp.paymasterAndData[52:];

    uint48 validUntil = uint48(bytes6(data[0:6]));

    uint48 validAfter = uint48(bytes6(data[6:12]));

    bytes calldata signature = data[12:];

    bytes32 hash = getHash(userOp, validUntil, validAfter)

    .toEthSignedMessageHash();

    bool ok = hash.recover(signature) == verifyingSigner;

    // validationData pack करें: low bit में failure flag, फिर time window।

    validationData = _packValidationData(!ok, validUntil, validAfter);

    context = ""; // मुफ़्त sponsorship में postOp के लिए कुछ reconcile नहीं

    }

    function postOp(

    PostOpMode,

    bytes calldata,

    uint256,

    uint256

    ) external onlyEntryPoint {

    // Sponsored gas: कुछ नहीं करना। ERC-20 paymaster यहाँ actualGasCost

    // के आधार पर user से transferFrom करता।

    }

    function _packValidationData(

    bool sigFailed,

    uint48 validUntil,

    uint48 validAfter

    ) internal pure returns (uint256) {

    return (sigFailed ? SIG_VALIDATION_FAILED : SIG_VALIDATION_SUCCESS)

    | (uint256(validUntil) << 160)

    | (uint256(validAfter) << (160 + 48));

    }

    }

    ध्यान दें कि sponsored gas के लिए postOp खाली है और context खाली है। ERC-20 paymaster के लिए आप validation के दौरान user address और एक token rate को context में encode करेंगे, फिर postOp के अंदर actualGasCost का उपयोग करके token.transferFrom(user, address(this), tokenAmount) call करेंगे।

    Paymaster को fund करना

    बिना funds के paymaster कुछ नहीं करता। EntryPoint के सामने दो actions ज़रूरी हैं:

  • deposit: वह ETH जिससे EntryPoint bundlers को चुकाता है। entryPoint.depositTo{value: x}(paymaster) call करें।
  • stake: एक अलग locked stake जो आपके paymaster को validation के दौरान storage छूने देता है बिना bundler reputation rules से throttle हुए। entryPoint.addStake{value: y}(unstakeDelaySec) call करें।
  • आम गलतियाँ

    • Validation rule का उल्लंघन: validatePaymasterUserOp में किसी दूसरे contract का mutable storage पढ़ने से bundlers आपकी operations reject कर देते हैं। Validation को pure और local रखें।
    • Chains के बीच replay: signed hash को हमेशा block.chainid और address(this) से bind करें, जैसा ऊपर दिखाया गया है।
    • Revert case भूलना: user op revert होने पर भी आप gas चुकाते हैं, इसलिए ERC-20 paymaster को mode == opReverted होने पर भी postOp में charge करना चाहिए।
    • Price slippage: पुराना rate lock करने वाला token paymaster volatile gas पर पैसे खो सकता है। एक margin जोड़ें।

    इसे Solingo पर practice करें

    Interface पढ़ना एक बात है, validatePaymasterUserOp और postOp को सही ढंग से wire करना दूसरी। आप एक verifying paymaster को step by step बना और test कर सकते हैं, validation simulation चला सकते हैं, और जानबूझकर इसे तोड़कर देख सकते हैं कि bundlers क्या reject करते हैं, app.solingo-blockchain.xyz के interactive lessons में। इसके बाद ERC-20 variant implement करने की कोशिश करें: validation को pure रखें, और सारा token math postOp में करें।

    Practice में लगाने के लिए तैयार हैं?

    Solingo पर interactive exercises के साथ इन concepts को apply करें।

    मुफ्त में शुरू करें