[Ethereum] Gasless transactions – sending transaction instead of signing message. How does it work

gasmetamaskopenzeppelinsolidityweb3js

I'm following this tutorial:
https://docs.openzeppelin.com/learn/sending-gasless-transactions

But it doesn't really explain clearly how it works in details. A standard transaction is pretty simple to understand. An user signs a message, sends it, pays for it and it goes to blockchain.

If there is gasless transaction on mainnet or testnet like ropsten, user doesn't send transaction to the blockchain, instead he only signs it. What happens next? There must be something which grabs user's personal signed message (because as far as I know, this message is "private", it doesn't go to the global network) and puts it to the network and pays for it but how does exactly it work?

For a moment I was thinking that if I make a contract which extends GSNRecipient any user transaction will change into sign request and this contract will be making transactions to the network. So I recreated a simple Counter contract which extends GSNRecipientUpgradeSafe.

pragma solidity ^0.5.0;
import "./GSNRecipient.sol";
contract Counter is GSNRecipientUpgradeSafe
{
    int public value;
    
    function addValue()
        external
    {
        value++;
    }
    
    function start()
        external
    {
        __GSNRecipient_init();
    }
    
    function acceptRelayedCall(
        address relay,
        address from,
        bytes calldata encodedFunction,
        uint256 transactionFee,
        uint256 gasPrice,
        uint256 gasLimit,
        uint256 nonce,
        bytes calldata approvalData,
        uint256 maxPossibleCharge
    )
        external
        view
        returns (uint256, bytes memory)
    {
        return _approveRelayedCall();
    }

    function _preRelayedCall(bytes memory context) internal returns (bytes32)
    {

    }

    function _postRelayedCall(bytes memory context, bool success, uint256 actualCharge, bytes32 preRetVal) internal
    {
        
    }
}

I deployed it and called start()

I didn't want to use any react scripts. I decided to use a normal web3js library and very basic simple website.

<span id="cnt">COUNTER</span>
<button onclick="getContract().methods.addValue().send({from:account})">click</button>
<script>
    let account;
    let web3;
    window.addEventListener('load', async () => {
        if(window.ethereum)
        {
            try
            {
                await ethereum.enable();
                web3 = new Web3(ethereum);
                web3.eth.defaultAccount = (await web3.eth.getAccounts())[0];
                account = web3.eth.defaultAccount;
                let c = getContract();
                $("#cnt").html(await c.methods.value().call());
                
            }
            catch(e)
            {
                console.log(e.toString());
            }
        }
    });
    
    function getContract()
    {
        return new web3.eth.Contract(abi, "0xaa9f0c1AC580EFA7A0e6d64d3eEBF62B4F970701");
    }
</script>

I have checked a gsn demo: https://metacoin.opengsn.org/
After clicking "send metacoin" I'm getting sign message request. In the source code there is normal contract.methods……send(). So I decided to recreate it.

But unfortunately:

<button onclick="getCounterContract().methods.addValue().send({from:account})">click</button>

This simply calls a normal transaction instead of sign request.

web3.version: "1.2.6"

I'm pretty sure I misunderstood something or didn't understand properly. How can I make this working? Maybe there is something better than OpenZeppelin's GSN network code?

I'm using metamask.

Best Answer

The idea of "gasless" transactions is that a user signs a message, and sends that message to a gas relayer. The relayer is a separate entity, which collects the messages from the users, and sends the transactions. This way, the user doesn't need to pay for the transaction, only the relayer does. Then, optionally, the user can pay in tokens or other means of payment to the relayer.

The users' funds are stored in a smart contract, and only with a valid signature provided by the user, can funds be sent from that contract. In your example, you're calling a function on a contract as the user. Since all actual transactions on the network require gas, this will simply send a transaction directly to the contract, rather than using a gas relayer.

To send transactions to the gas station network, you can use their JavaScript libraries, for example:

const { RelayProvider } = require('@opengsn/gsn')

const provider = new RelayProvider(web3.currentProvider, ...);
const web3 = new Web3(provider);

const counterContract = new web3.eth.Contract(abi, ...);
await counterContract.methods.addValue().send({ ... });

This will use the RelayProvider to sign the transaction and send it to a gas relayer, who will then send the transaction to your contract, on behalf of the user.

There's a more detailed guide for using OpenGSN here: https://docs.opengsn.org/gsn-provider/getting-started.html

Related Topic