Skip to content

Common Smart Contract Design Patterns

Here we outline several smart contract models that are useful for developers to get started on writing their own contracts. These examples demonstrate common use cases, design patterns, and best practives for developing Smart Contracts on the STRATO platform. All examples here use SolidVM.

Simple Storage

This contract demonstrates an easy way to store data as a contract on the STRATO blockchain.

contract SimpleStorage {
  string myString;
  uint myNumber;
  constructor(string _myString, uint _myNumber) {
    myString = _myString;
    myNumber = _myNumber;
  }
  function setString(string _s) {
    myString = _s;
  }
  function setNumber(uint _n) {
    myNumber = _n;
  }
}
Here we have two variables myString and myNumber that we might want to record the values of over time. If these values needs to be changed we simply call the respective set function with the new value. This would be done with a function call transaction.

Note that we do not have any accessor or 'getter' functions for our variables. This is because the STRATO platform easily indexes contract variables within its Cirrus Tables. Therefore if we wanted to get the current values of this contract, we would simply make a query to STRATO's Cirrus API for this contract. The Cirrus API allows for much faster data access than traditional contract function calls.

Governance

This contract demonstrates how membership of a given entity (usually a private chain) could be managed by using a voting system. For more information about private chains, see the Private Chains Section

contract Governance {
  enum Rule { AutoApprove, TwoIn, MajorityRules }
  Rule addRule;
  Rule removeRule;

  event MemberAdded (address member, string enode);
  event MemberRemoved (address member);

  mapping (address => uint) addVotes;
  mapping (address => uint) removeVotes;

  function voteToAdd(address m, string e) {
    uint votes = addVotes[m] + 1;
    if (satisfiesRule(addRule, votes)) {
      MemberAdded(m,e);
      addVotes[m] = 0;
    }
    else {
      addVotes[m] = votes;
    }
  }

  function voteToRemove(address m) {
    uint votes = removeVotes[m] + 1;
    if (satisfiesRule(removeRule, votes)) {
      MemberRemoved(m);
      removeVotes[m] = 0;
    }
    else {
      removeVotes[m] = votes;
    }
  }

  function satisfiesRule(Rule rule, uint votes) private returns (bool) {
    if (rule == Rule.AutoApprove) {
      return true;
    }
    else if (rule == Rule.TwoIn) {
      return votes >= 2;
    } else {
      return false;
    }
  }
}

Controlling Resource Access

Here we demonstrate how we can control or permission functions from being called by different users.

First we need to define a user as a smart contract.

contract User {
  enum Role { Admin, User };
  Role role;
  address userAddress;
  string username;
  constructor(bool _isAdmin, address _userAddress, string _username) {
    if (_isAdmin) {
      role = Role.admin
    }
    else {
      role = Role.User
    }
    userAddress = _userAddress;
    username = _username; 
  }
}
Right now this contract does not have any functions defined for it. Lets add some functionality so that a user can only change their own username.

// ...rest of user contract
  function setUsername(string _newUsername) {
    require(tx.origin == userAddress, 'You do not have permission to update this username');
    username = _newUsername;
  }
Since the tx.origin property contains the address of the account that called this function, we can check if the user who called this function has the same address as the registered address in the contract.

Contract Factory

Often we will want smart contracts to interact with each other and even make other smart contracts. We can accomplish this by using the solidity new keyword, and using a different contract's address as a reference in the contract.

Lets say we have a Widget Factory that will produce a new widget everytime we tell it to.

Every widget needs to have a unique ID and be able to know where it was made.

The factory needs to be able access the details of all the widgets it has made

contract WidgetFactory {
  uint counter = 0;
  string factoryName;
  mapping(uint => Widget) widgets;
  constructor(string _factoryName) {
    factoryName = _factoryName;
  }
  function makeWidget(string _color) returns (uint) {
    Widget widget = new Widget(counter, _color, this);
    widgets[widget.id()] = widget;
    counter += 1;
    return widget.id();
  }
  function getWidget(uint id) returns (Widget) {
    return widgets[id];
  }
  function paintAll(string _color) {
    for (int i = 0; i < counter; i++) {
      Widget w = widgets[i];
      w.paint(_color);
    }
  }
}
You'll notice that the factory references a contract called 'Widget', however we have not yet created that contract! Let's do that now:

