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.

Though object-oriented and Turing-complete, Solidity’s intentional limitations distinguish smart contract programming from conventional coding practices.
Navigating Solidity Versions
Every Solidity code journey begins with this line:
|
|
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:
|
|
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:
|
|
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.
|
|
The “is” keyword signals inheritance. Solidity even supports multiple inheritance, denoted by a comma-separated list of class names:
|
|
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:
|
|
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.
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:
|
|
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.
Once defined, structs act as native data types. Here’s how to “instantiate” the struct created above:
|
|
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.

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.
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.
|
|
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:
|
|
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:
|
|
Retrieving data from the mapping, from line 51 of BoxingOracle.sol:
|
|
Removing items from the mapping, though not used in this project, looks like this:
|
|
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:
|
|
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:
|
|
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:
|
|
To receive such a return value, we can do this:
|
|
Alternatively, explicitly declare variables with correct types beforehand:
|
|
This gives us seven variables to store the returned values. If only specific values are needed:
|
|
Carefully counting commas is crucial when extracting specific values from a tuple.
Importing External Code
Lines 3 and 4 of BoxingOracle.sol demonstrate imports:
|
|
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:
|
|
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:
|
|
To utilize the modifier, BoxingOracle
inherits from Ownable
. Within Ownable.sol on line 25, the modifier’s definition resides inside the “Ownable” contract:
|
|
(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:
|
|
It essentially means:
|
|
Solidity 0.4.22 and later allow adding an error message:
|
|
Finally, the peculiar line:
|
|
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.

This library simplifies date and time handling. It’s imported into BoxingOracle on line 4:
|
|
And utilized on line 13:
|
|
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
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:
|
|
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:
|
|
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.
|
|
Line 13 maps match IDs to bet instance lists, allowing retrieval of all bets for a particular match.
|
|
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.
|
|
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).
|
|
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.
|
|
Lines 72 and 79 contain getBettableMatches
and getMatch
, respectively, which simply forward calls to the oracle and return the results.
|
|
The placeBet
function (line 108) plays a vital role:
|
|
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:
|
|
msg.value
holds the sent amount. If all checks pass, line 123 transfers ownership of that amount from the user to the contract:
|
|
Lastly, line 136 offers a helper function for testing and debugging the oracle connection:
|
|
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.