Click here to Skip to main content
15,861,125 members
Articles / Security / Blockchain

Etherdrop: DAPP Lottery on Ethereum Blockchain

Rate me:
Please Sign up or sign in to vote.
4.82/5 (3 votes)
10 Aug 2018CPOL9 min read 14.9K   198   8   1
Your Ethereum Giveaway Smart Contract Lottery

Introduction

Etherdrop.app

Of course, we have all heard about 'Bitcoin' and the famous blockchain technology brought by Satoshi Nakamoto.

While Bitcoin is the first form of the real internet of money, Ethereum is the first form of 'the internet of decentralized computing trustless platform'!

The Blockchain technology allowed us to communicate over a network of trust, where there is a consensus enforced by cryptography, algorithm and power of the nodes / miners.

I won't go a lot into explaining bitcoin, ethereum or other form of cryptocurrencies. [Thanks to Google].

A well said sentence by the creator genius of Ethereum:

So well, Ethereum is about a decentralized computing platform that allows users to interact with others,
using a form of digital contract, the 'Smart Contract' that enforces specific behaviors between parties
given a decentralized contract well defined on the blockchain and immutable behavior once deployed there!

On Ethereum, we have 2 types of accounts (addresses):

  1. Users and Parties will have their 'user' accounts (wallet addresses) holding the ETH ether balance.
  2. Smart Contracts ('the immutable transparent behavior programs') address holding ETH and additional binary (EVM instruction - Contract OPCODE) structure and data (the contract state variables).

Background

Let us take an example of a 'Smart Contract' of a Lottery Entertainment Service:

  1. A number of people who will pay a ticket to subscribe
  2. Wait till the round closes
  3. A winner will be randomly picked from the pool!

You can see the Etherdrop live on etherdrop.app.

The old classic way is that you will never have access to centralized authority (server) behind the service to know or verify how this random user is picked, and what is happening behind the scenes, even in the presence of a human monitoring / regulations.

In this article, you will learn how to:

  1. Develop The Lottery Service Logic [Smart Contract] using Solidity language
  2. Deploy The SmartContract using NodeJs and Truffle on the Local Blockchain (Ganache) / or Mainnet Ethereum Blockchain
  3. Develop the Web
    FrontEnd - UI - Materialized, JS, Jquery, Web3Js, TruffleJs
    that supports Web3 (the next web generation) to interact with Ethereum Blockchain
  4. Develop the Web Backend - NodeJS
  5. Analyse and read the Blockchain Data On Etherscan

Project Structure

Using the Code

We will go section by section [ A . B . C . D . E ]