contract Widget {
  uint id;
  string color;
  uint creationTime;
  WidgetFactory creator;
  constructor(uint _id, string_color, WidgetFactory _wf) {
    id = _id;
    color = _color;
    creator = _wf;
    creationTime = tx.timestamp;
  }
  function getInfo() returns (string) {
    return "Widget ID: " + string(id) + ". Color: "+ color + ". Made at UNIX time:" + string(creationTime) + ". Factory Name: " + creator.factoryName(); 
  }
  function paint(string _newColor) {
    color = _color;
  }

}
Now let's take a look at what is happening here.

In the WidgetFactory contract, we have a counter, factoryName, and a mapping widgets.

  • counter is used to create a serial ID for the new widgets being created. While widgets could be identified by their contract address, it is normally more human readable to use a plain integer number. In a practical use-case this might be a serial or ID number of a real object.
  • factoryName is the user given name to this factory
  • widgets is an easy way to access the widgets that this factory has made. Since it maps an integer key to a Widget contract, we can get a widget just by using it's ID that it was given when it was created.

A widgetFactory contract is only uploaded once, so its constructor is simply declaring the name of this factory.

The makeWidget function creates a new widget and returns its unique ID.

  1. A new widget is created using the new keyword. Note the return type for a new contract is that contract's address. Since we are creating a new contract, we have to pass in it's constructor args just like we would when uploading a new contract. So we pass it the current value of the counter as it's ID, the user provided color, and the this variable, which is the address of the current contract.
  2. We add the newly created Widget to the factory's widgets, using the the widget's ID.
  3. Increment the counter so that the next widget created has a new ID.
  4. Return the new widget's ID,

You may realize that we call the function widget.id() when we have not defined an id() function on a widget. This is because Solidity will auto-generate getter functions for state variables in contracts. The functions generated will have the same name as the variable itself.

To access a specific widget within getWidget we simply have to know the ID of the widget we are looking for, and we get the widget with that ID from the widgets mapping.

As added functionality, we add the paintAll function. This allows us to paint all the widgets in the factory to a new color. This prevents us from having to manually paint each widget one at a time. 1. Since counter will be the ID of the next widget to be made, every widget in the factory will have an ID in the range of 0 and counter, not inclusive. 2. Get each ID of the widgets, and call the widget's paint function with the provided color.

For the Widget contract, this is a simple extension of a SimpleStorage contract. It has the id, color, creationTime, and creator variables.

  • id is the unique ID of this widget
  • color is the color of this widget
  • creationTime is the UNIX epoch time in ms at which this contract (Widget) was created
  • creator is a reference to the factory contract that created this widget

In the Widget constructor we simply set the values of id and color, and creator with the incoming values. We set the creationTime as the timestamp for the current transaction.

By passing in the addres of the WidgetFactory contract, we can now access it's methods to get relevant information. In the getInfo function, we use this to get the name of the factory that created it. In a more complex setup, we could imagine a factory having many different attributes that the widget might want to access, but not necessarily keep a copy of every single value. This way if the factory changes names, the widget will always return the correct information, without having to manually update the name of it's own factory.

By having these two contracts deployed onto STRATO together, it allows to have a powerful model that is capable of many complex design patterns and functionality.

Using Cirrus to Your Advantage

Because data in smart contracts on STRATO can be queried directly through Cirrus, we can take advantage of this in the design of our smart contracts. Avoid using complex data structures like arrays or structs inside our contracts, since those will not be indexed by Cirrus, and therefore require a function call to be accessed and returned. Instead, those data structures should be split out into their own separate smart contract, and referenced as a contract address in the smart contract that needs the data.

Lets model the definition of a line and turn it into an optimized smart contract for STRATO. As a reminder, to define a line, you need to have two points on a graph, each with an x coordinate, and a y coordinate.

contract Line {
  struct Point {
    int x;
    int y;
  };
  Point p1;
  Point p2;
  constructor(int x1, int y1, int x2, int y2) {
    p1 = Point(x1, y1);
    p2 = Point(x2, y2);
  }
}
Now we will create two contracts, one for a line, which contains two Points, and a Point, which has an X and Y value.

contract Point {
  int x;
  int y;
  constructor(int _x, int _y) {
    x = _x;
    y = _y;
  }
}

