11 Commandments Of Smart Contract Design

smart contract design patterns

Unlike my sleeping pattern, design patterns are meant to make things more stable and predictable in the future. They are an elegant solution to common problems in software design. They describe patterns for managing object creation, composing objects into larger structures, and coordinating control flow between them. There are two things to consider when writing a smart contract in Solidity:

  • Choosing the right design pattern (and sticking to it!)
  • Avoiding mistakes leading to common attacks

This time I will be talking about choosing the right design and why it matters.

Enter the real world

Back at the end of 2018 I had the task of making a custom bounty contract and was challenged with the problem of performance, size, maintainability, and well, using it in the real world.

The original contract I wrote was formidable. It worked as proof-of-concept but it was too long, cost-inefficient, with non-consistent error checking and vague feedback to the user about what is going on.

I needed a clear CHECKLIST in order to see the woods again and pay attention to important details that would make or break the functionality.

Before I present the checklist let’s first try and understand what are contracts in our example trying to do. Just as a hint at the complexity behind it and the necessity of a consistent approach.

What is it?

smart contract topicshare

Topicshare.io was made to enable a decentralized and simple way for people to advertise or spread messages in an organic way. Imagine that you just released a new product and you want others to spread the word about your product.

Rather than paying for an ad, you can harness the power of the social media community to share your product via their networks. For their efforts, you can reward them automatically, using a campaign reward system.

How does it work?

The original contract was broken in two contracts interfacing one another, enabling the selective upgradability of the system.

  • TopicShareOracle.sol

The TopicShareOracle smart contract uses oraclize.it (now called provable.xyz) and it’s API library to retrieve the message text of posts on Twitter via URL. These messages are stored on the Ethereum blockchain within this smart contract and are publicly accessible. Any user can call this contract to store a new Twitter post. Storing the data from Oraclize callback requires that this contract also has balance in Ether to function, and anyone can contribute Ether to keep the oracle running.

  • TopicShareBounty.sol

The main TopicShare smart contract is similar to Bounties Network’s StandardBounties.sol. Using the data on the TopicShareOracle contract, users are able to open a new campaign or claim fulfillment rewards on existing ones.

Completing a campaign requires that the user proves that the new tweet has not been used in the past and that it contains the same text as the original Twitter post for the campaign. Because this proof can be done programmatically, campaign verification and reward resolution can happen all at once, and without the need of the campaign creator to verify or accept fulfillments.

Campaign creators have control over their campaigns and can edit the fulfillment rewards or close already running campaigns they have. Anyone can contribute to the existing campaign to continue funding it and encourage others to keep fulfilling the campaign.

You can find contracts and inspect them yourself here.

Now that we understand what it does and how it works, let’s check out what we all came here for.

11 commandments to stick by

smart contract design patterns

1. Object-Oriented Design

OOP foundation is based on a pre-thought of decoupling your logic from objects and separate the responsibility accordingly. So, keeping objects simple and understandable in terms of what they are supposed to do is crucial. For example, TopicShareBounty contract uses two basic objects to simplify the management and access to the contract:

  • A Bounty object:
struct Bounty {
   address issuer;             // Owner of a bounty
   uint256 fulfillmentAmount;  // The amount that a user gets paid for fulfilling a bounty
   uint256 balance;            // The amount of funds the bounty has available for fulfillment payouts
   string tweetText;           // The specific text used to check for a fulfillment
   string topic;		// The specific text used to represent topic
   string follows;		// The specific text used to represent number of followers requested
   bool bountyOpen;            // Bounty state machine, checking that the bounty is open
   mapping (bytes32 => bool) tweetsUsed; // Tweets already used to filfill the bounty
}
  • A Fulfillment object:
struct Fulfillment {
    uint256 fulfillmentAmount;  // The amount the user got paid for fulfilling the bounty
    address fulfiller;          // The user that fulfilled the bounty
    string tweetId;             // The tweet ID used to fulfill the bounty
}

2. Fail early and fail loud

All functions should check for valid conditions as early as possible, and throw an exception if they fail. TopicShareBounty contract has ~9 function modifiers with require statements which are used at the beginning of a function. Every function has at least some number of relevant modifiers where applicable. Every required statement has an exception description customized for the failed condition. Additional conditions are checked as early as possible in the function to reduce unnecessary code execution.

3. Contract lifecycle

Take advantage of Open-Zeppelin libraries to manage the lifecycle of the contract.

4. Circuit Breaker

The contract should be pauseable, allowing the contract owner to disable all users from accessing all core functions. Read-only functions are still available and accessible when the contract is paused.

5. Contract Mortality

The contract should be destructible, allowing the contract owner to destroy the contract, release stored funds, and clean up data on the blockchain. You don’t want to create a zombie contract, do you?

6. Restricting Access

Throughout the TopicShareBounty contract, I ensure that only certain people with specific roles can call certain functions.

7. Contract Ownership

TopicShareBounty uses the Open-Zeppelin Ownable library. When the contract gets deployed, the contract creator is automatically set as the owner. Only this owner can access the following contract functions:

  • setOracle(): Allowing the owner to change the contract used to retrieve Twitter posts
  • pause and unpause: Allowing the owner to pause the contract
  • destroy() and destroyAndSend(): Allowing the owner to destroy the contract

8. Pull Payments

TopicShareBounty uses a user-initiated pull pattern for paying users who fulfill a bounty. Fulfillers must initiate a transaction with the TopicShareBounty contract which validates that the bounty conditions were met, and then will pay the user for their efforts. At no point does the contract automatically send funds to any other users than the calling address.

9. State Machine

TopicShareBounty keeps a bounty lifecycle that manages which functions can be called for certain bounties. As mentioned earlier, the bounty lifecycle can only be managed by the bounty issuer and is tracked on the bounty object by Bounty.bountyOpen boolean. The following functions check that Bounty.bountyOpen is true before running:

  • contribute(): Allowing any user to add additional funding to an open bounty
  • fulfillBounty(): Allowing any user to fulfill an open bounty and get paid for their efforts
  • changePayout(): Allowing the bounty issuer to change the fulfillment amount for an open bounty

10. Beware the speed bump

TopicShareBounty has a natural speed bump built into its functionality since it relies on an Oracle to retrieve and store Twitter posts. This limits the number of times a user will be able to execute certain functions due to other restrictions like checking if a Twitter post was already used to fulfill a bounty.

11. Upgradability

The TopicShare dApp is built on two separate contracts:

  • TopicShareBounty.sol (The bounty management, and twitter verification logic)
  • TopicSharOracle.sol (The Twitter oracle for retrieving and storing posts)

Since this contract is dependent on data scraped from Twitter through an Oracle, any changes to Twitter frontend can cause the contract to break. To prevent this, the TopicShareOracle contract can be redeployed with updated XPath logic, and the TopicShareBounty contract can be updated by the owner to point to the new Oracle. This transition will not break any existing bounties or future fulfillments.

So there it is.

Enjoy!

Related Posts