[ A ]

  • Starting our Smart Contract Development, the SmartContract is a kind of a class (in Java, C#, OOP) holding state variables (e.g., global variables) as well as functions.
  • Please note that any execution of a smart contract function requires an onchain transaction from a user account address to the smart contract address pointing to the specific function (with its data arguments)
  • A transaction to be executed and confirmed needs to be added to a block, which should be added to the blockchain (confirmed), e.g., 3 confirmations means 3 blocks after the transaction's block are added to the blockchain and so on (a block contains many transactions, given the block size...)
  • While reading or calling a passive function (view) that does not change state variables or contract data is free and near instant.
  • The miner (or node) that will pick your transaction will broadcast it for other nodes, and will pick it to be executed and added into the next block (so in the blockchain).
  • The execution will take place on the miner node hardware(s) (gpu, cpu ... ) where the data of the contracts and its variables are on the blockchain which is a kind of distributed decentralized (raw file or db) including the whole network of users, contracts data
  • The execution is not free as the miner is running its hardware and consuming electricity, energy, internet etc...
  • The execution price in ethereum is about a 'GAS' consumption, where each OPCODE on the EVM will cost a specific amount of GAS
  • The GAS price is in WEI ( 1 Ether = 10^18 Wei)
  • A transaction will always include:
    • GAS Limit (which is about how much GAS Maximum you are willing to spend)
    • GAS Price (which is about how much you want to pay for the miner of each GAS spent)
  • Please note that the GAS Limit will guarantee that no transaction (code execution) will hand or take time more than it is priced, and no bad infinite loops may live there.
  • In case the miner GAS consumption reached the limit set by the transacting account, the transaction however will be mined, but will not be executed, Ethereum Amount will be reversed, but the GAS spent will not, and the contract state is rolled back! transaction status is Failed - Out Of Gas!

    * Prepare your environment by installing GIT distributed version control system
    * Install nodejs and node package manager

    Notepad++ or any text editor is basically enough next on...

    Open a commnad line terminal [make sure git, npm are installed and set as in the system path (environment variable) ]

    > git clone git@bitbucket.org:braingerm/cp-etherdrop.git

    > cd <project_root_folder_path>

    > npm install (this will install the dependencies and truffle into node_modules from the packages.json)

    * Install the local blockchain emulation node Ganache

    * Run Ganache:

  • It will listen by default on localhost port 7545
  • It will create 10 accounts by default with menmonic phrase (with 100 ETH each)
  • The mnemonic phrase can be imported in Metamask or other supporting wallet.
    It is more like your account password (private keys)

    Now most of the tools are ready, it is time to explain, compile and deploy the EtherDrop SmartContract,
    Given the project structure, the EtherDrop smartcontract relies under ./contracts/EtherDrop.sol.

    *.sol extension stands for solidity.

    JavaScript
    pragma solidity ^0.4.20; // This is the Solidity Used Compiler Version
    
    /**
     * @author FadyAro
     *
     * 22.07.2018
     *
    
    /**
     * This contract is inherited later, to manage the owner, and use restriction for 
     * on some functions later in next contract(s)
     */
    contract Ownable {
    
        // the contract state variable as mentioned before
        address public owner;
    
        // whenever the ownership has been transferred log and event for auditing on the blockchain
        event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
    
        constructor() public {
            owner = msg.sender;
        }
    
        // this modifier when added to a function will make sure that the caller the owner
        // of the deployed contract
        modifier onlyOwner() {
            require(msg.sender == owner);
            _;
        }
    
        // a function to transfer the ownership to another account (address)
        // trigger the event later on (emit ...)
        function transferOwnership(address newOwner) public onlyOwner {
            require(newOwner != address(0));
            emit OwnershipTransferred(owner, newOwner);
            owner = newOwner;
        }
    }
    
    ...... // for briefing
    
    /**
     * This contract will run the EtherDrop logic
     */
    contract EtherDrop is Pausable {
    
        /*
         * lotto ticket price is 2e16 = 0.02 ETH
         */
        uint constant PRICE_WEI = 2e16;
    
        ...... // for briefing
    
        /*
         * this event is when we have a new winner
         * it is as well a new round start => (round + 1)
         */
        event NewWinner(address addr, uint round, uint place, uint value, uint price);
    
        struct history {
    
            /*
             * user black listed comment
             */
            uint blacklist;
    
            /*
             * user rounds subscriptions number
             */
            uint size;
    
            /*
             * array of subscribed rounds indexes
             */
            uint[] rounds;
    
            ...... // for briefing
        }
    
        /*
         * active subscription queue
         */
        address[] private _queue;
    
        /*
         * winners history
         */
        address[] private _winners;
    
        ...... // for briefing
    
        /*
         * active round queue pointer
         */
        uint public _counter;
    
        /*
         * allowed collectibles
         */
        uint private _collectibles = 0;
    
        /*
         * users history mapping
         */
        mapping(address => history) private _history;
    
        /**
         * get current round details
         */
        function currentRound() public view returns 
                 (uint round, uint counter, uint round_users, uint price) {
            return (_round, _counter, QMAX, PRICE_WEI);
        }
    
        /**
         * get round stats by index
         */
        function roundStats(uint index) public view returns 
                 (uint round, address winner, uint position, uint block_no) {
            return (index, _winners[index], _positions[index], _blocks[index]);
        }
    
       ...... // for briefing
    
        /**
         * round user subscription
         */
        function() public payable whenNotPaused {
            /*
             * check subscription price
             */
            require(msg.value >= PRICE_WEI, 'Insufficient Ether');
    
            /*
             * start round ahead: on QUEUE_MAX + 1
             * draw result
             */
            if (_counter == QMAX) {
    
                uint r = DMAX;
    
                uint winpos = 0;
    
                _blocks.push(block.number);
    
                // derive a random winning position
                bytes32 _a = blockhash(block.number - 1);
    
                for (uint i = 31; i >= 1; i--) {
                    if (uint8(_a[i]) >= 48 && uint8(_a[i]) <= 57) {
                        winpos = 10 * winpos + (uint8(_a[i]) - 48);
                        if (--r == 0) break;
                    }
                }
    
                _positions.push(winpos);
    
                /*
                 * post out winner rewards
                 */
                uint _reward = (QMAX * PRICE_WEI * 90) / 100;
                address _winner = _queue[winpos];
    
                _winners.push(_winner);
                _winner.transfer(_reward);
    
                ...... // for briefing
    
                /*
                 * log the win event: winpos is the proof, history trackable
                 */
                emit NewWinner(_winner, _round, winpos, h.values[h.size - 1], _reward);
    
                ...... // for briefing
    
                /*
                 * reset counter
                 */
                _counter = 0;
    
                /*
                 * increment round
                 */
                _round++;
            }
    
            h = _history[msg.sender];
    
            /*
             * user is not allowed to subscribe twice
             */
            require(h.size == 0 || h.rounds[h.size - 1] != _round, 'Already In Round');
    
            /*
             * create user subscription: N.B. places[_round] is the result proof
             */
            h.size++;
            h.rounds.push(_round);
            h.places.push(_counter);
            h.values.push(msg.value);
            h.prices.push(0);
    
            ...... // for briefing
    
            _counter++;
        }
    
        ...... // for briefing
         
    }

    After reading/analysing the commented functions, let's focus on the function() public payable whenNotPaused

    function() is the function keyword
    this one is different from other functions, it has no name
    it calls the fallback function of the contract
    that means when a contract is called with no point function, it will go to the fallback if found

    public means that a function can be executed from outside the contract via a transaction

    payable is a required keyword to make a function able to accept ethereum -> to the contract balance

  • Within the fallback function, you may notice many unassigned variable, please refer to the ethereum docs to know about global variables and transaction properties...
    • msg.value contains the transaction ether sent amount (to the contract)
    • msg.sender contains the sender address
msg.data (bytes): complete calldata
msg.gas (uint): remaining gas - deprecated in version 0.4.21 and to be replaced by gasleft()
msg.sender (address): sender of the message (current call)
msg.sig (bytes4): first four bytes of the calldata (i.e. function identifier)
msg.value (uint): number of wei sent with the message

_winner.transfer(_reward); _winner is a variable of type address, so the function transfer applies to transferring the _reward to the address, where _reward is a uint of size 256 bytes, in WEI unit

So the EtherDrop contract will work as:

  1. In a same round, only one subscription per address is allowed, else transaction is reversed:
    JavaScript
    // checkout the history struct  { }
    // get user history, _history is a more like a hashmap<address, history>
    h = _history[msg.sender];
    
    // this is a double check is the address or user has enter the same round before
    require(h.size == 0 || h.rounds[h.size - 1] != _round, 'Already In Round');
    
  2. In a round, the subscription price is 0.02 Ether (or 2e16 wei)
  3. When the queue is full, on the next round start the winner is picked
  4. Picking a user is from the upcoming block

Each block has a hash, the upcoming block hash will be used as source of randomness
A number between 1 and 1000 will be derived.
The _queue[winpos] will pick the winner and send its reward

Compile the contract using truffle:

In the commnad line terminal [make sure you installed truffle ]
> truffle compile (this will compile the contract and show errors / warnings...)

Notice 'writing artifacts to .\build\contracts', this will write output *.json contract built specs used later on
in the web client js, to interact with the contract on the blockchain...

[ B ]

To Deploy the SmartContract using truffle, it should be added under ./migrations./2_deploy_contracts.js:

JavaScript
let EtherDrop = artifacts.require("EtherDrop"); // read the contract EtherDrop.sol
module.exports = function (deployer) {
    deployer.deploy(EtherDrop);                 // direct deployment instruction
};

Now in the root folder, notice what's in the truffle.js file:

JavaScript
module.exports = {
  networks: {
    development: {
      host: "127.0.0.1",    // where (which node) to deploy the contract
      port: 7545,           // the node port
      gas: 20500000,        // if you want to set network gas block tx limit Current 
                            // Ropsten gas limit. See https://ropsten.etherscan.io/block/3141628
      gasPrice: 1100000000, // if you want to change default network gas price 1.1 GWei - based on 
                            // the lower end of current txs getting into blocks currently on Ropsten.
      network_id: "5777"    // this is the network id (in case of Ganache truffle it is 5777 
                            // as in the screenshot)
    }
  }
};

Let us now deploy the contract to the blockchain (test local ganache):

in the command line go to the migrations folder :
> cd migrations

> truffle migrate

Please note the contract address (@ Deploying EtherDrop... 0x7ac26cc...).

This is the Lottery Contract Address (on local test net of course).

It is where a participant will sent the 0.02 ETH to subscribe to a round (later on in Web UI)

You see the contract deployment transaction in ganache logs:

[ C ]

In this step, we will talk about the Web Client.

Since the name is EtherDrop, it is about a 'Drop' that will be filled when the round subscription is full.

I used Materializecss, a modern responsive front-end framework based on Material Design.

Please go there to learn more their layout, grids, columns, rows, container and styling...

We won't focus on the HTML design, let's jump to the Web3 and Js section, mainly where we interact with the smart contract and events.

Interacting with Blockchain needs a web3 provider, that most of current browsers do not support.

For that, you may install Metamask on chrome and get it pointed to localhost:

with ganache in networks settings, and import the wallet using the mnemonic phrase:

provided by ganache (in accounts sections):

Now let us check the js side:

Opening src/js/ed-client.js

    ... // brief
    
    // check for web3 provider
    initWeb3: () => {
        if (typeof web3 !== 'undefined') {
            BetClient.web3Provider = web3.currentProvider;
        } else {
            if (BetClient.onNoDefaultWeb3Provider) {
                setTimeout(BetClient.onNoDefaultWeb3Provider, 3000);
            }
            // if no web3 found or metamask just use for the infura
            let infura = 'https://mainnet.infura.io/plnAtKGtcoxBtY9UpS4b';
            BetClient.web3Provider = new Web3.providers.HttpProvider(infura);
        }
        web3 = new Web3(BetClient.web3Provider);
        return BetClient.initContract();
    },

    initContract: () => {
        // load the generated in build.. EtherDrop.Json
        dbg(`loading ${BetClient.jsonFile}`);
        $.getJSON(BetClient.jsonFile, (data) => {
            dbg('got abi');
            BetClient.contracts.EtherDrop = TruffleContract(data);
            BetClient.contracts.EtherDrop.setProvider(BetClient.web3Provider);
            // watch out the address from the network id (1 on mainnet)
            BetClient.address = data.networks[5777].address;
            if (BetClient.onContractLoaded) {
                BetClient.onContractLoaded();
            }
            BetClient.listen();
        });
    },

    // example of read the EtherDrop Current Round details
    loadRoundInfo: () => {
        BetClient.contracts.EtherDrop
            .deployed()
            .then((instance) => {
                // note that the instance is the contract EtherDrop itself
                // having in (EtherDrop.sol) the function currentRound()
                // that will return the round no, the counter, the price etc ...
                return instance.currentRound.call();
            })
            .then(function (result) {
                if (BetClient.onLoadRoundInfo)
                    // here we got the result[] of BigNumber uint256
                    // we may convert them to [] of Numbers
                    // and load them to the UI Presenter on callback
                    BetClient.onLoadRoundInfo($.map(result, (r) => {
                        return r.toNumber();
                    }));
            })
            .catch((e) => BetClient.raiseError(e, 'loadRoundInfo'));
    },

    ... // brief 


    // this is the payment part in case to be handler by Metamask or Web3 Browser
    participate: (amount) => {
        
        // get a loaded account from Metamask, or trustwallet 
        web3.eth.getAccounts((error, accounts) => {
            if (error) {
                BetClient.raiseError(error, 'Participate');
            } else {
               
                // use the account to do the transaction
                dbg(`accounts: ${JSON.stringify(accounts)}`);
                if (accounts.length === 0) {
                    BetClient.raiseError('No Accounts', 'Participate');
                } else {
                    BetClient.contracts.EtherDrop
                        .deployed()
                        .then((instance) => {
                            return instance.sendTransaction({
                                from: accounts[0],
                                value: amount // 0.02 ETH
                            }).then(function (result) {
                                dbg(JSON.stringify(result));
                            });
                        })
                        .catch((e) => BetClient.raiseError(e, 'Participate'));
                }
            }
        });
    },

    // listen to the contract events: New Winner, New Participation
    listen: () => {
        BetClient.contracts.EtherDrop
            .deployed()
            .then((instance) => {
                return instance; // the contract instance
            })
            .then((result) => {

                // blockchain read filtering from the latest to pending block
                const options = {fromBlock: 'latest', toBlock: 'pending'};

                // register for the NewDropIn (new subscription)
                result.NewDropIn({}, options).watch(function (error, result) {
                    if (error)
                        BetClient.raiseError(error, 'NewDropIn');
                    else if (BetClient.onNewDropIn)
                        BetClient.onNewDropIn(result);
                });

                // register for the NewWinner (and it is a new round start same time)
                result.NewWinner({}, options).watch((error, result) => {
                    if (error)
                        BetClient.raiseError(error, 'NewWinner');
                    else if (BetClient.onNewWinner)
                        BetClient.onNewWinner(result);
                });
            })
            // handle errors
            .catch((e) => BetClient.raiseError(e, 'listen'));
    }
};