contract Line {
  Point p1;
  Point p2;
 constructor(int x1, int y1, int x2, int y2) {
    p1 = new Point(x1, y1);
    p2 = new Point(x2, y2);
  }
}
This will allow us to keep all the data about a Point in its own Cirrus table, and the data in the Line table will show the addresses of the Point contracts the make this line. This is a trivial example, however for larger applications with more complex data, breaking contracts' data structures out into separate contracts will prove very useful in the long run.

Private Chain Contracts

STRATO's capability of having Private Chains allows for complex structuring of permissioned data. Here we show an example of how a contract can reference another contract on a private chain. It is important to note that because of the way private chain membership works, a contract can reference another contract on a private chain if that private chain is an ancestor of the current chain. So if we have the chain relationship as shown in the diagram:

Chain relationship

  • Chain A would not be able to call contracts on chains B or C.
  • Chains B and C would be able to call contracts on chain A.
  • Chain B would not be able to call contracts on chain C, or vice versa.

Since contracts cannot create contracts on private chains, there cannot be a factory as shown above in this setup.

Lets say we have some public facing information about a company. However we also have some data like the company's value and the company's stakeholder's name & investment. The general company information will be kept on a private chain, and the more sensitive data will be kept on a child private chain of that one. Here is what the company contract might look like:

contract Company {
  string name;
  string manager;
  string po_box;
  constructor(string _name, string manager, string _po_box) {
    name = _name;
    manager = _manager;
    PO_box = _po_box;
  }
  function getInformation() returns (string) {
    return name + "can written to at: " + po_box;
  }
}

Now lets define the contract that will be posted onto the child private chain:

contract DetailedCompany {
  string address;
  uint companyValue;
  Company company;
  mapping(string => uint) stakeholders;
  constructor(string _parentChainId, Company _company, string _address, uint value) {
    uint temp = uint(_parentChainId);
    company = Company(_company, temp);
    address = _address;
    value = _value;
  }
  function getInformation() returns (string) {
    return company.getInformation() + "\nHowever it can be written to directly at" + address + ". Its current estimated value is $" + value;
  }
  function addStakeHolder(string _name, uint investment) {
    stakeholders[_name] = investment;
    value = value + investment;
  }
  function removeStakeHolder(string _name) {
    uint investment = stakeholders[_name];
    stakeholders[_name] = 0;
    value = value - investment;
  }
}

With this model, we are able to have some information about the company on a more widely accessible chain, and then only members of the company view the more sensitive information by only having those members be part of the child chain. We are able to have a reference to the parent chain contract by including the address and parent chain of the Company contract.

Notice how only members of the chain can view it's value and make or remove stake in the company. For the sake of this example we do not add error checking to see if a stakeholder exists.

This is just one simple example of using private chains to permission data. We are sure you will find countless ways to utilize them to their fullest potential.

The Zoo

A common problem many programmers are asked to model is that of a Zoo, usually to enforce the ideas of OOP ideas such as Inheritance and Polymorphism. We will use this example as a simple way to incorporate all of the features of smart contracts we have shown so far to create a fully functional distrubuted application (Dapp) with Smart Contracts on the STRATO blockchain.

Specification

Entities & Actions:

  • Zoo
    • Accepts Visitors
    • Employs Zookeepers
    • Buys Animals from other zoos
    • Sells Animals to other zoos
  • Zookeeper
    • Enters Zoo
    • Enters Animal Exhibit
    • Feed Animal
    • Treat Animal
  • Visitor
    • Visit Zoo
    • Visit Animal Exhibit
    • Leave Zoo
  • Animal
    • Gets Fed
    • Gets Treated
    • Gets Visited
    • Makes a Noise
    • Provides Happiness to Visitors

As a start, we will define a contract for a generic Animal:

contract Animal {
  event animalFed (string name, string species, string food, Person zookeeper);
  event animalTreated (string name, string species, string memo, Person zookeeper);
  event animalNoise (string name, string species, string noise);
  string species;
  string name;
  string noise;
  uint value;
  Zoo zoo;
  uint weight;
  uint hungerLevel = 0; // on a scale from 0-10, 0 being not hungry, 10 being starving
  uint lastFed = 0;
  uint needsTreatment = false;
  uint lastTreated = 0;
  uint enjoymentFactor;
  string food;
  constructor(string species, string _name, uint _weight, string _noise,
     uint _value, Zoo _zoo, uint _enjoymentFactor) {
    species = _species
    name = _name;
    weight = _weight;
    noise = _noise;
    value = _value;
    zoo = _zoo;
    enjoymentFactor = _enjoymentFactor;
  }
  function feed(string _food, Person p) {
    require(_food == food, species + " only eat " + food);
    if (hungerLevel > 0) {
      hungerLevel = hungerLevel - 1;
    }
    emit animalFed(name, species, _food, p);
  }
  function treat(string memo, Person p) {
    needsTreatment = false;
    emit animalTreated(name, species, memo, p);
  }
  function makeNoise() returns (string) {
    emit animalNoise(name, species, noise);
  }
  function getSick() {
    needsTreatment = true;
  }
  function getHungry() {
    if (hungerLevel < 10) {
      hungerLevel = hungerLevel + 1;
    }
    else {
      getSick();
    }
  }
  function transfer(Zoo _newZoo) {
    zoo = _newZoo;
  }

}
In a more complicated application, one would likely define a specific contract type for different animal species so that all the data for that type could be properly captured. For this example we assume that all animals have the same basic properties that define them.

An animal can have different levels of enjoyment, so perhaps a more exciting animal has a higher enjoymentFactor and a less exciting one has a lower one.

For the feed function, we imagine that an animal has a specific diet and if a zookeeper attempts to feed the animal the wrong food, it will throw an error.

For the treat function, we imagine a zookeeper will add a memo for their treatment, i.e. A shot, a routine checkup, etc.

Whenever an animal gets fed, is treated, or makes a noise, a new solidity event is created so that it can be easily recorded when these things happened, what they entailed.

Let's define a person, who will either be a Visitor or a Zookeeper:

contract Person {
  string name;
  address addr;
  string role;
  uint money;
  uint happiness = 5;
  bool inZoo = false;
  string currentExhibit = "";
  constructor(string _name, string _role, address _address, uint _money) {
    name = _name;
    role = _role;
    addr = _address;
    money = _money;
  }
  function feedAnimal(Animal a, string _food) {
    require(currentExhibit == a.species(), "You are not in the proper enclosure to feed the animal");
    require(role == "Zookeeper", "Please do not feed the animals in the enclosure.");
    a.feed(_food, this);
  }
  function treatAnimal(Animal a, string memo) {
    require(currentExhibit == a.species(), "You are not in the proper enclosure to feed the animal");
    require(role == "Zookeeper", "Only a zookeeper can treat an animal");
    a.treat(memo, this);
  }
  function visitExhibit(Animal a) {
    require(inZoo);
    currentExhibit = a.species();
    if (role == "Visitor") {
      if (tx.timestamp % 87 != 0) {
        happiness = happiness + a.enjoymentFactor();
        if (tx.timestamp % 26 != 0) {
          a.makeNoise();
        }
      }
    }
  }
  function enterZoo() {
    inZoo = true;
    if (role == "Visitor") {
      happiness = happiness + 1;
    }
  }
  function leaveZoo() {
    inZoo = false;
    currentExhibit = "";
    if (role == "Visitor") {
      happiness = happiness - 1;
    }
  }
  function pay(uint amount) returns (uint) {
    require(amount >= money);
    money = money - amount
    return amount;
  }
  function getPaid(uint amount) returns (uint) {
    money = money + amount;
    happiness = happiness + 2;
    return money;
  }
}
A has a wallet, which allows them to pay for the zoo entrance, or for a zookeeper, get their wages from the zoo.

As an arbitrary metric, a person also has a happiness value, which increases when they visit the zoo and it's animals.

A Person can do many of the defined actions, which each have different behaviors depending on the role of the user completing the action. For example, when a visitor leaves the zoo, their happiness reduces, but when a Zookeeper, leaves, nothing happens to their happiness.

The feed and treat actions are also restricted by the user's role, so that a user cannot do the wrong action.

While these actions that a Person can do is defined directly on the Person contract, it just as feasible to omit them here and only put them in the overhead Zoo contract. This benefit is that it allows the Person contract to be a pure record of their data, without having to do actions on other Smart Contracts. This somewhat simplifies the number of custom function calls the developer must write for each entity. As an example of what this might look like, instead of having a function leaveZoo on the Person contract, we only define getters and setters for each field. Then within the Zoo contract we define a leaveZoo(Person p) function that accomplishes all of the same actions as the original Person leaveZoo() function.

