Building a Prediction Market DApp with Pyth Oracle & Morph starter kit
Ah, prediction markets. The current rave. From predicting election outcomes to forecasting crypto prices, these decentralized prediction platforms have captured the imagination of both traders and developers. Today, I will walk you through building your own prediction market dApp using Pyth Oracle.
Oracles serve as bridges between the blockchain and external data sources, fetching and delivering crucial information to your smart contracts. They allow your dApp to react to real-world events and make decisions based on accurate, timely data.
Pyth Oracle
The Pyth Network is a first-party financial oracle network designed to publish continuous real-world data on-chain in a tamper-resistant, decentralized, and self-sustainable environment. It connects market data owners to applications across multiple blockchains, with data contributed by over 100 first-party publishers, including major exchanges and market-making firms.
Pyth supports over 200 price feeds, including crypto, equities, FX, and metals, updating twice per second without access restrictions. The network has secured over $2.0B in total value and supports significant trading volumes, making it a trusted oracle for over 350 protocols on 55+ blockchains. For more details, visit Pyth Network.
What are we building?
If you are following so far you must have already gleaned it. We are building a prediction market dApp that allows us to predict the prices of any crypto token, using solidity(foundry), Pyth oracle for fetching prices, Typescript and Nextjs for our frontend.
Features
Our prediction market dApp will allow us to do the following,
- Create markets: This could be any crypto, equity, fx or even commodity
- Buy shares: Users will be able to buy shares for whatever their prediction is(Yes or No).
- Resolve markets: Once the epoch elapses, the dApp resolves the market by using pyth oracle to determine the outcome and winners.
- Claim : winners will be able to claim their winnings from making correct predictions.
Let's get to building!
Setting up our environment
We are going to start by using the Morph starter kit. This starter kit was designed to abstract away any complexity involved in setting up your environment to build and deploy on morph.
So in your terminal, run the command below and choose the name of your dapp or hit Enter to select the default.
After its successfully installed, you should see some setup options.
Great! Now we have our kit installed, we can finally start building.
We are going to be using foundry to write and deploy our smart contracts. So navigate to the contracts/foundry directory and run the following commands to rename the env.example file to env and also, to compile our boilerplate contracts.
.env
Before we start building, we have to set our environment variables. Paste in your private key in the .env file in the root of your project.
Token.sol
Now, we can finally start writing our smart contracts. In your src folder, delete the boilerplate file (MyToken.sol) and create a new file called Token.sol. This will serve as the currency for our prediction dApp. Paste in the code below
Market.sol
The market contract is the implementation of the prediction market dApp. Still in your src folder, create another file called Market.sol and paste in the code in this gist. Now, you might see some errors regarding the Pyth imports. This is because we have not installed Pyth yet in our project.
In your terminal(enure you are in the foundry directory) , run the command below to install Pyth to our foundry project
Next, we add Pyth to our remappings in the foundry.toml file. Paste this line in the remappings array in your foundry.toml file.
Your foundry.toml file should look like this
Also, the warnings in the market contract should have disappeared and your Market.sol should look like below.
Contract Walkthrough
Key variables
createMarket function
- Creates a new prediction market
- Validates that timestamps are correct (end time > current time, resolution time > end time)
- Emits a MarketCreated event
- Increments the market counter
buyShares function
- Allows users to buy position shares in a market
- _isYes: true for "above strike price", false for "below strike price"
- Takes 1% fee from the input amount
- Updates the user's position and market's total shares
- Transfers payment tokens from user to contract
- Protected against reentrancy
sellShares function
- Allows users to sell their position shares
- Verifies user has sufficient shares to sell
- Takes 1% fee from the sale amount
- Updates positions and transfers tokens back to user
resolveMarket function
- Called after market end time to determine the outcome
- Uses Pyth oracle to get the final price
- Converts price to 18 decimals for consistent comparison
- Sets market outcome (true if price >= strike price)
- Marks market as resolved
- Requires payment for Pyth oracle update
claimRewards function
- Allows winners to claim their rewards after market resolution
- Calculates reward based on share of winning side
- Transfers rewards to user
- Resets user's position to prevent double claims
Deploying our contract
Deploying our contract is super easy with our development kit. Update your run function in your script/Deployer.s.sol file.
This function deploys the token( ERC20 token which we are using as our currency) and passes the address, alongside the address of the Pyth oracle deployment on Morph, as arguments to the constructor in our prediction market contract.Pyth morph address:
Your deployment script should be similar to what's below
Next, we run the commands to deploy our contract on morph
Setting up the frontend
.env
The first thing we are going to do is to create a walletconnect id and add it to our env file (rename .env.example to .env).
Next, run yarn install to install dependencies and yarn dev to start our dev server.
page.tsx
This is our home page. Replace the default code with the one below
Also, this is the best time to install the Pyth oracle dependency
Markets.tsx
This component renders all markets created. In your components folder, create a Markets.tsx file and paste in the code from this gist. We are going to go over some selected functions.
Market creation:
In this function, we use the writeContractAsync hook from wagmi to create a new market by calling the “createMarket” function on our contract and passing in the the crypto pair, strike price, end time, resolution time and price id (gotten from pyth. Each crypto pair has a unique price id).
Getting approvals
In this function, we useReadContract hook from wagmi to call the allowance function on the token contract to check if the market contract has sufficient allowance. Next, we call the approve function to approve the market contract to spend money from our wallet.
Market.sol
The next component we are going to work on is the Market component. In your components folder, create another file called Market.sol and paste in the code from this gist.
Buying shares
The handleBuy function calls the “buyShares” function in the contract and takes in the id of the market, user position (yes/no) and amount.
Resolving markets
This function first creates a connection to Pyth’s price service. hermes.pyth.network is Pyth’s endpoint for getting price updates. It provides real-time price data.
After making the connection, it gets the price feed update data. This is the latest price data which includes the latest price, confidence interval, timestamp and publisher signature.
Finally, it calls the “resolveMarket” function in the contract passing in the id of the market and the update data while also paying for the data. Also worthy to note that we are passing an arbitrary amount(0.001 eth) only for demonstration purposes. Ideally, you can call the getUpdateFee method to get the exact fee for each price update.
So to put the flow of data more explicitly,
Connecting to the contract
After we are done with the UI (and i know its a load of errors in your browser but here comes the magic!), it's time to connect to the smart contract. First, we navigate to the constants folder and create two files marketAbi.ts and tokenAbi.ts.
This is where we are going to paste in the ABIs from our contracts. To get the ABI, go to contracts/foundry and locate the out folder. In the folder, look for the name of our contracts (market and token respectively) and copy the json file.
Back in our constants folder, paste the json file into the respective files (marketAbi.ts and tokenAbi.ts). Also ensure you paste only the ABI array and not include the metadata. For ease and as an example, here are my marketAbi.ts and tokenAbi.ts files.
Finally, in the index file in our constants folder, we modify it to export our ABIs and also the addresses of our market and token contracts.
Testing it all out
Time for the fun part. Fiest lets run “yarn dev” to see that we got nothing wrong along the way. Your screen should look like mine below
Next, we connect our wallet (sometimes this could happen automatically) and get this beautiful modal courtesy of web3modal which comes pre-configured with the kit (isnt that cool? Modals, wagmi, viem etc all come already setup with the kit).
After, connecting our wallet, we should get this default screen.
Finally, we create a market. This market will be focused on the ETH/USD crypto pair and our strike price will be $2700. Our duration will be really short for demonstration purposes.
After creating a market, users will be able to take either yes or no positions. But in order to do so, they have to acquire shares(stake). So first, we call the approve function to allow the Market contract transfer our stake from our wallet. Notice how after granting approval, the approve button disappears.
After granting approvals, next is to buy shares. Note, we take a one percent fee on whatever position the user takes (refer to the buyShares function in our contract).
Once the epoch of our market elapses, a new button pops up. The button allows us to “resolve market”. This means it calls the “handleResolveMarket” function which connects and queries Pyth oracle for the exact price of our crypto pair as at the resolution time. This function determines the winners and losers in the market.
After resolving the market, go ahead and claim your rewards. Watch your token balance to see the rewards come in.
Conclusion
In this guide, we have walked through building a full stack prediction market dApp. From setting up our environment using the morph starter kit to deploying our smart contract and then interacting with it from the frontend.
I have also left a small challenge (as I would be doing so from now on) called fixed the bug. There is an intentional bug in our dApp. After claiming your rewards, even though the balance updates and the funds is sent to the winner, it doesnt reset the yes/no shares amount, to zero.Two ways to fix this. Either from the resolveMarket function in the contract, or modify the UI to indicate the amount has been claimed and market closed.
Send links to your solutions to discord and tag me (ernestnnamdi) and i’ll definitely respond. See you again soon.