Prediction Market
Building a prediction market that uses the UMA Optimistic Oracle V3 for settlement and event identification
This section covers the Prediction Market contract, which is available in the Optimistic Oracle V3 quick-start repo and enables the creation of a binary prediction market using Optimistic Oracle V3 (OOV3) assertions.
Prediction Market
A prediction market smart contract allows individuals to make predictions and place bets on the likelihood of different outcomes for a future event. These types of contracts can be used for any kind of events such as:
Sports games
Cryptocurrency price predictions
Product launches
Political policy decisions
This smart contract enables the creation of prediction markets based on any off-chain event with three possible outcomes, two of which are chosen by the market creator and the third reflecting the remaining outcomes (i.e. tie or a draw in a sporting event).
By participating in the prediction market, individuals can create outcome tokens (shares) and buy and sell them of the different outcomes based on their assessment of the likelihood of each outcome occurring. The market determines the price of each share based on supply and demand, and individuals can profit by purchasing shares whose value grows as the likelihood of a certain event increases. Nevertheless, this contract does not cover the selling of outcome tokens; only their creation is covered.
Development environment
This project uses forge as the Ethereum testing framework. You will also need to install Foundry, refer to Foundry installation documentation if you don’t have it already.
You will also need git
for cloning the repository, as well as bash
shell and jq
tool in order to parse transaction outputs when interacting with deployed contracts.
Clone the UMA Optimistic Oracle V3 quick-start repository and install the dependencies:
Contract implementation
The contract discussed in this tutorial can be found at dev-quickstart-oov3/src/PredictionMarket.sol
(here) within the repo.
Contract creation and initialization
To initialize the state variables of the contract, the constructor takes three parameters:
_finder
: address that represents the Finder contract, which is used to locate and retrieve other contracts within the system._currency
: address that represents the currency that will be used for trading in the markets created by this contract._optimisticOracleV3
: address that represents the OptimisticOracleV3, contract to assert truths about the world which are verified using an optimistic escalation game.
Market creation
The initializeMarket()
function is used to create a new market in the Prediction Market contract.
Once the contract has been deployed, anyone can call initializeMarket()
after approving the optional reward
amount to be paid to the wallet that runs the assertion.
To create a new market, the initializeMarket()
function is called with the following parameters:
outcome1
: A short name for the first outcome (i.e., "yes").outcome2
: A short name for the second outcome (i.e., "no").description
: A description of the market and the event being predicted.reward
: The amount of currency (in Wei) that will be available as a reward for the user that runs that creates the assertion in the OOV3, necessary to settle the market once it's ready.requiredBond
: The amount of currency (in Wei) that users must bond to assert the outcome of the event.
Once the input checks are passed, the function creates two new ERC20 tokens (one for each outcome) using the ExpandedERC20
contract, which extends the standard ERC20 contract by adding the ability to mint and burn tokens. The Prediction Market is assigned the minter and burner roles to simplify how the outcome tokens handled when creating, redeeming or settling the tokens.
The Market
is then created and stored internally associated to a marketId
returned by the initializeMarket()
and emitted in the MarketInitialized
event at the end of the process together with the parameters defining the market.
Create outcome tokens
The createOutcomeTokens
function mints a pair of tokens representing the value of outcome1 and outcome2 for a given market identified by marketId
. This allows participants to trade on the outcome of the market by exchanging these tokens with each other outside of the scope of the Prediction Market contract.
The createOutcomeTokens
function takes two parameters:
tokensToCreate
: an unsigned integer (uint256
) value representing the number of tokens to be created for each outcome. The total number of tokens created will be twice this value, as two outcome tokens are created for each market.marketId
: abytes32
value representing the unique identifier of the market for which the outcome tokens are being created.
The function first checks if the market exists. If the market exists, it transfers the specified amount of currency tokens from the caller to the contract using safeTransferFrom
function from currency
contract instance.
Then, the function mints tokensToCreate
amount of both outcome1 and outcome2 tokens to the caller using the mint
function of the respective ExpandedERC20
token instances. Finally, it emits an event to notify that tokens have been created.
It is important to note that before calling this function, the caller must approve the contract to spend the required amount of currency tokens by calling the approve
function on the currency
contract instance.
Redeem outcome tokens
The redeemOutcomeTokens
function burns an equal amount of outcome1Token
and outcome2Token
tokens for a given market and transfers the corresponding settlement currency tokens to the caller's account.
The function takes two parameters:
marketId
: abytes32
value representing the unique identifier of the market for which the tokens are being redeemed.tokensToRedeem
: an unsigned integer (uint256
) value representing the number of tokens of each outcome to be redeemed. The total number of tokens redeemed will be twice this value, as two outcome tokens are burned for each market.
Both parameters are passed to the function as arguments when it is called.
The function first retrieves the Market data from the storage using the provided marketId. Then, it checks if the outcome1Token is not equal to the zero address, which indicates the market exists.
Next, it burns the specified number of tokens for both outcome1Token and outcome2Token tokens using the burnFrom()
function of their respective contracts, which decreases the balance of the caller's account.
Finally, it transfers the specified number of settlement currency tokens from the contract's account to the caller's account using the safeTransfer()
function. The function emits a TokensRedeemed
event, which includes the marketId, the caller's address, and the number of tokens redeemed.
Settle outcome tokens
The settleOutcomeTokens
function allows a user to settle their outcome tokens for a given market and receive a payout in the settlement currency. The payout depends on the resolved market outcome and the number of tokens burned for each outcome.
The function takes one parameter:
marketId
: abytes32
value representing the unique identifier of the market for which the outcome tokens are being settled.
The marketId
parameter is passed to the function as an argument when it is called.
The function first retrieves the Market data from the storage using the provided marketId
. It then checks if the market has been resolved by verifying that the resolved
flag in the market struct is set to true
. If the market has not been resolved, the function throws an error.
Next, the function determines how many outcome1Token
and outcome2Token
tokens the caller has by calling the balanceOf()
function of their respective contracts. The balances are stored in outcome1Balance
and outcome2Balance
variables.
After that, the function calculates the payout based on the resolved market outcome and the number of tokens burned for each outcome. If the market was resolved to the first outcome, then the payout equals the balance of outcome1Token
while outcome2Token
provides nothing. If the market was resolved to the second outcome, then the payout equals the balance of outcome2Token
while outcome1Token
provides nothing. If the market was resolved to the split outcome, then both outcome tokens provide half of their balance as currency payout.
Finally, the function burns the caller's outcome tokens using the burnFrom()
function of their respective contracts, transfers the calculated payout from the contract's account to the caller's account using the safeTransfer()
function, and emits a TokensSettled
event, which includes the marketId, the caller's address, the payout amount, and the number of outcome1Token
and outcome2Token
tokens burned. The calculated payout is returned by the function.
Assert market
The function assertMarket
is used to assert a market with any of the three possible outcomes: outcome1, outcome2, or unresolvable. The function takes two arguments:
marketId
, which is a unique identifier for the marketassertedOutcome
, which is a string representing the asserted outcome.
The function first checks that the market exists by verifying that the outcome1 token is not a null address. It then computes the hash of the asserted outcome using the keccak256
function and checks that the market does not already have an active or resolved assertion. It also checks that the asserted outcome is one of the three allowed outcomes.
If all checks pass, the function sets the assertedOutcomeId
of the market to the hash of the asserted outcome and computes the bond required to make the assertion. If the bond required by the market is higher than the minimum bond required by the oracle, the market bond is used instead. The function then composes a claim with the asserted outcome and the market description, pulls the bond from the caller's account, approves it for the oracle, and makes the assertion using the _assertTruthWithDefaults
function.
Finally, the function stores information about the asserted market and emits a MarketAsserted
event with the market ID, the asserted outcome, and the assertion ID.
Tests and deployment
To run the contracts tests written in solidity with forge run the following command:
Deployment
Run a local node with anvil (included in foundry toolkit) in a console:
In a second console continue running the rest of commands. Let's first export the environment variables for the wallets to use:
Then Deploy the UMA Oracle Sandbox contracts to be used by running:
Find in the logs the FINDER_ADDRESS
and export it with:
Then use the Finder to export rest of addresses by running the following commands:
We are ready to deploy the Prediction Market contract with the following command:
Interacting with deployed contract
It's time to initialise a market. We can first export the market parameters to use.
Here the description shows that it's a market based on the outcome of a match between two teams. The two possible outcomes are yes
or no.
We offer 100 units of DEFAULT_CURRENCY
to the asserter of the claim and require a bond of 5,000 units of DEFAULT_CURRENCY
to assert or dispute the assertion.
We need to mint the amount of asserter rewards and approve them before creating the market:
Then we are ready to initialise the market with the DEPLOYER_WALLET
Then we can mint the necessary tokens to then create the outcome tokens:
We can now create the outcome tokens. With an amount 10,000 units of DEFAULT_CURRENCY
we get 10,000 OUTCOME_TOKEN_ONE
and 10,000 OUTCOME_TOKEN_TWO
tokens:
At any point before the market is settled we can redeem outcome tokens. By redeeming an amount we are burning the same amount of OUTCOME_TOKEN_ONE
and OUTCOME_TOKEN_TWO
to receive that amount of DEFAULT_CURRENCY
:
After redeeming 5,000 tokens we can see how both balances of OUTCOME_TOKEN_ONE
and OUTCOME_TOKEN_TWO
have decreased by 5,000 and DEFAULT_CURRENCY
has increased that same amount.
Now, let's simulate how the DEPLOYER_WALLET
would trade one position of the market by transferring the remaining 5,000 OUTCOME_TOKEN_ONE
to another user. By doing this, DEPLOYER_WALLET
is now only exposed to the outcome two ("no") because he only holds OUTCOME_TOKEN_TWO
. On the other side, USER_WALLET
is exposed to the outcome one ("yes") as he has traded some other currency against OUTCOME_TOKEN_ONE
. This trade is out of the scope of this example, thats why we simulate it by running the following transfer:
At this point, let's imagine that the match between The Glacial Storms and the Electric Titans has taken place and that The Glacial Storms won. Then anyone can now assert
that this has occurred by calling assertMarket
with outcome "yes"
as the claim defined in DESCRIPTION
is true. We can do it, from the ASSERTER_WALLET
, by running the following command:
Now, let's move forward 2 hours to go pass the challenge window of the assertion:
Now the assertion can be settled in the OptimisticOracleV3
. We can do it by running the following command:
We can now check how the ASSERTER_WALLET
has received back the assertion bond plus the reward:
Now, both the DEPLOYER_WALLET
and USER_WALLET
can settle their outcome tokens:
Finally we can see how the USER_WALLET
won the bet, as he got OUTCOME_TOKEN_ONE
so he now has 5,000 DEFAULT_CURRENCY
and the deployer wallet only has 5,000 DEFAULT_CURRENCY
from his initial 10,000:
Last updated