#BUIDLGUIDL — 0x2: meta transactions

Ephemeral accounts and gasless transactions

Austin Thomas Griffith
13 min readOct 26, 2018

In this episode of #BuidlGuidl, we’ll create an example application that uses meta transactions to abstract away gas costs. If you can show your users the heart of the application first, then slowly educate them about blockchain concepts as they earn value you’ll see a much higher adoption rate. It’s much easier to educate someone about a seed phrase once it’s protecting something valuable rather than as the first step of onboarding.

We’ll be using Clevis & Dapparatus to build this dApp. If you aren’t familiar, check out #BUIDLGUIDL — 0x0: Clevis & Dapparatus & #BUIDLGUIDL — 0x1: GuidlCoin.

We’re going to create a project where users can collaborate to write a short story. We’ll call it “Nonce Upon A Time”

This first step will be to initialize a Clevis project. This can be done in a number of different ways. The easiest method is to just fire up a docker container with one command:

docker run -ti --rm --name clevis -p 3000:3000 -p 8545:8545 -v ~/nonce-upon-a-time:/dapp austingriffith/clevis:latest

This method will also bring up ganache-cli and your react app for you.

Once Clevis initializes (it can take several minutes depending on your machine), we can fire up a web browser and navigate to:

http://localhost:3000
Make sure MetaMask is unlocked and the network is http://localhost:8545

Let’s jump right in and create our main smart contract called Stories:

clevis create Stories

Remember, if you are using a Docker container, you only run Clevis command line actions in the container. If you want to edit your code, use your favorite editor. For example:

atom ~/nonce-upon-a-time
Clevis creates an empty smart contract scaffolding for you.

Our smart contract will be incredibly simple. We’ll just have a write function that emits a Write event with the line of the story and what address wrote it.

function write(string line) external {
emit Write(msg.sender, line);
}
event Write(address sender, string line);

Now we can compile it and make sure everything is solid:

clevis compile Stories

You will see a bunch of information including the bytecode and metadata, but as long as you don’t see any errors, you should be good to go. Again, if you haven’t used Clevis & Dapparatus before you should start with the intro.

Let’s go ahead and deploy the contract to our private chain:

clevis deploy Stories 0

Remember, the 0 here stands for account 0. This is the address that pays the gas to deploy the contract.

Now let’s publish our deployed contract into our frontend with:

clevis test publish

Next, let’s uncomment the contract loader in src/App.js:

Dapparatus’ ContractLoader will automatically load in all Clevis contracts.

In the frontend, you can look at the console to verify that our contract is now loading into React:

Notice the write(string) function is listed in the Stories contract.

Next, let’s go ahead and uncomment the Transactions component:

After hitting save we will now see the blocks as they are mined in the bottom right corner. Transactions will show up here too eventually:

Finally, let’s build a simple UI for adding lines to the story with a title:

if(contracts){
contractsDisplay.push(
<div key="UI" style={{padding:30}}>
<h1>
Nonce Upon A Time...
</h1>
<input
style={{verticalAlign:"middle",width:800,margin:6,marginTop:20,maxHeight:20,padding:5,border:'2px solid #ccc',borderRadius:5}}
type="text" name="writeText" value={this.state.writeText} onChange={this.handleInput.bind(this)}
/>
<Button size="2" onClick={()=>{}}>
Write
</Button>
</div>
)
}

This will give a nice little UI, but it won’t be wired up yet:

Let’s have that Write button actually fire off a transaction when it is clicked:

tx(contracts.Stories.write(this.state.writeText),(receipt)=>{
console.log("TX CALLED BACK",receipt)
this.setState({writeText:""})
})

And if we test it out…

Oh no, Insufficient funds.

Okay, we’ll need to send this account a little test ETH each time we deploy our contracts. Find the metamask() section in the tests/clevis.js file and add in your MetaMask address:

You can run just this test with:

clevis test metamask
Now you should have some test ETH.

This will happen every time you deploy too. So from now on this MetaMask account should have plenty of ETH to play around. Let’s try out the UI:

We can see that the transaction went through:

BUT, we aren’t displaying anything yet. We need to add in an event parser:

At first, let’s set it up where hide:false so we can make sure it’s working.
<Events
config={{hide:false}}
contract={contracts.Stories}
eventName={"Write"}
block={block}
onUpdate={(eventData,allEvents)=>{
//console.log("EVENT DATA:",eventData)
this.setState({events:allEvents})
}}
/>

After we save it should auto-reload and show us our event:

Cool, it worked, but that’s really ugly. Let’s hide that component and just have it update the state.

Then, let’s add in some UI to display the lines from each event:

let lines = []
for(let e in this.state.events){
let anEvent = this.state.events[e]
lines.push(
<div key={e}style={{position:"relative"}}>
<Blockie config={{size:2.5}} address={anEvent.sender}/>
<span style={{paddingLeft:10}}>
{anEvent.line}
</span>
</div>
)
}

Don’t forget to add in the lines array into your UI:

And there we go, we should have our lines of the story added into the UI. Let’s add a few more to make sure it’s solid. Bonus points if you can figure out how to supply multiple accounts with ETH and write a collaborative story:

Sweet, our dApp #BUIDL is complete. 🚢#SHIPL it……….

Not so fast jabronie. Let’s just take a look at our app in the dreaded Safari where we don’t have the MetaMask plugin installed:

Well that looks gross. So users without MetaMask are going to basically just shut off your app at this point. There are a lot of numbers floating around the space, but I’ve heard that a very, very small percent of users that you drive to your dApp with advertising actually make it past this step right here.

We need to fix that.

Okay, well, what can we do? Well, we could at least wire up Infura or another web3 provider so we can at least read the story and display it.

To do this, let’s replace the stock MetaMask component with a new and improved Dapparatus component. First, make sure you import Dapparatus:

Then replace <Metamask /> with <Dapparatus />:

<Dapparatus
config={{requiredNetwork:['Unknown','Rinkeby']}}
fallbackWeb3Provider={new Web3.providers.HttpProvider(WEB3_PROVIDER)}
onUpdate={(state)=>{
console.log("metamask state update:",state)
if(state.web3Provider) {
state.web3 = new Web3(state.web3Provider)
this.setState(state)
}
}}
/>

For now let’s define that WEB3_PROVIDER as http://0.0.0.0:8545, but in the future you will point that at infura like: https://mainnet.infura.io/<your-token>

const WEB3_PROVIDER = 'http://0.0.0.0:8545'

Now when we visit from Safari:

OH WHAT? We have an account too? That’s right, you are reading from the blockchain AND we automatically generated you a throwaway account that lives in a cookie in your browser. Rad!

Now that a user can see your app and we generate a keypair, there is only one thing left for seamless UX: meta transactions. Normally, Ethereum transactions must be signed by a wallet and then when they are put on-chain, that wallet will have to pay the gas for the transaction. In this case, our ephemeral wallet won’t have any ETH. We’ll need to employ a different trick.

The first thing that most of us think of when tackling this problem is custodial accounts. We’ll just hold their keys for them and when they want to make a transaction they can send it to us and we’ll sign it and send it to the network. This is a really bad solution. First of all, it totally throws all this trustless decentralization right in toilet, but second, it is susceptible to all kinds of classic web2 attacks.

Instead, we’ll use cryptography and a trustless smart contract. With ecrecover() we can prove that the user signed a message and then, with a relayer, we’ll pay the gas to execute their transaction on-chain. We’ll also leave a paper trail via events that traces back to their account within the smart contract.

We’ll need to create this contract that proxies requests based on signed messages and fires events to allow our frontend to trace who does what:

clevis create Proxy

The proxy won’t have any kind of whitelisting system on it. Basically, if you hit it with a correctly signed transaction, it will trigger the call function with your signed call data. Here is all the code:

That’s a lot to unpack, but the TL;DR of this proxy contract is: there is a forward() function that receives call data and a destination address along with a signature. Then it does an ecrecover() to prove the signature matches the signer and the nonce. If everything is correct it passes the call data right into the call() function of assembly and that executes a real Ethereum transaction.

Disclaimer: this Proxy isn’t safe for real deployment. You will need some sort of whitelisting system because right now anyone can execute commands.

Let’s compile this contract to make sure we have the syntax right:

clevis compile Proxy

Now everytime we do a ‘clevis test full’ we will have our Stories contract and our Proxy contract.

Next, we need to build a relayer that will catch our meta transactions and pay the gas to submit them to the Proxy contract. For now, many projects are just using a centralized relayer. However, anyone with ETH can submit a meta transaction and more complex versions contain a reward. Therefore, a peer-to-peer relayer network is the next logical step. For the sake of simplicity, let’s just build a simple http server that receives meta transactions and submits them blindly to the proxy:

This http server listens for meta transactions to be POSTed to the /tx endpoint. It then recovers the signer from the signature to prove it is a valid meta transaction and then pays the gas to send it to the smart contract for execution.

Create a file called relayer.js in your project directory with the above contents. Then you will need to install some of the needed packages inside the Clevis container:

npm install --save express helmet cors

It will want to know what network you are on, use 999 for local ganache:

echo "999" > deploy.network

Then fire it up with:

node relayer.js

Note: you will need to stop and start this every time you redeploy your contracts so it loads the new addresses and abis.

We will have to make one last change to how we fire up our Docker container too. In our run.sh we will want to expose port 9999 so it works externally:

Now, we will want to ‘exit’ out of the Docker container and spin up a fresh one with 9999 exposed. You can do that with ./run.sh or:

docker run -ti --rm --name clevis -p 3000:3000 -p 8545:8545 -p 9999:9999 -v ~/nonce-upon-a-time:/dapp austingriffith/clevis:latest

When you get to the Clevis prompt run through a full test suite again:

clevis test full

Then fire up your relayer:

node relayer.js

You can test that the relayer is open by hitting:

http://localhost:9999

Now it’s time to wire up the frontend. Dapparatus is written to talk to a Proxy contract automatically, you just need to tell it where the relayer is. In src/App.js let’s add:

const METATX = {
endpoint:"http://0.0.0.0:9999/",
contract:require("./contracts/Proxy.address.js"),
}

