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.

Naïve Approach

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 ownership and holes arrays.

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 data mapping.

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 holes structure is used).

structure 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 holes arrays with all the new purchases, and emits a purchased event.

Other Functionality

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.