How to be smarter about developing smart contracts in Solidity

by: | Jul 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:

contract CryptoSticker is ERC721, Ownable {
  function mint(address _to, uint256 _stickerId, string _stickerName) external;
  function burn(address _owner, uint256 _stickerId) external;
  function stickerName(uint256 _stickerId) external view returns (string);
}

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:

contract CryptoStickerStore is Ownable {
  function transferTokenOwnership(address _to) external;
  function mintSticker(uint256 _stickerId, string _stickerName) external;
  function burnSticker(uint256 _stickerId) external;
  function listSticker(uint256 _stickerId, uint256 _price) external;
  function unlistSticker(uint256 _stickerId) external;
  function listingPrice(uint256 _stickerId) external view returns (uint256);
  function listingName(uint256 _stickerId) external view returns (string);
  function purchaseSticker(uint256 _stickerId) external payable;
  function withdraw() external;
}

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:

function listSticker(uint256 _stickerId, uint256 _price) external onlyOwnerOf(_stickerId) {
  require(token.exists(_stickerId));
  require(listings[_stickerId].price == 0);
  require(_price > 0);

  uint256 index = listedIds.push(_stickerId) - 1;
  listings[_stickerId] = Listing(_price, index);

  emit ListSticker(msg.sender, _stickerId, _price);
}

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 the onlyOwnerOf 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.