Skip to content

Category: Web3

Walkthrough – Solidity Smart Lottery project

The source of the project is from:

https://github.com/PatrickAlphaC/smartcontract-lottery
https://www.youtube.com/watch?v=M576WGiDBdQ&t=22298s

The purpose of this walkthrough is to explain each part of the Smart Contract code with more context so that we can gain better understanding.

Lottery.sol

Think about the different processes for a lottery.

  1. Participants have to enter the lottery. We need to track down who are the participants for this lottery.
  2. We need to know how much to charge the participants for entering the lottery.
  3. Owner will need to start the lottery.
  4. Owner will end the lottery and we need a random way to calculate the winner of the lottery. And we need a robust way of generating the random winner that minimise the probability of participants cheating.
  5. We need to track the state of the lottery: whether it is opened, closed or the winner is being calculated.

Tracking Participants

We need to create an address array to track down the participants. Before we add the new player to the participant list, they have to pay an entry fee. This entry fee is calculated based on current Ether price.

address payable[] public players;

function enter() public payable {
    require(msg.value > getEntranceFee());
    players.push(msg.sender);
}

We can use ChainLink price feed to get the latest Ether data. To do so, we need to specify the dependencies and remappings in brownie-config.yaml file.

import "@chainlink/contracts/src/v0.6/interfaces/AggregatorV3Interface.sol";

[...]
AggregatorV3Interface internal ethUsdPriceFeed;

function getEntranceFee() public view returns (uint256) {
    (, int256 price, , , ) = ethUsdPriceFeed.latestRoundData();
    uint256 adjustedPrice = uint256(price) * 10**10;
    uint256 costToEnter = (usdEntryFee * 10**18) / adjustedPrice;
    return costToEnter;
}

Walkthrough – Solidity Local Development setup (with Python)

Requirements

  • Install python3 and the following python packages:
pip install web3 py-solc-x
  • VS code IDE and extensions: Solidity (Juan Blanco), Python (Microsoft)