This will need to be added into the <Dapparatus /> component:

Then, once our contracts are loaded, we need to save the Proxy contract as our metacontract so Dapparatus knows where to send the meta transactions:

Finally, the <Transactions /> component will also need a bunch of meta transaction pieces added too:

This wires up meta transactions and tells Dapparatus how to package up the data to sign.
metaAccount={this.state.metaAccount}
metaContract={this.state.metaContract}
metatx={METATX}
balance={this.state.balance} /* so we can metatx if balance 0 */
metaTxParts = {(proxyAddress,fromAddress,toAddress,value,txData,nonce)=>{
return [
proxyAddress,
fromAddress,
toAddress,
web3.utils.toTwosComplement(value),
txData,
web3.utils.toTwosComplement(nonce),
]
}}

At this point we should be ready to test. Fire up your dApp and submit a few normal transactions to make sure it works:

Now open up Safari or an incognito window and try submitting a few lines too:

It works, but we have a problem. Look at the Identicons. Our ephemeral account icon doesn’t match the icon that is next to the lines that we posted. This is because the msg.sender that the Stories contract sees is actually the Proxy contract. When building with meta transactions, the frontend has to do a little extra parsing of events to track down who really is responsible for the transaction.

What we need to do is parse the Forwarded events within the Proxy contract and compare them to the events from the Stories contract. Let’s add in an <Events /> component to the frontend

let metaEventLoader = ""
if(this.state.metaContract){
metaEventLoader = (
<Events
config={{hide:true}}
contract={this.state.metaContract}
eventName={"Forwarded"}
block={this.state.block}
onUpdate={(eventData,allEvents)=>{
console.log("Forwarded",eventData)
this.setState({MetaForwards:allEvents.reverse()})
}}
/>
)
}

Then, for each event of our story that has a sender equal to our Proxy contract address, we will need to run through all the Proxy events to see if we can find a match:

let metaAddress = ""
console.log("comparing",anEvent,this.state.metaContract)
if(anEvent.sender.toLowerCase() == this.state.metaContract._address.toLowerCase()){
console.log("SENDER IS METAACCOUNT, SEARCH FOR ACCOUNT IN FORWARDS!")
for(let f in this.state.MetaForwards){
if(this.state.MetaForwards[f].destination==contracts.Stories._address){
console.log("FOUND ONE GOING TO THIS CONTRACT:",this.state.MetaForwards[f].data,this.state.MetaForwards[f].signer)
if(this.state.MetaForwards[f].data.indexOf("0xebaac771")>=0){
console.log("this is the 'write' function...")
let parts = this.state.MetaForwards[f].data.substring(10)
let writeString = this.state.web3.eth.abi.decodeParameter('string',parts)
console.log("writeString",writeString)
if(writeString == anEvent.line){
console.log("MATCH!")
metaAddress=this.state.MetaForwards[f].signer
}
}
}
}
}
let accountToPay = anEvent.sender
let extraBlockie = ""
if(metaAddress){
accountToPay = metaAddress
extraBlockie = (
//metaAddress
<div style={{position:"absolute",left:0,top:0}}>
<Blockie config={{size:1.9}} address={metaAddress}/>
</div>
)
}

And if we find an extraBlockie, we’ll render it over the top of the original:

Now let’s fire off a few more lines and see if they are parsed correctly:

Nice! We can see now that our icons are correct. The Proxy contract icon is there, but then partially covered by the ephemeral account’s icon. Very rad.

Now any user from any device can jump on your dApp and contribute to the story on the blockchain while we pay the initial gas. Then, as the narrative continues and they wish to protect this throwaway account, they will be much more likely to learn about seed phrases. We will onboard them after they have provided value and earned reputation that they will wish to protect.

Final Thoughts and Discussion

The sweeping of this ephemeral account to a more secure identity can be done in a way where the throwaway account and the new account are linked on-chain and frontends can display them as one consistent user. Because a narrative has already developed, the ephemeral account may be linked to other accounts that can act as M of N social recovery addresses for the new identity contract.

When I speak with dApp developers, the first thing I mention (once they are stoked on the idea of using meta transactions to provide a seamless UX), is Sybil resistance. It will be very important and different for every dApp, but you need some metric to determine a good user from an attacker so you aren’t wasting gas. For my silly #EtherJamJam example dApp, since it was using Spotify to display songs, it was an easy choice to also use a Spotify social login to verify a token before paying the gas.

Another interesting topic is replayable meta transactions. You’ll notice above that we used a nonce to avoid replay attacks, but what if you wanted to replay transactions? What we found is we can use a period nonce that allows the meta transaction to become valid every X seconds and therefore you can create set-it-and-forget-it subscriptions or even delegated execution subscriptions. This body of work will eventually support the EIP1337 (l33t!) token standard.

If you are interested in building dApps powered by meta transactions with Clevis & Dapparatus, I’m hosting a workshop at Devcon in Prague Oct 31 at 3:30PM in the Coquelicot room.

Thanks for following along and if you have any questions or comments, please hit me up on Twitter: @austingriffith or https://austingriffith.com

--

--