How to be smarter about developing smart contracts in Solidity
Getting into blockchain development? Follow these step-by-step instructions how to write a smart contract for Ethereum blockchain using Solidity.
5 min. read - July 2, 2018
I’ve been hosting a series of internal meetups here at ArcTouch for our blockchain developers to exchange ideas and best practices on building blockchain solutions. My last presentation showed step-by-step instructions how to develop smart contracts in Solidity, and we used the example of collectible assets in the style of CryptoKitties. Our aim was to create unique “stickers” that could be traded and sold on the blockchain. The goal of the presentation then was to define, build, and test the set of smart contracts needed to support this functionality on Ethereum.
RELATED: How to set up a private Ethereum blockchain in 20 Minutes
The solution itself comprises two smart contracts. The first contract represents each sticker that can be individually traded and sold. Because each sticker is unique, we chose to use an ERC721-based contract. ERC721 is a standard similar to ERC20, but it represents a non-fungible asset. A non-fungible asset can’t be readily exchanged with another asset of its kind, regardless of value, because the asset itself contains data or attributes which make it genuinely unique.
Step 1: Find an open source Solidity contract as a starting point
The ERC721 standard is readily available, but rather than write our own implementation from scratch, we chose to leverage the existing base contracts from OpenZeppelin. OpenZeppelin, written and vetted by blockchain engineers, is a great resource for Solidity contracts that can be integrated and extended to build a complete blockchain solution.
Step 2: Define the abstract token contract
Based on ERC721, we defined an abstract contract for our CryptoSticker token:
1
2
3
4
5
In addition to the functions inherited from the base ERC721 contract — such as approve, transfer, etc. — our contract defines functions for creating a new sticker (mint)
, destroying a sticker (burn)
, and for retrieving additional metadata (stickerName)
for each unique sticker. The only other addition to the contract is Ownable, which can restrict certain functions to be callable only by the owner of the contract.
Step 3: Define the abstract store contract
The second contract represents the “store,” where individual stickers can be listed for sale by their respective owners and then subsequently purchased in a simple first-come-first-served market. The store contract is not based on any existing OpenZeppelin contract, and we’ve defined the abstract contract as:
1
2
3
4
5
6
7
8
9
10
11
It’s worth noting that the store contract uses a withdraw pattern instead of a direct-send pattern when a sticker purchase is finalized. As per the Solidity Security Considerations, this pattern helps to isolate any failures to the caller in question, rather than impacting all callers of a particular function.
For ease of maintenance, the CryptoSticker contract will be owned by the CryptoStickerStore contract (which is itself an Ownable). This allows minting and burning of stickers to be managed by the store, with default ownership assigned to the store itself. The transferTokenOwnership
function allows us to transfer ownership of the CryptoSticker contract if needed in the future (such as when upgrading the store contract). The remainder of the CryptoStickerStore contract defines functionality for listing, querying and purchasing stickers in the open market.
Step 4: Write test cases for use with TDD
When developing smart contracts, and specifically Ethereum contracts written in Solidity, I prefer a test-driven development (TDD) approach. This approach helps keep changes focused by concentrating on one function, or one set of functions, at a time. The goal is to clearly and completely identify the conditions for which the function should be expected to operate correctly. Any scenario that does not exactly match these conditions should fail.
For example, take listing a sticker for sale in the store. To create a successful listing:
Only the owner of the particular sticker may list the sticker for sale.
The sticker must actually exist.
The sticker must not have been previously listed for sale.
The requested listing price must be greater than zero.
In addition to the positive/successful test case, this yields at least the four following negative test cases:
WhenNotOwner_ListStickerShouldFail
WhenDoesntExist_ListStickerShouldFail
WhenAlreadyListed_ListStickerShouldFail
WhenPriceZero_ListStickerShouldFail
Step 5: Implement the smart contract code
Once the test cases have been implemented (and are subsequently failing due to the missing contract code), we can finally turn our attention to writing the needed code:
1
2
3
4
5
6
7
8
9
10
The function follows the recommended development pattern of Check-Effects-Interactions, which helps to prevent re-entrancy attacks by ensuring that all internal state modification (Effects) are completed before the function interacts with external contract or accounts (Interactions). It also makes the code cleaner and easier to read. Again, this pattern is recommended by the Solidity Security Considerations documentation. The Checks are:
onlyOwnerOf(_stickerId);
— Ensures that only the owner of this particular sticker can list it for sale.require(token.exists(_stickerId));
— Ensures that this particular sticker must actually exist. Although this is already implied by theonlyOwnerOf
modifier, I have included an explicit requirement for clarity.require(listings[_stickerId].price == 0);
— Ensures that this particular sticker has not been previously listed by checking that the lookup of the listing price is 0, the default value.require(_price > 0);
— Of course ensures that the listing price is greater than 0.
There are a number of additional tests that could and should be written to cover updates to the internal state, as well as validating that events were logged as expected.
Step 6: Create a custom dev chain for testing
As part of your TDD, it’s crucial to quickly and easily run all unit tests to verify that a given change has not altered the expected behavior of a contract. Public testnet Ethereum chains do not generally provide quick enough transaction processing times to make using them for unit tests a good choice (and they probably aren’t a good choice anyway because they are public). Instead, I rely on a custom proof-of-stake (PoS) development chain where the block time and the target gas price have both been set to zero. This allows insta-mining of transactions and eliminates the need to fund interacting accounts solely to pay for the transaction’s cost.
Of course, transaction cost is a real concern when running on a public chain, but a lack of funds (for paying transaction fees specifically) is not something that a smart contract function can even respond to. The caller either has the necessary funds to run the transaction or it doesn’t — and in that case, the code isn’t even executed. Such checks then lend themselves to integration testing instead of unit testing, so that a more complete picture of the necessary transaction flow can be considered. A similar development chain could be used for integration testing (and we often do just this). But it’s critical to also run integration tests against a public testchain (Rinkeby for example) as a last step in verifying correct operation of your contracts before deployment.
All done. Ready to develop your smart contract?
I hope this was helpful. The next step of course is to develop your own smart contract. We’re very bullish on blockchain — and we think now is the right time for companies to invest in building a blockchain PoC.
Blockchain technology and the ecosystem around it is changing fast. Please subscribe to our mailing list to get updates on our latest posts about mobile and blockchain.
And if you’re thinking about building a blockchain-based app but not sure where to start, contact us — at a minimum, we’re happy to provide free feedback and suggestions about how blockchain may fit into your business.
Article Author:
Subscribe for more insights
Get our newsletter in your inbox.
Contact us.
Let's build something lovable. Together.
We help companies of all sizes build lovable apps, websites, and connected experiences.