We are going to walkthrough a tutorial created by Patrick Collins (https://github.com/PatrickAlphaC/web3_py_simple_storage)

  • Install nodejs
  • Install ganache using yarn or npm (to simulate a local blockchain VM)

Setting and Deploying the Smart Contract

Create a new project folder and copy the sample solidity code to a new file SimpleStorage.sol:

// SPDX-License-Identifier: MIT

pragma solidity >=0.6.0 <0.9.0;

contract SimpleStorage {
    uint256 favoriteNumber;

    // This is a comment!
    struct People {
        uint256 favoriteNumber;
        string name;
    }

    People[] public people;
    mapping(string => uint256) public nameToFavoriteNumber;

    function store(uint256 _favoriteNumber) public {
        favoriteNumber = _favoriteNumber;
    }

    function retrieve() public view returns (uint256) {
        return favoriteNumber;
    }

    function addPerson(string memory _name, uint256 _favoriteNumber) public {
        people.push(People(_favoriteNumber, _name));
        nameToFavoriteNumber[_name] = _favoriteNumber;
    }
}

Create a deploy.py file. We are going to walkthrough step-by-step to understand each part of the code.

1. Import these required modules.

from solcx import compile_standard
from solcx import install_solc
import json
import os
from web3 import Web3

2. We use solcx to compile the smart contract code. This is a python wrapper for the Solidity compiler.

First we open and read the content of the smart contract code.

Then install the solidity compiler version. It will download the compiler from the project’s Github download link.

Configure the compile standards:

  • language
  • sources
  • settings
  • solidity compiler version

We can dump the compiled code to see the structure of the code.

with open("./SimpleStorage.sol", "r") as file:
    simple_storage_file = file.read()

print("Installing solc...")
install_solc('0.6.0')

# Configure compile standards
compiled_sol = compile_standard(
    {
        "language": "Solidity",
        "sources": {"SimpleStorage.sol": {"content": simple_storage_file}},
        "settings": {
            "outputSelection": {
                "*": {
                    "*": ["abi", "metadata", "evm.bytecode", "evm.bytecode.sourceMap"]
                }
            }
        }
    },
    solc_version="0.6.0"
)

# Dump the compiled code to see the structure of the code
with open("compiled_code.json", "w") as file:
    json.dump(compiled_sol, file)

3. Get the bytecode and abi from the compiled code. How do we know the structure of the JSON? Refer to the compiled_code.json file

bytecode = compiled_sol["contracts"]["SimpleStorage.sol"]["SimpleStorage"]["evm"]["bytecode"]["object"]

abi = compiled_sol["contracts"]["SimpleStorage.sol"]["SimpleStorage"]["abi"]

4. Now start ganache server and we can see the generated dummy accounts and private keys. Note down where the server is listening.

$ ganache
ganache v7.0.2 (@ganache/cli: 0.1.3, @ganache/core: 0.1.3)
Starting RPC server

Available Accounts
==================
(0) 0xB136383615B477B1B816f4227A509ea8F0C0c9DD (1000 ETH)
(1) 0xB6c6BDb34A834BAcc8e07c9765E2f85D1619beDc (1000 ETH)
(2) 0xb2e141ed4EF4F30BC7a1848FFbd623b19B08608C (1000 ETH)
(3) 0x32c0DB620E7355feE0254813932a4E7a454D74f8 (1000 ETH)
(4) 0xB19D507aEE3BdA3c9da9b360E491B88FFd857f14 (1000 ETH)
(5) 0x7B0929a005B39Cce2C9795558371F3865Cff1Bf9 (1000 ETH)
(6) 0x13665EC9cEE2915402BD7Ce69c05F70E9CBCF2ef (1000 ETH)
(7) 0x2C2c3C4585c9425969C03055554dD0C15f5a57b8 (1000 ETH)
(8) 0xdAD8Ae2871Cb242C001A68EB5Bc6941BFDB0d2A7 (1000 ETH)
(9) 0x85214561dCD632581a0b60BeE5989607005BE663 (1000 ETH)

Private Keys
==================
(0) 0xa4c6bac88b45ba1e21eafbd736c92ca60b67bbfb956ccd3da37fa6f83ebe38c1
(1) 0xfa197f239d6df371b0242b8fe96b0d1883a392ff5ab4502cefae0e972f07f081
(2) 0x6c96d2d7b0fb9b56cad49887e3f198de2faa93d321240201d0572732f83bbcc8
(3) 0xd9eb1ec171c1aee37b0603b85d58b615d678f3f9c85c4e5fe31c322113d009d3
(4) 0xd6c212925da4e19a4708b43d1728efef1cf5c839fd44ee411107837b30d8e38c
(5) 0x048c50d14357791a5cbf1ecbf1febda6bf215f946bac50fac410524cde6cb397
(6) 0xb9eeb83abac9b23a5bc801e584c646f92ecb37cd4b7770100f84ea2ccdf3a304
(7) 0xad157fbf68f3e7fb3047ca653c073b4389f5020ce8d397c7c8f3533c491a15ba
(8) 0x01caede47d8ad5d0e5c125b9d4ceba7abd201a26be10730fc926f673f275fd42
(9) 0x07da7e22ff6a56ce2c07bf724913e507621181f369d481fd94ebb12d577d4650

HD Wallet
==================
Mnemonic:      team shoot anchor limit inform imitate melody decrease wing sadness orange mammal
Base HD Path:  m/44'/60'/0'/0/{account_index}

Default Gas Price
==================
2000000000

BlockGas Limit
==================
30000000

Call Gas Limit
==================
50000000

Chain Id
==================
1337

RPC Listening on 127.0.0.1:8545

5. Setup connection to the ganache server


w3 = Web3(Web3.HTTPProvider("http://127.0.0.1:8545"))
chain_id = 1337
my_addr = "0xB136383615B477B1B816f4227A509ea8F0C0c9DD"
private_key = os.getenv("PRIVATE_KEY")

DO NOT HARDCODE YOUR PRIVATE KEY IN YOUR CODE REPOS

If we want to connect to Testnet environment, we can use an ETH gateway such as Infura or Alchemy.

6a. Create the contract with the provider using the abi and bytecode.

SimpleStorage = w3.eth.contract(abi=abi, bytecode=bytecode)

6b. Get latest transaction count and use it as the nonce

nonce = w3.eth.getTransactionCount(my_addr)

6c. Submit the transaction that deploys contract using the chainId, gasPrice, from (which addr) and nonce

transaction = SimpleStorage.constructor().buildTransaction(
    {
        "chainId": chain_id,
        "gasPrice": w3.eth.gas_price,
        "from": my_addr,
        "nonce": nonce
    }
)

6d. Sign the transaction with private key

signed_txn = w3.eth.account.sign_transaction(transaction, private_key=private_key)

7. Deploy the contract

tx_hash = w3.eth.send_raw_transaction(signed_txn.rawTransaction)tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)contract_addr = tx_receipt.contractAddressprint(f"Contract is deployed to {contract_addr}")

Using the Smart Contract

Since the Smart Contract is deployed, we can provide the contract address and abi to create the smart contract (“simple_storage”).

simple_storage = w3.eth.contract(address=contract_addr, abi=abi)

We can make a function call without changing any state in the smart contract. In this example, we are trying to retrieve the stored value in the smart contract.

print(f"Initial Stored Value = {simple_storage.functions.retrieve().call()}")

Now, we call the store function in the smart contract to update favoriteNumber variable.

greeting_transaction = simple_storage.functions.store(15).buildTransaction(
    {
        "chainId": chain_id,
        "gasPrice": w3.eth.gas_price,
        "from": my_addr,
        "nonce": nonce + 1
    }
)

We will sign this transaction with the private key, send the transaction to the Ganache server and then wait for the transaction receipt.

Notice if you execute the transaction in local blockchain VM, the transaction speed will be very fast. But in actual Testnet or Mainnet, the transaction is likely to be slower.


signed_greeting_txn = w3.eth.account.sign_transaction(greeting_transaction, private_key=private_key)

tx_greeting_hash = w3.eth.send_raw_transaction(signed_greeting_txn.rawTransaction)

tx_receipt = w3.eth.wait_for_transaction_receipt(tx_greeting_hash)

Let’s print the stored value and we can see it is changed to 15

print(simple_storage.functions.retrieve().call())