Features of Solidity Code for Ethereum Oracle Contracts

This three-part series kicks off with a practical tutorial on a simple contract-oracle pair. Part 1 outlined the setup using Truffle, compiling and deploying the code to a test network, running the application, and debugging. However, we only skimmed the surface of the code itself. This segment will delve into the unique aspects of Solidity smart contract development, particularly in the context of contract-oracle interactions. While we won’t dissect every line (leaving that as an exercise for your further exploration), we aim to highlight the most notable, intriguing, and crucial features of the code.

To get the most out of this, it’s recommended to have your project version or the code readily available for reference.

You can find the complete code at this stage here: https://github.com/jrkosinski/oracle-example/tree/part2-step1

A Deep Dive into Ethereum and Solidity

While not the only fish in the sea, Solidity reigns supreme as the most prevalent smart contract development language, especially for Ethereum. Its widespread adoption is supported by a robust community and a wealth of available resources.

Diagram of crucial Ethereum Solidity features

Though object-oriented and Turing-complete, Solidity’s intentional limitations distinguish smart contract programming from conventional coding practices.

Every Solidity code journey begins with this line:

1
pragma solidity ^0.4.17;

As Solidity is under constant development, version numbers will inevitably change. Our examples use version 0.4.17, while the latest version at the time of publication is 0.4.25. The version you encounter while reading this might be entirely different. Exciting new features are in the pipeline (or at least on the drawing board), which we’ll touch upon shortly.

For a rundown of different Solidity versions, refer to the documentation.

Pro tip: Specifying a version range is possible (though not commonly seen) like this:

1
pragma solidity >=0.4.16 <0.6.0;

Unveiling Solidity’s Programming Prowess

Solidity boasts a mix of familiar and distinct features. Drawing inspiration from C++, Python, and JavaScript, it still manages to carve its own unique path.

Understanding the Contract

The .sol file serves as the foundational code unit. In BoxingOracle.sol, line 9 introduces the contract:

1
contract BoxingOracle is Ownable {

Much like classes form the bedrock of object-oriented languages, contracts do the same in Solidity. For now, think of contracts as the “classes” of the Solidity world.

Embracing Inheritance

Solidity contracts embrace inheritance, behaving as expected. Private members remain within their class, while protected and public members are passed down. Overloading and polymorphism function as anticipated.

1
contract BoxingOracle is Ownable {

The “is” keyword signals inheritance. Solidity even supports multiple inheritance, denoted by a comma-separated list of class names:

1
2
contract Child is ParentA, ParentB, ParentC {

While overly complex inheritance structures are best avoided, this insightful article delves into Solidity’s approach to the Diamond Problem.

Enumerating with Enums

Enums are present and accounted for:

1
2
3
4
5
6
    enum MatchOutcome {
        Pending,    //match has not been fought to decision
        Underway,   //match has started & is underway
        Draw,       //anything other than a clear winner (e.g., cancelled)
        Decided     //index of participant who is the winner 
    }

As in other languages, each enum value is assigned an integer starting from 0. These values can be converted to any integer type (uint, uint16, uint32, etc.), but explicit casting is mandatory.

Solidity Docs: Enums Enums Tutorial

Structuring Data with Structs

Similar to enums, structs provide a way to create custom data types. C/C++ veterans will find them familiar. Here’s a struct example from line 17 of BoxingOracle.sol:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//defines a match along with its outcome
    struct Match {
        bytes32 id;
        string name;
        string participants;
        uint8 participantCount;
        uint date; 
        MatchOutcome outcome;
        int8 winner;
    }

Note to Seasoned C Programmers: While struct “packing” exists in Solidity, its behavior might differ from C. Refer to the documentation and understand the context to determine if packing is beneficial.

Solidity Struct Packing

Once defined, structs act as native data types. Here’s how to “instantiate” the struct created above:

1
Match match = Match(id, "A vs. B", "A|B", 2, block.timestamp, MatchOutcome.Pending, 1); 

Deciphering Solidity’s Data Types

Let’s explore the fundamental building blocks of data in Solidity. Being statically-typed, explicit data type declarations are required.

Data types in Ethereum Solidity

Solidity Data Types

Working with Booleans

Boolean types are represented by bool, with values true or false.

Dealing with Numbers

Solidity supports signed and unsigned integers, ranging from 8-bit (int8/uint8) to 256-bit (int256/uint256). uint is shorthand for uint256 (similarly, int represents int256).

A notable absence is floating-point types. This decision stems from the inherent risks of using floating-point variables with monetary values, where precision loss can occur. Ether values are expressed in wei (1/1,000,000,000,000,000,000th of an ether), providing sufficient precision.

Limited support for fixed-point values is available. As per the Solidity documentation: “Fixed point numbers are not fully supported by Solidity yet. They can be declared, but cannot be assigned to or from.”

https://hackernoon.com/a-note-on-numbers-in-ethereum-and-javascript-3e6ac3b2fad9

Note: Sticking to uint is generally recommended. Reducing the size to, say, uint32, can surprisingly increase gas costs. Unless there’s a compelling reason, uint is your go-to choice.

String Manipulation

Strings in Solidity are a bit of a mixed bag. While the string data type exists, its functionality is limited. Common operations like parsing, concatenation, replacement, trimming, and even length calculation are absent, leaving you to implement them manually. Some opt for bytes32 as an alternative.

Fun article about Solidity strings

Perhaps creating your own feature-rich string type and sharing it with the community could be an interesting project!

Addressing the Address Type

Unique to Solidity, the address data type handles Ethereum wallet or contract addresses. This 20-byte value stores addresses of that specific size and includes type members tailored for them.

1
address internal boxingOracleAddr = 0x145ca3e014aaf5dca488057592ee45305d9b3a22; 

Address Data Types

Handling Dates and Times

Solidity lacks a dedicated Date or DateTime type like JavaScript. Dates are managed as uint (uint256) timestamps, typically in Unix format (seconds since epoch). Open-source libraries are available for converting to human-readable formats when needed. In our BoxingOracle example, we use DateLib.sol. OpenZeppelin offers date utilities and other helpful libraries, which we’ll explore shortly in the library section.

Pro tip: OpenZeppelin is a valuable resource (among others) for both knowledge and readily available code snippets to assist you in building contracts.

Mapping Data

Line 11 of BoxingOracle.sol introduces a mapping:

1
mapping(bytes32 => uint) matchIdToIndex;

Mappings in Solidity provide efficient lookups, similar to hashtables, where data resides on the blockchain. As the contract executes, data added to the mapping persists on the blockchain, making it accessible from anywhere.

Adding data to the mapping, from line 71 of BoxingOracle.sol:

1
matchIdToIndex[id] = newIndex+1

Retrieving data from the mapping, from line 51 of BoxingOracle.sol:

1
uint index = matchIdToIndex[_matchId]; 

Removing items from the mapping, though not used in this project, looks like this:

1
delete matchIdToIndex[_matchId];

Understanding Return Values

Solidity’s resemblance to JavaScript might lead you astray. Strict type definitions are enforced. Consider the function definition from line 40 of BoxingOracle.sol:

1
function _getMatchIndex(bytes32 _matchId) private view returns (uint) { ... }

Let’s break this down. function designates it as a function. _getMatchIndex is the function name (the underscore signifies a private member—more on that later). It accepts a single argument, _matchId (underscore convention for function arguments), of type bytes32. The private keyword restricts its scope, view informs the compiler that this function doesn’t alter blockchain data, and lastly: ~~~ solidity returns (uint) ~~~

This indicates that the function returns a uint. Functions returning void would omit the returns clause.

The parentheses around uint signify that Solidity functions can return tuples.

Consider the definition from line 166:

1
2
3
4
5
6
7
8
function getMostRecentMatch(bool _pending) public view returns (
        bytes32 id,
        string name, 
        string participants,
        uint8 participantCount,
        uint date, 
        MatchOutcome outcome, 
        int8 winner) { ... }

This function returns a tuple of seven elements. Since returning structs from public functions isn’t directly supported yet, tuples provide a workaround.

Line 159 illustrates returning a tuple:

1
return (_matchId, "", "", 0, 0, MatchOutcome.Pending, -1);

To receive such a return value, we can do this:

1
var (id, name, part, count, date, outcome, winner) = getMostRecentMatch(false); 

Alternatively, explicitly declare variables with correct types beforehand:

1
2
3
4
5
6
7
8
//declare the variables 
bytes32 id; 
string name; 
... etc... 
int8 winner; 

//assign their values 
(id, name, part, count, date, outcome, winner) = getMostRecentMatch(false); 

This gives us seven variables to store the returned values. If only specific values are needed:

1
2
3
4
5
6
//declare the variables 
bytes32 id; 
uint date;

//assign their values 
(id,,,,date,,) = getMostRecentMatch(false); 

Carefully counting commas is crucial when extracting specific values from a tuple.

Importing External Code

Lines 3 and 4 of BoxingOracle.sol demonstrate imports:

1
2
import "./Ownable.sol";
import "./DateLib.sol";

As expected, these import definitions from files residing in the same project folder as BoxingOracle.sol.

Modifying Function Behavior with Modifiers

Function definitions often include various modifiers. First, visibility: private, public, internal, and external—function visibility.

Moreover, the keywords pure and view inform the compiler about potential data modifications, impacting gas costs. For a detailed explanation, refer to: Solidity Docs.

Now, let’s focus on custom modifiers. Examine line 61 of BoxingOracle.sol:

1
function addMatch(string _name, string _participants, uint8 _participantCount, uint _date) onlyOwner public returns (bytes32) {

The onlyOwner modifier restricts access to the contract owner. This crucial feature is not native to Solidity (though it might be in the future). onlyOwner exemplifies a custom modifier.

Let’s investigate its definition in the file Ownable.sol, imported on line 3 of BoxingOracle.sol:

1
import "./Ownable.sol"

To utilize the modifier, BoxingOracle inherits from Ownable. Within Ownable.sol on line 25, the modifier’s definition resides inside the “Ownable” contract:

1
2
3
4
modifier onlyOwner() {
	require(msg.sender == owner);
	_;
}

(This Ownable contract, by the way, is sourced from one of OpenZeppelin’s public contracts.)

The modifier keyword allows its use to alter function behavior. The heart of the modifier is a “require” statement. These act like assertions but are not for debugging. If the condition fails, an exception is thrown.

Paraphrasing the “require” statement:

1
require(msg.sender == owner);

It essentially means:

1
2
if (msg.send != owner) 
	throw an exception; 

Solidity 0.4.22 and later allow adding an error message:

1
require(msg.sender == owner, "Error: this function is callable by the owner of the contract, only"); 

Finally, the peculiar line:

1
_; 

The underscore serves as shorthand for “execute the entire modified function.” In essence, the require statement executes first, followed by the actual function.

Modifiers offer even more capabilities. Explore the documentation for a deeper dive: Docs.

Leveraging the Power of Libraries

Solidity introduces the concept of libraries. Our project demonstrates this in DateLib.sol.

Solidity Library implementation!

This library simplifies date and time handling. It’s imported into BoxingOracle on line 4:

1
import "./DateLib.sol";

And utilized on line 13:

1
using DateLib for DateLib.DateTime;

DateLib.DateTime is a struct exposed by the DateLib contract (line 4 of DateLib.sol). This line signifies that we’re “using” the DateLib library for a specific data type. Methods and operations defined in the library now apply to that data type.

For clearer examples, check out libraries provided by OpenZeppelin, such as Math, SafeCast, and SignedMath. These libraries apply to native Solidity data types and enjoy widespread use.

Defining Contracts with Interfaces

Similar to mainstream object-oriented languages, Solidity supports interfaces. Defined as contracts, interfaces omit function bodies. Refer to OracleInterface.sol for an example. Here, the interface represents the oracle contract, whose actual implementation resides elsewhere with a separate address.

Following Naming Conventions

While not globally enforced, adhering to naming conventions enhances code readability and collaboration.

Project Overview: Setting the Stage

With a grasp of the language features, let’s zoom in on the project’s code. The project aims to provide a semi-realistic demonstration of a smart contract leveraging an oracle. At its core, it’s a contract interacting with another.

Here’s the business case:

  • Users can place bets (using ether) on boxing matches, receiving winnings if successful.
  • Bets are placed through a smart contract (a full DApp with a web3 front-end in a real-world scenario; we’re focusing on the contract side).
  • A separate smart contract, the oracle, maintained by a third party, keeps track of boxing matches, their states (pending, in progress, finished), and the winner (if decided).
  • The main contract fetches pending matches from the oracle, making them available for betting.
  • Bets are accepted until a match begins.
  • Once decided, the main contract distributes winnings (and losses) using a simple algorithm, taking a commission, and paying out upon request (losers forfeit their stake).

Betting rules:

  • A minimum bet (in wei) is defined.
  • No maximum bet exists.
  • Bets are accepted until a match’s status changes to “in progress.”

Winnings distribution:

  • All bets go into a “pot.”
  • A small percentage is deducted as the house commission.
  • Winners receive a portion proportional to their bet size.
  • Winnings are calculated upon the first user request after the match result is available.
  • Winnings are awarded upon the user’s request.
  • In case of a draw, bets are refunded, and the house receives no commission.

Unmasking BoxingOracle: The Oracle Contract

Public and Private Functions

The oracle has two sides: one for the owner/maintainer to feed data (from the outside world) onto the blockchain and a public-facing side providing read-only access to this data.

Public Functions:

  • List all matches
  • List pending matches
  • Retrieve details of a specific match
  • Get the status and outcome of a specific match

Owner Functions:

  • Add a match
  • Change match status
  • Set match outcome
Illustration of user and owner access elements

User story:

  • A new boxing match is scheduled for May 9th.
  • The contract maintainer (e.g., a sports network) adds the match to the oracle with a “pending” status. Anyone can now access and utilize this data.
  • Once the match starts, its status is updated to “in progress.”
  • After the match, the status changes to “completed,” and the winner is declared.

Dissecting the Oracle Code

Let’s analyze BoxingOracle.sol’s code (line numbers refer to this file).

Lines 10 and 11 define how match data is stored:

1
2
	Match[] matches; 
	mapping(bytes32 => uint) matchIdToIndex; 

matches is an array holding match instances. The mapping allows quick retrieval of a match’s index in the array using its unique ID (a bytes32 value).

Line 17 defines the match structure:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    //defines a match along with its outcome
    struct Match {
        bytes32 id;             //unique id
        string name;            //human-friendly name (e.g., Jones vs. Holloway)
        string participants;    //a delimited string of participant names
        uint8 participantCount; //number of participants (always 2 for boxing matches!) 
        uint date;              //GMT timestamp of date of contest
        MatchOutcome outcome;   //the outcome (if decided)
        int8 winner;            //index of the participant who is the winner
    }

    //possible match outcomes 
    enum MatchOutcome {
        Pending,    //match has not been fought to decision
        Underway,   //match has started & is underway
        Draw,       //anything other than a clear winner (e.g., cancelled)
        Decided     //index of participant who is the winner 
    }

Line 61: The addMatch function, restricted to the contract owner, adds new matches.

Line 80: The declareOutcome function lets the owner set the match as “decided” and declare the winner.

Lines 102-166: These functions are publicly accessible, offering read-only data:

  • getPendingMatches returns IDs of all “pending” matches.
  • getAllMatches returns IDs of all matches.
  • getMatch returns complete details of a match given its ID.

Lines 193-204: These functions aid in testing, debugging, and diagnostics:

  • testConnection verifies contract reachability.
  • getAddress returns the contract’s address.
  • addTestData populates the match list with test data.

Take some time to explore the code and run the oracle contract in debug mode (as explained in Part 1). Experiment with different function calls and observe the results.

Introducing BoxingBets: The Client Contract

It’s crucial to delineate the client contract’s responsibilities. It does not manage boxing match lists or declare outcomes. Instead, it “trusts” (a topic for Part 3) the oracle for that. The client contract handles bet acceptance, calculates winnings distribution based on the oracle’s match outcome, and transfers funds accordingly.

Furthermore, everything operates on a pull-based model. The contract pulls data from the oracle—match data, outcomes, etc.—and calculates/transfers winnings upon user request.

Essential Functions

  • List pending matches
  • Retrieve match details
  • Get match status and outcome
  • Place a bet
  • Request/receive winnings

Unveiling the Client Code

Let’s examine BoxingBets.sol’s code (line numbers correspond to this file).

Lines 12 and 13 define mappings for storing contract data:

Line 12 links user addresses to bet ID lists, enabling quick retrieval of all bets placed by a specific user.

1
    mapping(address => bytes32[]) private userToBets;

Line 13 maps match IDs to bet instance lists, allowing retrieval of all bets for a particular match.

1
    mapping(bytes32 => Bet[]) private matchToBets;

Lines 17 and 18 deal with oracle connection. The boxingOracleAddr variable stores the oracle contract’s address (initialized to zero). Hardcoding the address would hinder flexibility (a double-edged sword—we’ll discuss this in Part 3). The next line creates an instance of the oracle interface (defined in OracleInterface.sol) using the stored address.

1
2
3
    //boxing results oracle 
    address internal boxingOracleAddr = 0;
    OracleInterface internal boxingOracle = OracleInterface(boxingOracleAddr); 

Line 58 introduces the setOracleAddress function, allowing modification of the oracle address (and re-instantiation of the boxingOracle instance with the new address).

Line 21 sets the minimum bet size in wei (a tiny amount: 0.000001 ether).

1
    uint internal minimumBet = 1000000000000;

Lines 58 and 66 present setOracleAddress and getOracleAddress, respectively. The former is restricted to the contract owner using the onlyOwner modifier. The latter is public, allowing anyone to see the active oracle.

1
2
3
function setOracleAddress(address _oracleAddress) external onlyOwner returns (bool) {...

function getOracleAddress() external view returns (address) { ....

Lines 72 and 79 contain getBettableMatches and getMatch, respectively, which simply forward calls to the oracle and return the results.

1
2
3
function getBettableMatches() public view returns (bytes32[]) {...

function getMatch(bytes32 _matchId) public view returns ( ....

The placeBet function (line 108) plays a vital role:

1
function placeBet(bytes32 _matchId, uint8 _chosenWinner) public payable { ...

The payable modifier is key here. It allows the function to accept funds along with other data. This is where users define their bet, its amount, and send the money.

Before accepting the bet, several checks are performed. On line 111:

1
require(msg.value >= minimumBet, "Bet amount must be >= minimum bet");

msg.value holds the sent amount. If all checks pass, line 123 transfers ownership of that amount from the user to the contract:

1
address(this).transfer(msg.value);

Lastly, line 136 offers a helper function for testing and debugging the oracle connection:

1
2
3
    function testOracleConnection() public view returns (bool) {
        return boxingOracle.testConnection(); 
    }

Summing It Up

This example stops at accepting bets. The logic for winnings calculation, payout, and other functionalities is intentionally omitted to keep things simple and focused on demonstrating oracle interaction. The complete implementation exists in a separate project, an extension of this example currently in development.

We’ve gained insights into the codebase and explored various language features offered by Solidity. This part aimed to enhance your understanding of the code and use it as a springboard to dive into Solidity and smart contract development. The final part will delve into the strategic, design, and philosophical implications of using oracles in the context of smart contracts.

Optional Challenges for the Inquisitive Mind

If you’re eager to learn more, consider extending this code: implement new features, fix bugs, complete unfinished functionalities, test and modify function calls, add a web3 front-end, or create a facility for match removal or outcome modification (for error correction). What about handling cancelled matches? Try implementing a second oracle. While a contract can utilize multiple oracles, what challenges arise?

A few suggestions to get you started:

  • Run the contract and oracle in a local testnet (as explained in Part 1) and experiment with function calls.
  • Implement winnings calculation and payout upon match completion.
  • Add functionality for bet refunds in case of a draw.
  • Allow bet cancellation or refund requests before a match starts.
  • Handle match cancellations (requiring full refunds).
  • Ensure that the oracle used to place a bet is the same one determining its outcome.
  • Implement a second oracle with different features or covering a different sport (the participant structure allows for this).
  • Modify getMostRecentMatch to return either the most recently added match or the one closest to the current date.
  • Implement exception handling.

Once you’re comfortable with the contract-oracle interaction, Part 3 will address strategic, design, and philosophical considerations arising from this example.

Licensed under CC BY-NC-SA 4.0