[ D ]

Now we have talked enough, let us run the webserver locally.

We are using nodeJs - lite server (defined in the package.json dependency file) which uses bs plugin (browser sync).

I like this static pages serving server, it syncs the webpage on each file content change.

Handy for light webapp development, note the bs-config.json:

It tells the lite-server to watch and serve the files under ./src and ./build/contracts (where the EtherDrop.json is there):

JavaScript
{
  "server": {
    "baseDir": ["./src", "./build/contracts"]
  }
} 

Yupp, now let's run the server:

in the command line terminal type:

> npm run dev

TADA! The DApp will launch on your default browser, open it on Chrome.

Make sure metamask is well configured as mentioned before! Congrats!

[ E ]

When it is deployed in the mainnet [Network Id is 1].

In the EtherDrop.Json in the end of file, you will have to replace the network id from 5777 to 1.
Also, replace the creation transaction hash and contract address by the corresponding ones (deployed live contract, e.g., from remix IDE).

Let's have a look at the deployed SmartContract on Etherscan.io:

https://etherscan.io/address/0x81b1ff50d5bca9150700e7265f7216e65c8936e6

  • Click on transactions to browser the transactions done on our EtherDrop Smart Contract.
    We can see a transaction that's failing because the sender did include enough amount of GAS :(
  • Click On Code to view the verified contract source code (binary compilation verification):

Points of Interest

This contract is on its first release, on the next drop / round, the UI will be updated, more features will be added and the contract will be migrated and a new one will be deployed including optimization, performance enhancements and a lot of upcoming precautions. ;)

Join in and keep yourself updated! Long Live Bitcoin, Long Live Ethereum!

Thank you!

Etherdrop.app

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
Lebanon Lebanon
Bitcoin, Ethereum - Smartcontracts, Full Stack, Architecture & Development, Music!

Comments and Discussions

 
QuestionDApps Development Pin
Roudy Khoury10-Aug-18 21:14
Roudy Khoury10-Aug-18 21:14 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.