Writing a Parimutuel Wager Contract
This post will demonstrate how to write a smart contract that implements parimutuel betting. The contract will accept and pay off bets on the outcome of a single proposition.
Parimutuel betting is a simple way of managing bets and paying off winners. For a given proposition, there are multiple mutually-exclusive outcomes to bet on, only one of which will win. For each possible outcome, a separate tally of bets is maintained. After determining the winning outcome, all of the wagered money is divided amongst those who bet correctly. The money awarded to a bettor is proportional to the fraction of the winning wagers that bettor represents. An example will help.
Suppose $1000 total is bet on the horses in a race, with $200 of that being bet on the winner. Furthermore, suppose that you bet $30 on the winner. Your bet represents 15% ($30/$200) of the winning bets, so you are entitled to 15% of all the money bet ($1000). Therefore, you would receive $150.
A Trusted “Oracle”
Unlike most of our smart contracts, this contract will require some off-blockchain information to pay off bets. Specifically, I am assuming that the bets are about off-chain events like political elections or sporting events.
One motivation for blockchains and smart contracts is distrust of non-chain entities. To resolve a bet about an external event, we must trust some off-chain entity to present the winning outcome. Such an entity is called an “oracle”.
Creating an independent, distributed, trusted oracle is beyond the scope of this post, so I’m going to just assume that bettors trust the contract’s owner to resolve bets correctly.
Parameterization
Bets are parameterized by a description of the proposition and a list of the possible outcomes. I use bytes32
rather than string
for each outcome due to a Solidity limitation. (Solidity does not support an array of strings as a parameter to the constructor.)
contract ParimutuelContract {
address public owner;
string public proposition;
bytes32[] public outcomes;
uint256 public timeout;
constructor(string _proposition, bytes32[] _outcomes, uint256 timeoutDelay)
public
{
owner = msg.sender;
proposition = _proposition;
outcomes = _outcomes;
timeout = now + timeoutDelay;
}
// more to come...
}
The proposition
and outcomes
values are purely descriptive. They are used by external entities to understand what proposition the contract represents. They are not used in the rest of the contract.
The timeout
is used to safeguard bettors from the possibility that the owner never resolves the proposition and bets are never paid off. It is used in cancel
below.
Bet Tracking
The contract can be in one of four states:
Open
: the state when bets are acceptedClosed
: the state after bets are accepted but before the winner is knownResolved
: the state after the winner is known during which bets are paid offCancelled
: the state of having been cancelled. Any unresolved bet can be cancelled.
enum States { Open, Closed, Resolved, Cancelled }
States state = States.Open;
Betting
Bet outcomes are indicated using the index into the outcomes
array corresponding to the desired outcome.
Tracking bets requires updating three running tallies:
betAmounts
tracks per-bettor bets on individual outcomes.totalPerOutcome
tracks the total amount bet on each specific outcome.total
tracks the total amount bet.
mapping(address => mapping(uint256 => uint256)) public betAmounts;
mapping(uint256 => uint256) public totalPerOutcome;
uint256 public total;
function bet(uint256 outcome) public payable {
require(state == States.Open);
betAmounts[msg.sender][outcome] += msg.value;
totalPerOutcome[outcome] += msg.value;
total += msg.value;
require(total < 2 ** 128); // avoid overflow possibility
}
Note that the code limits the total amount bet. It does so to avoid a potential overflow in the computation of bet payoffs. The limit is conservative.
Closing Betting
Only the owner can close betting, which is simply a change to the state
from Open
to Closed
.
function close() public {
require(state == States.Open);
require(msg.sender == owner);
state = States.Closed;
}
Resolving the Bet
The owner resolves the bet by indicating the winning outcome. This also changes the state
of the bet from Closed
to Resolved
. I could have allowed a direct transition from Open
to Resolved
, but this is simpler.
uint256 winningOutcome;
function resolve(uint256 _winningOutcome) public {
require(state == States.Closed);
require(msg.sender == owner);
winningOutcome = _winningOutcome;
state = States.Resolved;
}
Claiming Winnings
Once a winning outcome has been declared, claiming winnings is straightforward.
function claim() public {
require(state == States.Resolved);
uint256 amount = betAmounts[msg.sender][winningOutcome] * total
/ totalPerOutcome[winningOutcome];
betAmounts[msg.sender][winningOutcome] = 0;
msg.sender.transfer(amount);
}
Note that we can be certain that the multiplication cannot overflow because of the limit on total
enforced in bet
.
Cancelling the Bet
An unresolved bet can be cancelled under either of two conditions:
- The owner can cancel an unresolved bet at any time.
- Anybody can cancel an unresolved bet after the timeout.
function cancel() public {
require(state != States.Resolved);
require(msg.sender == owner || now > timeout);
state = States.Cancelled;
}
Bettors must be able to reclaim bets made prior to cancellation. To keep things simple, the contract does not keep track of the total amount bet by each bettor. Therefore, refunds must be requested on a per-outcome basis.
function refund(uint256 outcome) public {
require(state == States.Cancelled);
uint256 amount = betAmounts[msg.sender][outcome];
betAmounts[msg.sender][outcome] = 0;
msg.sender.transfer(amount);
}
Incentives
I ignored the fact that gambling establishments typically keep a portion of the bets for themselves. Such a vig would not introduce significant complexity to this contract. The claim
function would need to scale down payments, and the remainder would need to be paid to the owner.
While a vig-less contract may seem desirable, it has one significant drawback: the owner has no tangible incentive to actually resolve the proposition. Compensating the owner only after they resolve the proposition would give them incentive to do so. It would also give them incentive to resolve the proposition truthfully. Someone who regularly creates propositions needs to maintain an honest reputation to encourage future participation.
Summary
- The smart contract supports parimutuel betting.
- The contract goes through multiple states, which are tracked explicitly with
enum
values. - The contract relies on its owner to transition between states and to resolve the proposition.
- The contract employs a timeout to guard against a perpetually unresolved proposition.