Building A Betting Smart Contract That Interacts With Third Party API Using Oraclize
Overview
The smart contract takes bet on a game between two teams, and at the end of the game checks for the winning team using oraclizeAPI then distributes loosers pool to winners based on percentage ratio of individual bet.
Pretty smart hun!
What Will I Learn?
In this tutorial you would learn how to build a smart contract in solidity that uses oraclize to get information form a third party.
- Build a betting smart contract in solidity
- Use Oraclize with ethereum bridge on testnet to allow your smart contract communicate with third party
- Test for your smart contract using truffle
Requirements
For this tutorial, you will be needing the following.
- A linux machine
- Have node and npm installed on your machine
- Install truffle (To complile and test our smart contract)
- Download and install GUI version of ganache (To run our own private blockchain)
- Install ethereum bridge (Allows us to use oraclize on testnet)
- Any IDE that supports solidity syntax, atom or phpstorm will do just fine
Difficulty
Intermediate
Setup
Installation
For this tutorial we need to set up a new truffle project for our smart contract.
- Create a project directory with a suitable name for this tutorial, i would be going with the project name
Game
- Navigate to the directory in your terminal and run command
truffle init
. Now you would have a project structure that looks exactly like the diagram below.
The/var/www/html/Game
was placed there by phpstorm showing the path to my project directory, so if yours does not show that its ok. - In the contract folder, create a file and name it SafeMath.sol then copy the content from here into it. This is the SafeMath Library by OpenZappelin, we will be needing it.
- In the contract folder create another file with name OraclizeAPI.sol also copy the content from here into it.
Buidling A Simple API Endpoint
We would be needing a simple API for oraclize to communicate it to determine the winner of the bet for our smart contract.
Note: You can skip this step and simply use the existing endpoint in the smart contract
Create a a subdomain api
on your server so we can have something like https://www.api.yourdomain.com
. In the folder pointing to your subdomain create a file index.php
and put the content below.
<?php
$game = file_get_contents(__DIR__.DIRECTORY_SEPARATOR.'game.json');
header("Content-Type: application/json;charset=utf-8");
print_r($game);
exit;
Create another file game.json
and also put the content below.
{
"winner": "swansea"
}
Now if you visit your subdomain url your page should return a valid json file. When oraclize calls the url its should be able to get the winner.
Coding Our Smart Contract
The full code to this project can be found here.
The Code
pragma solidity ^0.4.18;
import "./OraclizeAPI.sol";
import "./SafeMath.sol";
contract Game is usingOraclize {
using SafeMath for uint;
//MINIMUM_STAKE allowed is 0.01 ether
uint public constant MINIMUM_STAKE = 10 ** 16;
//MAXIMUM_STAKE allowed is 1 ether
uint public constant MAXIMUM_STAKE = 10 ** 18;
string public constant TEAM_A = 'realmadrid';
string public constant TEAM_B = 'swansea';
//Game playoff
uint256 public startTime;
//Game off
uint256 public endTime;
//Address that created the contract
address public owner;
//Address that bet charges are paid to
address public referee;
bool public closed = false;
//Total amount to be distributed among winners
uint public totalPayable;
//Total amount of winners stake, to us to calculate
//ratio of total payable to be payed per account
uint public totalHolding;
string public winner;
//Precision to use for calculation that will yield
//decimal values to convert to non decimal value
uint precision = 10 ** 18;
//Profile of each account betting
struct Punter {
address account;
uint stake;
string supporting;
}
//Lists of all bets placed
Punter[] public bettings;
struct Ration {
address account;
uint percentage;
}
Ration[] public rations;
mapping (address => uint) public payouts;
mapping (address => uint) public payoutAddresses;
mapping (address => uint) public bettingAddresses;
mapping (bytes32 => bool) public queryIds;
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
modifier ended() {
require(block.timestamp >= endTime || closed == true);
_;
}
modifier notStarted() {
require(started() == false);
_;
}
modifier validContribution() {
require(msg.value >= MINIMUM_STAKE && msg.value <= MAXIMUM_STAKE);
_;
}
modifier haveNoStake() {
require(bettingAddresses[msg.sender] == 0);
_;
}
modifier validTeam(string team) {
require(keccak256(TEAM_A) == keccak256(team) || keccak256(TEAM_B) == keccak256(team));
_;
}
modifier notClosed() {
require(closed == false);
_;
}
event Bet(address account, uint amount);
event BetClosed(uint timestamp, string result);
event FetchedResult(string winner);
event newOraclizeQuery(string description);
event Payment(uint winning);
/*
* @param start startTime
* @param end endTime
*/
function Game(uint start, uint end) public payable {
startTime = start;
endTime = end;
owner = msg.sender;
OAR = OraclizeAddrResolverI(0xF08dF3eFDD854FEDE77Ed3b2E515090EEe765154);
}
/*
* @returns bool
*/
function started() public view returns (bool) {
return block.timestamp >= startTime;
}
/*
* @param team realmadrid, swansea
*/
function placeBet(string team) notStarted validContribution haveNoStake validTeam(team) public payable returns (uint) {
bettings.push(Punter({
account: msg.sender,
stake: msg.value,
supporting: team
}));
bettingAddresses[msg.sender] = bettings.length - 1;
Bet(msg.sender, msg.value);
}
function endGame() onlyOwner public {
closed = true;
}
/*
* @param address
* @return team
* @return stake
*/
function getAccountInfo(address account) public view returns (address, uint, string) {
uint location = bettingAddresses[account];
Punter storage info = bettings[location];
return (info.account,info.stake,info.supporting);
}
function getWinner() ended public payable {
if (oraclize_getPrice("URL") > this.balance) {
newOraclizeQuery("Oraclize query was NOT sent, please add some ETH to cover for the query fee");
} else {
newOraclizeQuery("Oraclize query was sent, standing by for the answer..");
bytes32 queryId = oraclize_query("URL", "json(https://www.api.ogunmoye.com).winner");
queryIds[queryId] = true;
}
}
function __callback(bytes32 myid, string result) public {
require(msg.sender == oraclize_cbAddress());
require(queryIds[myid] == true);
winner = result;
closed = true;
delete queryIds[myid];
FetchedResult(result);
BetClosed(block.timestamp, result);
}
function distributeStake() onlyOwner public payable returns (address){
calculateTotalPayable();
calculateIndividualRation();
for(uint i = 0; i < rations.length; ++i) {
Ration storage ration = rations[i];
uint winning = ration.percentage.div(100) * totalPayable;
winning = winning.div(10 ** 18);
payouts[ration.account] = winning;
address(ration.account).transfer(winning);
Payment(winning);
}
}
function calculateTotalPayable() internal {
require(bettings.length > 0);
for(uint i = 0; i < bettings.length; ++i) {
Punter storage profile = bettings[i];
//String cannot be compared directly
//Hash to do comparision
if(keccak256(profile.supporting) == keccak256(winner)) {
totalHolding += profile.stake;
rations.push(Ration({
account: profile.account,
percentage: profile.stake
}));
payoutAddresses[profile.account] = rations.length - 1;
}else{
totalPayable += profile.stake;
}
}
}
//Calculate each individual payout in percentage
//ratio of the total payout
function calculateIndividualRation() internal {
for(uint i = 0; i < rations.length; ++i) {
Ration storage ration = rations[i];
ration.percentage = getPercentage(ration.percentage);
}
}
//Calculate percentage and add precision to eliminate decimals
//which cannot be handled
function getPercentage(uint stake) internal view returns (uint) {
uint percentage = (stake.mul(precision) / totalHolding).mul(100);
return percentage;
}
function getAccountPercentage(address account) public view returns (uint percentage) {
uint location = payoutAddresses[account];
return rations[location].percentage;
}
function getBalance(address account) public view returns (uint balance) {
return (address(account).balance);
}
}
CODE HIGHLIGHTS
Notice the two import statements:
The first one imports the Oraclize API contract containing methods such as oraclize_getPrice
,oraclize_query
that we will use to interact with oracle and allows us to implement a __callback
method that will be called when oraclize returns result after a successfull request.
And SafeMath Library by OpenZeppelin to perform mathematical calculation, in this code the using SafeMath for uint
is used to allow the methods of SafeMath library to be called directly on integers.
import "./OraclizeAPI.sol";
import "./SafeMath.sol";
To use oraclize on testnet, launch your installed ganache GUI then from your terminal navigate to ethereum bridge
directory you cloned from here and run command node bridge -H localhost:7545 -a 0 --dev
You would be presented with a similar screen like below
Copy where it says Please add this line to your contract constructor
. Note do not use the one icluded in this snippet as it might not work for you.
/*
* @param start startTime
* @param end endTime
*/
function Game(uint start, uint end) public payable {
startTime = start;
endTime = end;
owner = msg.sender;
OAR = OraclizeAddrResolverI(0x6f485C8BF6fc43eA212E93BBF8ce046C7f1cb475);
}
The constructor initializes with the startTime
and endTime
of the game. Next lets take a look at the method to place bet.
/*
* @param team realmadrid, swansea
*/
function placeBet(string team) notStarted validContribution haveNoStake validTeam(team) public payable returns (uint) {
bettings.push(Punter({
account: msg.sender,
stake: msg.value,
supporting: team
}));
bettingAddresses[msg.sender] = bettings.length - 1;
Bet(msg.sender, msg.value);
}
The method takes only one argument which is one of the two teams we have in our contract. It also checks against the following modifier notStarted
, validContribution
, haveNoStake
validTeam
//Checks if team is one of the allowed teams
//TEAM_A = 'realmadrid'
//EAM_B = 'swansea'
modifier validTeam(string team) {
require(keccak256(TEAM_A) == keccak256(team) || keccak256(TEAM_B) == keccak256(team));
_;
}
//Check if the person placing the bet does not already
//have an existing bet
modifier haveNoStake() {
require(bettingAddresses[msg.sender] == 0);
_;
}
//Check if ether sent is not less than 0.1 ether
//or greater than 1 ether
modifier validContribution() {
require(msg.value >= MINIMUM_STAKE && msg.value <= MAXIMUM_STAKE);
_;
}
//Ensure bet will only be allowed only
//before match time
modifier notStarted() {
require(started() == false);
_;
}
Now comes the most interesting part:
Once the game is over you can end the game by calling method endGame
which can only be called by the owing account used to create this contract. Once the game is over the contract will allow owner to call the getWinner
method.
function getWinner() ended public payable {
//Check if there is enough balance in contract
if (oraclize_getPrice("URL") > this.balance) {
//If this contract does not have enough balance fire event to notify
newOraclizeQuery("Oraclize query was NOT sent, please add some ETH to cover for the query fee");
} else {
//Fire event to notify successfull call to endpoint
newOraclizeQuery("Oraclize query was sent, standing by for the answer..");
//Save queryId for later validation to ensure same query id was returned with response
bytes32 queryId = oraclize_query("URL", "json(https://www.api.ogunmoye.com).winner");
queryIds[queryId] = true;
}}
Once the request is successfull, Oraclize will return a response to the __callback
function
in our contract that was implemented from the OraclizeAPI.
function __callback(bytes32 myid, string result) public {
//Check if calling address is valid
require(msg.sender == oraclize_cbAddress());
//Validate if query id is the same
//we have
require(queryIds[myid] == true);
//Set winner as result
//and close the betting
winner = result;
closed = true;
delete queryIds[myid];
FetchedResult(result);
BetClosed(block.timestamp, result);
}
Finally we will look into the distributeSTake
method, this methods does three things:
First it calculates the total payable to the accounts that bet on the winner team.
function calculateTotalPayable() internal {
//We can only calculate payable if there are more
//than one bet
require(bettings.length > 0);
//Loop through all the bets
//Check if an account bet on winning team then
//add the account stake to totalHolding(Sum of all stake of winners)
//push the account info into an array of ratios
//to later calculate the ration of the winning to be payed out to it.
//
//Else sum the total of account that lost the bet as totalPayable
for(uint i = 0; i < bettings.length; ++i) {
Punter storage profile = bettings[i];
//String cannot be compared directly
//Hash to do comparision
if(keccak256(profile.supporting) == keccak256(winner)) {
totalHolding += profile.stake;
rations.push(Ration({
account: profile.account,
percentage: profile.stake
}));
//Store indexes of where account info are located
//for easy access using the address as key on a map
payoutAddresses[profile.account] = rations.length - 1;
}else{
totalPayable += profile.stake;
} }}
Now that the total amount payable have been calculated and we have stored the information of the winners. The method then calls the calculateIndividualRation
method to get the percentage of the total payout that should go to each account. using the formulae (stake/totalHolding) * 100
, where totalHolding
is sum of all stake of the account that bet on the winning team, allowing us to determine what ratio the account contributed to the total pool.
//Calculate each individual payout in percentage
//ratio of the total payout
function calculateIndividualRation() internal {
for(uint i = 0; i < rations.length; ++i) {
Ration storage ration = rations[i];
ration.percentage = getPercentage(ration.percentage);
}}
//Calculate percentage and add precision to eliminate decimals
//which cannot be handled
function getPercentage(uint stake) internal view returns (uint) {
uint percentage = (stake.mul(precision) / totalHolding).mul(100);
return percentage;
}
Note: One thing to note is that since solidity doesn't provide a way to store decimal values, a precision of 10e17 is used to multiply the percentage value since ether is 18 decimal place this will be sure to eliminate any occurence of decimal point and when payout is to be carried out the precision will be deducted and the value transfered in wei.
Now that we have the percentage of actual individual payout, the method then loop through the addresses and multpily the percentage with the total payout and divide it by the precision to determine the actual value in wei and finally make transfer.
for(uint i = 0; i < rations.length; ++i) {
Ration storage ration = rations[i];
uint winning = ration.percentage.div(100) * totalPayable;
winning = winning.div(10 ** 18);
payouts[ration.account] = winning;
address(ration.account).transfer(winning);
Payment(winning);
}
Testing Our Smart Contract
We would be testing the contract for three behavious :
- Test if a user can place a bet
- Test to check if there is a winner of the game
- Test to ensure contract successfully distribute the correct value to individual account.
Clone this project here, open your terminal and navigate into the project .
Ensure your ethereum bridge is running, copy and replace the OAR outputted in your terminal and replace the OAR in the contract constructor with yours.
Test if a user can place a bet
const Contract = artifacts.require("./Game.sol");
let moment = require("moment");
contract('Place Bet : ', async (accounts) => {
//Current timestamp
let now = moment().utc();
//Set start 10min from now & end time 1hr 40min from now
//considering a game is 90min let start = now.add({minutes : 10}).unix();
let end = now.add({minutes: 100}).unix();
it('a user can only place bet only if game is not started', async () => {
let game = await Contract.new(start,end);
let status = await game.started();
assert.isFalse(status);
});
it('two users can stake between 0.01 and 1 ether on any game ones', async () => {
let game = await Contract.new(start,end);
await game.placeBet('realmadrid',{value: web3.toWei(0.6, "ether"), from: accounts[1]});
let betInfoA = await game.getAccountInfo(accounts[1]);
assert.equal(betInfoA[0], accounts[1]);
assert.equal(betInfoA[1], web3.toWei(0.6, "ether"));
await game.placeBet('swansea',{value: web3.toWei(0.4, "ether"), from: accounts[2]});
let betInfoB = await game.getAccountInfo(accounts[2]);
assert.equal(betInfoB[0], accounts[2]);
assert.equal(betInfoB[1], web3.toWei(0.4, "ether"));
})});
run truffle test test/a_user_can_place_bet.js
Test to check if there is a winner of the game
const Contract = artifacts.require('./Game.sol');
let moment = require("moment");
contract('Check For Winner : ', async (accounts) => {
//Current timestamp
let now = moment().utc();
//Set start 10min from now & end time 1hr 40min from now
//considering a game is 90min let start = now.add({minutes : 10}).unix();
let end = now.add({minutes: 100}).unix();
it('game has no winner', async () => {
let game = await Contract.new(start,end);
let winner = await game.winner();
assert.equal(winner,"");
});
it('game has as a winner', async () => {
let game = await Contract.new(start,end);
await game.endGame();
await game.getWinner({value: web3.toWei(0.1, "ether")});
// Wait for the callback to be invoked by oraclize and the event to be emitted
const logWhenBetClosed = promisifyLogWatch(game.BetClosed({ fromBlock: 'latest' }));
let log = await logWhenBetClosed;
assert.equal(log.event, 'BetClosed', 'BetClosed not emitted');
assert.equal(log.args.result, 'swansea');
}).timeout(0);
/**
* @credit https://github.com/AdamJLemmon
* Helper to wait for log emission. * @param {Object} _event The event to wait for.
*/ function promisifyLogWatch(_event) {
return new Promise((resolve, reject) => {
_event.watch((error, log) => {
_event.stopWatching();
if (error !== null)
reject(error);
resolve(log);
}); }); }
});
run truffle test test/successfully_check_for_winner.js
This test makes a call to the game api that returns result, the etherum bridge console should have a log and look something like this.
Finally we would test to ensure contract successfully distribute the correct value to individual accounts.
const Contract = artifacts.require("./Game.sol");
let moment = require("moment");
contract('Place Bet : ', async (accounts) => {
//Current timestamp
let now = moment().utc();
//Set start 10min from now & end time 1hr 40min from now
//considering a game is 90min let start = now.add({minutes : 10}).unix();
let end = now.add({minutes: 100}).unix();
it('successfully transfer payout to winners', async () => {
let game = await Contract.new(start,end);
//Place bets
await game.placeBet('swansea',{value: web3.toWei(0.1, "ether"), from: accounts[1]});
await game.placeBet('swansea',{value: web3.toWei(0.2, "ether"), from: accounts[2]});
await game.placeBet('realmadrid',{value: web3.toWei(0.3, "ether"), from: accounts[3]});
await game.placeBet('realmadrid',{value: web3.toWei(0.4, "ether"), from: accounts[4]});
let prevAccBalance = await game.getBalance(accounts[2]);
//End game
await game.endGame();
//Retrieve winner from oracle
await game.getWinner({value: web3.toWei(0.4, "ether")});
// Wait for the callback to be invoked by oraclize and the event to be emitted
const logWhenBetClosed = promisifyLogWatch(game.BetClosed({ fromBlock: 'latest' }));
let log = await logWhenBetClosed;
assert.equal(log.event, 'BetClosed', 'BetClosed not emitted');
await game.distributeStake();
let conversion = 10 ** 18;
let totalPayable = await game.totalPayable();
let accountPayable = await game.payouts(accounts[2]);
let newAccBalance = await game.getBalance(accounts[2]);
let totalPayableInEther = totalPayable.toNumber() / conversion;
let accountPayableInEther = accountPayable.toNumber() / conversion;
let prevAccBalanceInEther = prevAccBalance.toNumber() / conversion;
let newAccBalanceInEther = newAccBalance.toNumber() / conversion;
//assert.equal(totalPayableInEther, 0.3); //0.3 Sum of stake lost to swansea punters
assert.equal(prevAccBalanceInEther + accountPayableInEther, newAccBalanceInEther);
}).timeout(0);
/**
* @credit https://github.com/AdamJLemmon
* Helper to wait for log emission. * @param {Object} _event The event to wait for.
*/ function promisifyLogWatch(_event) {
return new Promise((resolve, reject) => {
_event.watch((error, log) => {
_event.stopWatching();
if (error !== null)
reject(error);
resolve(log);
}); }); }});
run truffle test test/successfully_distribute_payout.js
Now we have a smart contract that passes all three crateria.
In future posts about this topic, we might considering launching it on a testnet like rinkeby and build an actual UI to place bet, interacting with it using web3 and placing bet with wallet like metamask.
Thank you for the contribution! It has been approved.
Need help? Write a ticket on https://support.utopian.io.
Chat with us on Discord.
[utopian-moderator]
Hey @alofe.oluwafemi I am @utopian-io. I have just upvoted you!
Achievements
Utopian Witness!
Participate on Discord. Lets GROW TOGETHER!
Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x
Your contribution cannot be approved because it does not follow the Utopian Rules.
You can contact us on Discord.
[utopian-moderator]
@laxam the linked repository has a license. Please find it here https://github.com/oraclize/ethereum-api/blob/master/oraclizeAPI_0.5.sol#L8-L28
@laxam the linked repository has a license. Please find it here https://github.com/oraclize/ethereum-api/blob/master/LICENSE
Nice article. Have a look at ethfiddle if you wanna share your Solidity snippets.
https://ethfiddle.com