contract Zoo {
  string name;
  uint funds;
  uint minBalance;
  uint entranceFee;
  uint dailyWage;
  mapping(address => Person) people; // maps a user's strato address to their smart contract record
  mapping(string => Animal) animals; //maps an animal's name to the smart contract record
  constructor(string _name, uint _funds, uint _minBalance, uint _entranceFee, uint _dailyWage) {
    name = _name;
    funds = _funds;
    minBalance = _minBalance;
    entranceFee = _entranceFee;
    dailyWage = _dailyWage;
  }
  function enterZoo() {
    Person p = people[tx.origin];
    if (p.role() == "Visitor") { 
      funds = funds + p.pay(entranceFee);
    }
    else {
      require(funds > (dailyWage + minBalance));
      funds = funds - dailyWage;
    }
    p.enterZoo()
  }
  function visitExhibit(string species) {
    Person p = people[tx.origin];
    p.visitExhibit(species);
  }
  function leaveZoo() {
    Person p = people[tx.origin];
    if (p.role() == "Zookeeper") {
      p.getPaid(dailyWage);
    }
    p.leaveZoo();
  }
  function buyAnimal(Zoo oldZoo, Animal a) {
    withdraw(a.value());
    oldZoo.removeAnimal(a.name());
    addAnimal(a);
  }
  function sellAnimal(Zoo newZoo, string name) {
    Animal a = animals[name];
    newZoo.withdraw(a.value());
    funds = funds + a.value();
    newZoo.addAnimal(a);
    removeAnimal(name);
  }
  function addAnimal(Animal a) {
    animals[a.name()] = a;
    a.transfer(this);
  }
  function removeAnimal(string name) {
    animals[a] = address(0);
    a.transfer(address(0));
  }
  function withdraw(uint amount) {
    require(funds >= (amount + minBalance));
    funds = funds - amount;
  }
}
This Zoo contract essentially acts as a manager of all the actions that we want to accomplish in the app's specification. It provides an interface for each action that completes it based on the user that has called the function.

Now lets examine how these contracts would work as a deployed application.

First, we would have to create the entities that exist within our Zoo-niverse - Zoos, People, and Animals. These contracts would have to be created programmatically/manually by a person using the contract. For example to instantiate the application with two zoos, you would upload two new Zoo contracts each with its respective properties such as the zoo name, its entrance fee, etc.

We must also create the Person contracts for each zookeeper and visitor.

For the sake of this example, we will assume that all animals are created manually by a user, i.e. an animal never gives birth and creates another animal. So whenever a new animal is added, the Animal contract is created, and then the user subseqently adds that Animal to the correct Zoo.

Modelling Scenarios

When a visitor enters the zoo, the enterZoo method must first be called on the Zoo contract. This requires them to pay the entrance fee to the zoo, and boosts the visitor's happiness. After this happens they are able to view different exhibits via the visitExhibit function since they are marked as 'in the zoo'. Zookeepers cannot enter the zoo unless the zoo has enough money to pay them for their shift. The zoo's funds are set aside for the time being and the zookeeper is only paid after they leave their shift via leaveZoo.

In the visitExhibit function, we can see that it now sets the person's currentExhibit to proper animal species, as well as boosts their happiness based on the specie's happinessFactor, and maybe the animal even will make a noise!. This emulates the actual experience of a person visiting a zoo.

A zookeeper can visit an exhibit and then subquently perform more actions on it since they are a privileged user. They can feed the animal if it is hungry or treat it if it is sick or injured. Currently the method of recording that an animal is hungry or sick (getSick, and getHungry) is a manual process that would need to be called by the user of the application.

After a person has visited all the exhibits they want to, they can leave the zoo using the leaveZoo function. To enter again this would require a Visitor to pay the entrance fee.

The zoo itself can buy and sell animals from other zoos in case it wants to diversify its exhibits or cut costs. Thus the buyAnimal and sellAnimal functions allow two zoos to trade amongst each other like a normal transaction of an asset.

The methods provided within this example are clearly very simple applications and do not provide much real-world functionality. However they serve as an inspiration and spring-board for you to create your complex system of Smart Contracts so you can create the Dapp perfect for your use-case.