paint-brush
Smart Contract Tutorial for Beginners — Lottery dAPP [Part 1]by@api3
3,598 reads
3,598 reads

Smart Contract Tutorial for Beginners — Lottery dAPP [Part 1]

by API3October 4th, 2022
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

In this tutorial we’ll be walking through building and deploying a decentralized lottery smart contract in Solidity using [Hardhat]. Anyone can choose a number 1–10,000 and buy a ticket to enter into a weekly lottery. Ticket revenue is collected into a pot in the contract. After 7 days, the contract will allow anyone to trigger the drawing. The contract will then call the [API3 QRNG[https://api3.org/QRNG] for a truly random number. The pot will be split amongst all users that chose this winning number.

Company Mentioned

Mention Thumbnail
featured image - Smart Contract Tutorial for Beginners — Lottery dAPP [Part 1]
API3 HackerNoon profile picture


By Camron Haider - API3 Core Team

Twitter: @CamBrazy3


In this tutorial, we’ll be walking through building and deploying a decentralized lottery smart contract in Solidity using Hardhat.


In this example use case, anyone can choose a number 1–10,000 and buy a ticket to enter into a weekly lottery. The ticket revenue is collected into a pot in the contract. After 7 days, the contract will allow anyone to trigger the drawing. The contract will then call the API3 QRNG for a truly random number generated by quantum mechanics. The pot will be split amongst all users that chose this winning number. If there are no winners, the pot will be rolled over to the next week. Once deployed, the lottery will continue to run and operate itself automatically without any controlling parties!


By the end of this tutorial you should be able to:


Deploy a decentralized lottery smart contract to the Goerli testnet that uses Quantum Randomness.


Who is this tutorial for?


Developers with a basic understanding of the Solidity and Javascript languages that would like to expand their knowledge of building with smart contracts using oracles.


In Part 1, we create a centralized lottery smart contract. In Part 2, we decentralize our lottery by integrating the API3 QRNG.

Setup

Create a folder and open it up in your preferred IDE. We prefer Visual Studio Code with the Hardhat extension.

1. Initialize a Node.js project

Create a folder for your project and open it in VSCode. In a terminal, initialize a project by running the following command:

npm init -y

2. Install Hardhat

Hardhat is a npm library with a built-in local Ethereum node that allows you to develop smart contracts. Because it will only be used for development purposes, we can install it as a development dependency:

npm install -D hardhat

3. Initialize the Hardhat project

We’ll use the Hardhat CLI to create a boilerplate Web3 project:

npx hardhat


Follow the prompts to Create a JavaScript project and choose the default options for the rest.


When the CLI is done creating the project, you should see a few new files and directories inside of your project. Boilerplate contracts are located in the contracts folder. Tests for that contract are located in the tests folder. We’ll be deleting these files in the next steps so now would be a good time to look through them.


Run the test command to see the boilerplate contract in action.

npx hardhat test

When we run npx hardhat test, Hardhat spins up a local node and tests against it before shutting down. This makes it fast and free to execute our contracts.

Writing the Smart Contract


Writing the Smart Contract

The complete contract code can be found in the Part1 branch

1. In the contracts folder, delete the Lock.sol file and create a file named Lottery.sol

2. Set the solidity version, and start with an empty contract

pragma solidity ^0.8.9;
contract Lottery {}

3. Add global variables to the contract

contract Lottery {
    uint256 public pot = 0; // total amount of ether in the pot
    uint256 public ticketPrice = 0.0001 ether; // price of a ticket
    uint256 public week = 1; // current week counter
    uint256 public endTime; // unix datetime lottery is closable
    uint256 public constant MAX_NUMBER = 10000; // max guess
}

4. Underneath the global variables, add our error handling

error EndTimeReached(uint256 lotteryEndTime);

Underneath the errors, add the mappings for tickets and winning numbers

The tickets mapping stores the list of addresses that guessed a specified number during a specified week. The winningNumber mapping stores each weeks winning number.

mapping(uint256 => mapping(uint256 => address[])) public tickets;
mapping(uint256 => uint256) public winningNumber;


Mappings can be accessed throughout our code by using winningNumber[weekNumber] for example.

6. Underneath the mappings, add the constructor function

When deploying the contract, we’ll need to pass in a unix timestamp of when the lottery will end. We store the _endTime value in our endTime global variable from step 3.

constructor(uint256 _endTime) {
endTime = _endTime;
}

7. Underneath the constructor function, add a function to buy a ticket

Users can call this function with a number 1–10,000 and a value of 0.001 ether to buy a lottery ticket.

function enter(uint256 _number) external payable {
    require(_number <= MAX_NUMBER, "Number must be 1-MAX_NUMBER");
    if (block.timestamp >= endTime) revert EndTimeReached(endTime);
    require(msg.value == ticketPrice, "Price is 0.0001 ether");
    tickets[week][_number].push(msg.sender);
    pot += ticketPrice; 
}


We make the function external so that it can only be called from an external user. We also make it payable because users will have to send funds to purchase the ticket.


On line 2 we add a [require](https://docs.soliditylang.org/en/v0.4.24/control-structures.html#error-handling-assert-require-revert-and-exceptions) statement to prevent users passing in a guess higher than the maximum allowed. Line 3 we throw an error in the case that the current time has passed the endTime. Line 4 we ensure the ticket price was sent in the transaction.


On line 5 we add the user’s address (msg.sender) to our tickets mapping from step 5, then add the revenue from the ticket sales to the pot.

8. Create a function to mock picking the winners

Before we decentralize our lottery, let’s mock the random number generation so that we can test the contract’s functionality. We’ll be decentralizing this function in Part 2 of this tutorial by using the API3 QRNG.

function closeWeek(uint256 _randomNumber) external {
    require(block.timestamp > endTime, "Lottery has not ended"); 
    winningNumber[week] = _randomNumber;
    address[] memory winners = tickets[week][_randomNumber];
    week++;
    endTime += 7 days;
    if (winners.length > 0) {
        uint256 earnings = pot / winners.length; 
        pot = 0; 
        for (uint256 i = 0; i < winners.length; i++) {
            payable(winners[i]).call{value: earnings}("");
        }
    }
}


Since we’re just mocking randomness in this step, we’ll make our function take a _randomNumber argument and make it external. In line 3 we store the “random number” in our winningNumber mapping. In line 4 we get all of the addresses that chose the winning number and store them in memory instead of storage since we won’t be changing any of the data, just referencing it. Next, we increment the week counter and endTime.


Then, unless there are no winners this week, divide the pot amongst the winners. Line 9 we set the pot back to 0 ether before we pay out winners on line 11.

9. Create read-only function

This function will return the list of addresses that chose the given number for the given week. This will make our lives easier during the testing steps.

function getEntriesForNumber(uint256 _number, uint256 _week) 
public view returns (address[] memory) {
    return tickets[_week][_number];
}

We mark the function as public so that it can be used externally and internally if necessary. We also mark it as view because no data should be changed when calling this function.

10. Create receive function

The receive function will be called if funds are sent to the contract. In this case, we need to add these funds to the pot.

receive() external payable {
pot += msg.value;
}

Testing the contract

If our contract is used in production, users’ real funds will be at stake. That makes thorough testing extremely important in smart contract development.


If you haven’t worked with unit tests before, I recommend you learn the basics. We’ll mainly be interacting with our contract through testing and scripts.

1. In the test folder, delete the Lock.js file and create a file called Lottery.js

2. Import npm libraries

const { expect } = require("chai");
const { ethers } = require("hardhat");

We’ll be using the ethers library inside of Hardhat which includes some extra capabilities.

3. Add tests

We’ll start with a simple deployment test to be sure that the contract is deploying correctly.

describe("Lottery", function () {
  let lotteryContract, accounts, nextWeek;
  it("Deploys", async function () {
      const Lottery = await ethers.getContractFactory("Lottery");
      accounts = await ethers.getSigners();
      nextWeek = Math.floor(Date.now() / 1000) + 604800;
      lotteryContract = await Lottery.deploy(nextWeek);
      expect(await lotteryContract.deployed()).to.be.ok;
  });
});


On line 2, we create empty global variables to store a few values that we’ll be using in multiple tests. In our Deploys test, we’ll get our Lottery contract factory that we’ll use to deploy new instances of our contract. We get all of our signers, or wallets that are connected to Hardhat, and store them globally as accounts. We also store nextWeek globally because we’ll use it later.


On line 7 we deploy the contract using .deploy() from our contract factory. Our constructor takes in 1 argument, endTime, which we’ll pass into .deploy().


We use expect().to.be.okay to validate the boolean returned from .deployed() is truthy.


We can use npx hardhat test to run the test.


Let’s add a few more tests but feel free to add any/all of the relevant tests from the completed test file.

describe("Lottery", function () {
    let lotteryContract, accounts, nextWeek;
  
    it("Deploys", async function () {
        const Lottery = await ethers.getContractFactory("Lottery");
        accounts = await ethers.getSigners();
        nextWeek = Math.floor(Date.now() / 1000) + 604800;
        lotteryContract = await Lottery.deploy(nextWeek);
        expect(await lotteryContract.deployed()).to.be.ok;
    });
  
    it("Users enter between 1-3", async function () {
        for (let account of accounts) {
            let randomNumber = Math.floor(Math.random() * 3);
            await lotteryContract
                .connect(account)
                .enter(randomNumber, { value: ethers.utils.parseEther("0.0001") });
            const entries = await lotteryContract.getEntriesForNumber(randomNumber, 1);
            expect(entries).to.include(account.address);
        }
    });
  
    it("Choose winners", async function () {
      const winningNumber = 2;
      // Move hre 1 week in the future
      let endTime = await lotteryContract.endTime();
      await ethers.provider.send("evm_mine", [Number(endTime)]);
      
      const winners = await lotteryContract.getEntriesForNumber(winningNumber, 1);
      let balanceBefore = await ethers.provider.getBalance(winners[0]);
      await lotteryContract.closeWeek(winningNumber);
      const balanceAfter = await ethers.provider.getBalance(winners[0]);
      expect(balanceAfter.gt(balanceBefore)).to.be.true;
    });
});


Run npx hardhat test to try it out.

Conclusion

All of the completed code for Part 1 can be found in the Part 1 Branch of the repo.

In Part 1 of this tutorial we learned how to build and test a lottery smart contract using Hardhat. The problem is, our closeWeek function is not secure, as it is public and can be called by anyone accessing the smart contract after the week's lottery ends. In its current state, anyone could enter the lottery and then pass their number into the closeWeek function to steal the pot. Because a decentralized online gambling application is only as feasible as the degree to which it is fair, secure, and unexploitable, it requires a source of random number generation that is unbiased and tamper-proof.


In Part 2, we’ll be decentralizing our lottery contract and addressing the security concerns using the API3 QRNG. Any participant will still be able to call the closeWeek function, but will not be able to provide a winning number. Instead, our contract will call the API3 QRNG to generate a truly random number that will be used to determine the winner(s). Once deployed, the lottery will continue to run and operate itself automatically without any controlling parties!


Also Published Here