What we learned building our first Ethereum Dapp
A couple of months ago, myself and my colleague Samm decided it was past time we learned Ethereum and Solidity development. After passively following Ethereum since its launch, we decided to build an Ethereum Dapp as a side project to help us better understand the underlying technology.
We had a few goals in mind for the Dapp we wanted to build:
- The Dapp had to be small in scope, but cover all the meaningful features of Ethereum and Solidity.
- The Dapp had to run without us needing to manage any cloud services ourselves — meaning we would only write client code and Ethereum smart contracts.
- We would open source whatever code we wrote and share what we learned.
What we ended up building was a small game called Eth Plot! Eth Plot was inspired by Reddit’s famous r/place April Fool’s joke and by the million dollar homepage. It allows you to buy “plots” of digital space on a grid. In the plot you purchase, you can place an image and a link to a website of your choosing. You can also resell your plots at any given time.
Feel free to check out the Dapp here! It is running on the mainnet as well as Ropsten, Rinkeby and Kovan. We hope some folks use the application, but our primary goal with this project was our own education. We learned a lot, and in this article, we will share with you what we learned during this endeavor.
Warning: this is a bit of a long read! Want to skip directly to the code? Here’s the main smart contract, and here is the React/Redux application that forms the UI. We hope the sample code will be helpful to those embarking on the journey of building their first Dapp.
Our intended audience for this article is developers who are familiar with Ethereum, but perhaps haven’t written their first Dapp yet and would like an overview of what it entails, accompanied by some tips and tricks we picked up along the way. We do not claim to be experts, and aren’t aiming to write another in-depth tutorial on Dapp development. Here’s what we will be covering in this article:
- A high level overview of our tech stack — all the technologies we used to build Eth Plot.
- The details of the Eth Plot smart contract — implementing the functionality needed for this app in an efficient way was surprisingly complicated.
- Tips and tricks — a catch-all collection of helpful tidbits we learned while developing Eth Plot.
Our Tech Stack
For this project, we used the following stack:
- Solidity Smart Contracts — a primary contract and one helper
- IPFS for storing image data via Infura
- Truffle and Ganache for our development and testing framework
- React / Redux / Material UI / TypeChain for our front-end development
- MetaMask for our web3 provider in production
The core functionality of Eth Plot (the ability to buy and sell plots in a decentralized manner) is only made possible by Ethereum’s smart contract functionality. We also make frequent use of events, which provide a cheaper form of storage and allow for responsiveness in our web app. Because the main smart contract that powers Eth Plot is the most interesting aspect of the project, we’ve dedicated an entire section of this article to discussing it.
Trying to store image data on-chain is prohibitively expensive. As is now fairly standard, we instead upload the image to IPFS, receiving a hash of that image file in the process. This hash is what we store on-chain. This allows Eth Plot to remain purely decentralized despite not storing all the data on Ethereum.
To avoid the hassles associated with hosting and managing an IPFS node ourselves, we are using Infura’s IPFS service.
For our front end, we chose to use React, along with Redux for state management. This is a pretty standard web dev stack so we won’t spend any time here, but feel free to checkout the source code for details.
We also utilized components from the Material UI project. Material UI is set of React components that implements Google’s Material Design spec. This is a very well written library, with good documentation, that provides a bunch of nice looking components.
We enjoy writing our web based projects with TypeScript, so we utilized a helpful project called TypeChain which provides TypeScript bindings for Solidity contracts.
As is pretty standard, we require that users install MetaMask to interact with Eth Plot. Working with MetaMask was pretty straightforward, with a few exceptions:
- MetaMask is not ideal for local testing due to the unpredictable amount of caching. Even with the “Reset Account” feature of MetaMask, there are times where transaction state can be cached, making development difficult. Instead, we directly used the web3 provider from ganache while developing locally.
- Dealing with changes in user accounts for MetaMask state is not ideal. The recommended approach is to run an interval that repeatedly checks the current account.
That being said, MetaMask is a fantastic project, and the Dapp space would be nowhere near what it is today without MetaMask.
Smart Contract Deep Dive
The main smart contract which backs Eth Plot was one of the most challenging and unique aspects of the project. This was primarily due to how expensive it is to store data and perform lots of computations in a contract.
To start, we listed out the requirements of our contract:
- Define a grid system with constrained dimensions of at least 250x250.
- Each coordinate in the grid represents a 1x1 plot which has an owner, a buyout price, a website, and visual data associated with it.
- Plots can be larger than 1x1 to create a continuous area with an image
- To allow users to buy any plot they want — rather than being forced to buy an existing plot in its entirety — partial sections of plots can be sold and one plot can overlap another.
A naïve approach to the problem would be to create a contract with a large 2D array. Each entry in that array would represent a coordinate of the full grid and contain the corresponding information for that coordinate. When purchasing a plot, the buyer would send in a transaction which indicates all the coordinates they’d like to purchase and the data to associate.
Unfortunately, looking at the gas price of storing data (the SSTORE operation) it costs 20,000 gas to store a single 32 byte word. For our array we’d need to store at least 62,500 words (250x250) which would cost 1,250,000,000 gas. At a gas price of 5 gwei, that would cost ~6.25 ETH (~3,750 USD at the time of writing!) in transaction costs. This is also ~300 times higher than the maximum gas allowed in a single block (8,000,000 at time of writing). Even if we could get this kind of contract deployed, the transaction costs for interacting with it would be too high to ever see any adoption.
Our Approach — Contract Storage
Ultimately, we came up with an approach that makes efficient use of the limited resources available and allows Eth Plot to function as intended.
To start, let’s look at how we store the state of our contract. First, instead of a 2D array representing individual coordinates, we store summaries of plots: their starting coordinate (x & y) and dimensions (height & width). This has the benefit of larger plots not costing any more to store than smaller ones. The contract stores an array of these plots in the order they were purchased, with later plots appearing after earlier ones in the array. This is called the
ownership array in the contract. This
ownership array contains an entry for each plot which includes its geometry (x, y, w, h) and the owning address. Because the geometry has a range of 0–250, we can store each of the four values in a three byte
uint24 variable. The owner’s
address is 20 bytes, which means each entry in the array takes 20 + (4 * 3) = 32 bytes. This is the exact word size in the EVM, making the storage more efficient.
With this data representation, you might wonder how we support selling subsections of plots. We accomplish this with the
holes mapping, which tracks information about which plots overlap each other, meaning that the later plot bought a subsection of the earlier plot. Here is an example representation of the relationship between the
Adding this overlap logic was crucial as it allowed us to efficiently verify that the plot someone is trying to purchase is valid. By keeping the
holes mapping up-to-date, we can keep much less state in memory when validating new purchases — preventing us from exceeding gas limits as seen in the naïve approach.
Next, instead of storing the image data directly in the blockchain, we upload the image for a particular plot to IPFS, then store the IPFS hash of that image in the contract. Along with the image hash, we store the website associated with the plot. This information is stored in its own mapping separate from the
ownership object so we don’t need to read it into memory while computing new purchases, making purchase transactions cheaper. This information is stored in the
Finally, we store the current buyout price for a particular plot in a separate mapping called
plotIdToPrice. The buyout price for a plot can be updated by the owning user at any point. Just like the
data mapping, this information is stored separately from the
ownership data because we will access it much less often (only when computing payouts).
Our Approach — Purchasing Plots
Now that we’ve looked at the data storage, let’s walk through what happens when purchasing a plot. The function you call to purchase a new plot is called
purchaseAreaWithData. The data is specified in a somewhat unique format to make the execution of the contract as efficient as possible.
In our initial versions, the caller simply passed in the rectangle they wanted to purchase, and the contract attempted to compute all of the sub-plots which needed to be purchased. This worked in small cases, but we quickly pushed the limits of the EVM and encountered inflated transaction costs and stack-too-deep errors caused by the amount of data which was loaded into memory. Instead, the caller of the method (the web app in our case) does all of the computation beforehand and the contract merely validates this data and transfers funds. To do this, the caller sends in an array of sub-plots which form a complete tiling of the purchased area. These sub-plots represent sections of the existing plots which are about to be purchased. In addition to the sub-plots, the indices where these sub-plots exist are also passed into the function.
The contract itself is heavily commented and worth taking a look at, but here is a high level overview of what the purchase function does:
- Validates & bounds checks input parameters.
- Check to see that the sub-plots passed in form a complete tiling of the plot being purchased.
- Checks to make sure that all of the sub-plots being purchased are for sale and are still owned by the plot which the caller said they are purchasing from (this is where the
holesstructure is used).
- Pays out the owners of all the sub-plots with the funds sent in with the transaction and emits events for the transfers.
- Stores the new plot, the new data, the buyout price, updates all of the
holesarrays with all the new purchases, and emits a purchased event.
The remainder of the contract contains a few trivial functions which are less interesting. This includes owner accessible functions for changing the buyout price of a plot and changing the plot’s data, as well as admin functions for marking content as illegal and withdrawing the funds which have collected in the contract. Finally, there are some view functions which make reading the contract’s data more efficient by aggregating information from the various separate data structures.
Why Not Use ERC-721?
When we were working on our design, we came across the ERC-721 specification which looked promising, but ultimately didn’t fit with the requirements we had. The biggest issue we weren’t able to solve with ERC-721 was how to subdivide plots to allow for selling just a section of a plot. The other issue we saw with ERC-721 was there was no way to recombine the tokens into larger plots once they had been subdivided.
Tips and Tricks
We had some other interesting takeaways from this project which we wanted to share.
It’s tough to determine transaction costs, so write some tests then guess and check — One of the most useful optimization techniques for us was to write some unit tests which ran through what we thought was a representative set of transactions for our contract. By calculating the gas cost of this test, we could experiment with refactoring the contract and seeing what impact that had on gas cost. We were able to make the contract~40% more efficient using this technique.
Compute off-chain, validate on-chain — Any computation you can move out of the contract is worth it. If you find yourself writing complex contract code, see if you can instead compute more things off-chain and just have the contract validate that data. For example, rather than doing all the purchase computations in the contract itself, we pushed much of that work to the client — code sample here.
Pay attention to how storage is structured, mappings may be better than arrays — One interesting observation which came out of our optimization exercises was how much of a difference it made to split up our related data into multiple objects. Since the EVM has to load full words into memory at a time, only storing the plot geometry and owner in the ownership array prevented loading unneeded data about images, buyout prices, etc. Additionally, using mappings instead of parallel arrays for those other data structures was much cheaper because we don’t need to update the length value of the arrays (which involves an expensive SSTORE operation).
The truffle debugger is massively helpful, when it works — Using the truffle framework was very helpful as it allowed us to deploy local testnets, write unit tests, and debug contracts. The truffle debugger (
truffle debug command) was really helpful for debugging transactions which failed during manual testing. Unfortunately, due to how truffle unit tests are set up, the debugger doesn’t allow you to debug transactions originating from unit tests.
truffle doesn’t enable optimization by default — while writing this article, we realized that
truffle compile doesn’t enable contract optimization by default. Turning it on saved another ~20% gas in our tests. You can enable it via the truffle-config file.
Thank you for reading! We learned a great deal during this project and plan to do much more Ethereum development in the future.
Again, checkout the Eth Plot application in action! We aren’t looking to earn whatever fake internet points are available here, but we would love to engage with you, so please leave any comments or questions here or on the GitHub repo. Everything is open sourced, so take a look if you are searching for some starter code for the development of your first (or next) Dapp.
Update: August 2018
We learned something else after having this on the mainnet! If you’ve tried out the app recently on the mainnet, you’ll notice it’s painfully slow to load in the beginning. For those curious, the reason for this is that we are storing our app data in event logs for the sake of price. The downside here is that it is extremely time consuming to search through logs, especially on the mainnet. We didn’t notice this flaw when testing locally and on test nets due to size.
In order to solve this, we would need to add a dedicated caching layer. We likely won’t spend the time adding it to EthPlot since we are working on some other projects currently, but it’s helpful to keep in mind. I can foresee a future Infura (or Infura competitor!) making a feature to help with this issue as well.
Update: November 2018
EthPlot was one of the motivating factors that resulted in us starting Nodesmith.io. We wanted to build a service that gives you everything you need to build a decentralized application that doesn’t feel like it’s usability is hindered by the underling blockchain infrastructure. With Nodesmith, we’re giving you APIs that not only give you access to underlying blockchain infrastructure (like sending transactions and reading network state), but also gives you services that supercharge your application to make it more user friendly. For example, we’re adding smart contract event caches to make reading event logs really fast. We’re also adding a webhooks API so you don’t have to constantly poll the network.