Native Meta Transactions
integrate signed message recovery directly in your contracts
Meta transactions allow apps to abstract away seed phrases and wallet downloads at first. Users can simply use an app and the notion of gas is abstracted away. Then, after they have earned value, they are much easier to educate about wallets and blockchain.
Native meta transactions let users signal directly to smart contracts with signed messages. For instance, you could transfer an ERC721 (NFTy) that you own to a friend without ever having any ETH to pay for gas.
Most of the meta transaction demos I have built to date revolve around creating a gasless layer on top of existing contracts. To do this there are a few trade-offs including extra work on the frontend to track down the real msg.sender. A good example of this is in my EtherJamJam build.
If you are able to deploy fresh, you should probably be building meta transactions directly in your contracts as opposed to running a proxy contract to forward calls to an existing fleet. For instance, if you create a token, you can provide a native interface in your smart contract for signed messages and relay transactions to allow users to interact without paying for gas.
Let’s build an ERC20 token that gives etherless accounts the ability to transfer() and we’ll build a relay that pays the gas. Let’s fire up a Clevis Docker container pointing to ~/native-meta-transactions to get started:
docker run -ti --rm --name clevis -p 3000:3000 -p 8545:8545 -p 9999:9999 -v ~/native-meta-transactions
If you would like to skip to the completed repo, it is here. To build this POC we are going to reuse a lot of code from two different #BuidlGuidl articles: #BUIDLGUIDL — 0x1: guidlcoin and #BUIDLGUIDL — 0x2: meta transactions.
Once your empty project spins up and you see the Clevis prompt, let’s create our token contract called MetaCoin:
clevis create MetaCoin
We will extend OpenZepplin’s ERC20Mintable to add a metaTransfer() function:
The metaTransfer() function will perform the exact same actions as a normal transfer(), but first it will validate a signature of the hash of the parameters and ecrecover() a signature to prove the signer so everything is still cryptographically backed. Then, instead of doing actions on behalf of msg.sender, it will transfer the signer’s tokens. Finally, we can also wrap in a token reward to incentivise the relayer.
Let’s get our contract compiled and deployed to our local blockchain with:
clevis test full
Like in many previous Clevis & Dapparatus demos, you will want to make sure your MetaMask is pointed to the localhost:8545 RPC and give your account some test ETH in the metamask section of the tests/clevis.js file. Once you have that, you should get a blank dApp and have a little test ETH:
Let’s uncomment all the scaffolding code in src/App.js and make a few changes. First, let’s simplify the UI down to just showing the address of the MetaCoin contract and following the metaTransfer events:
Next, we’ll steal a bunch of code from #BUIDLGUIDL — 0x1: guidlcoin that will allow us to mint coins and view token balances. Let’s start with a poll function to grab token balances and figure out if our account is a minter:
We will need to fire off our poll() function once the <ContractLoader/> component signals that it is ready:
Next, we’ll create some UI for minting, transferring, and viewing tokens:
We’ll place these components in our main UI:
Then, just like we did in the GuidlCoin article, we’ll make sure our MetaMask user can mint tokens every time we deploy our contracts by adding an addMinter() test to tests/clevis.js:
Don’t forget to run that function in tests/deploy.js:
Now run a full deploy:
clevis test full
And we should see that our MetaMask user can deploy MetaCoins:
At this point you should be able to mint and transfer tokens to and from accounts that have some test ETH. View the full code up to this point here.
Now it’s time to get meta. If you are unfamiliar with meta transactions, check out this article. Basically, we need accounts that hold tokens but no ETH to be able to sign messages that tell the smart contract to send tokens on their behalf. These messages will be sent by a special relayer that is somehow incentivized.
Normally, meta transactions are run natively with Dapparatus and you can see how we do that in this code. But, this time we are going to try to do it all right in the UI to make it plain and simple. When someone hits the Send button, let’s detect if they are using a MetaAccount (ephemeral key pair generated on page load) or if they don’t hold any ETH. If this is the case, let’s package up a special message, sign it, and send it along to a relayer:
We will also need to install axios to make the ajax call work:
npm install --save
Notice that we added a fallback web3 provider and replaced the <MetaMask/> component with the new <Dapparatus/> component. This new component will adapt to the injected web3 and if none exists, it will generate and ephemeral key pair automatically:
Great, now we need to build the relayer to run on port 9999 that will pass these signed messages to the contract and pay the gas. We can start by stealing the relayer from Nonce Upon A Time (my devcon4 workshop).
We’ll make a few tweaks, but basically we accept a POST and push the arguments and signature on-chian, paying the gas:
If you look close there is a whitelist object commented out. Uncomment it if you would like to play around with those mechanics. It doesn’t scale well and we probably want to employ some kind of exponential backoff to avoid token holders draining the relay. In fact, there are many attack vectors to worry about with the relayers and we would love for you to join the dialog here.
Once your relay code is solid, you will need to install the dependencies:
npm install --save express helmet cors
Then you can fire up your relay in the Clevis container with:
Now, try using a MetaMask account with no ETH to transfer tokens. You can even try opening up a browser window in incognito mode or Safari. These accounts should now be able to transfer tokens too!
You’ll notice in the demo above that the top browser is transferring ERC20 tokens without paying gas and without having a wallet installed. This would work in Safari on iOS just like the Burner Wallet.
If you are working on a fresh new set of contracts, take some time to look at adding signed message recovery. The UX benefits are huge when your users are not required to purchase ETH or even have MetaMask downloaded.
The final repo for this demo is located here.
UPDATE: I just noticed that I was previously following the “metaTransfer” event but that doesn’t exist in the contract. If you update it to “Transfer” you will see the correct events: