Staker | Web3.0 dApp Dev 0x09

国内用户请加微信:19726581

Authors: Fenix, msfew

0x00 Goal

Buidl a decentralized Staking dApp.

This example also corresponds to Challenge 0x01 in SpeedrunEthereum (an official Ethereum developer challenge):

https://speedrunethereum.com/challenge/decentralized-staking

An example of one of these completions can be found at:

Contract: https://rinkeby.etherscan.io/address/0x4707468C95558E9B2F339c2A41DAEF483Ce11104
dApp: https://duckgo_staking.surge.sh/

image-20220717121147667
image-20220717121147667

0x01 What is Staking

Staking, which translates to pledge, staking mining, or staking of interest, is the algorithm used to generate new blocks, and is derived from Proof of Stake.

If you're familiar with how Bitcoin works, you're probably familiar with Proof of Work (PoW). This mechanism allows transactions to be collected into blocks. These blocks are then linked together to create a blockchain.

Specifically, miners compete to solve complex mathematical puzzles, and whoever solves the puzzle first is entitled to add the next block to the blockchain. Proof of workload has proven to be a very powerful mechanism to facilitate consensus in a decentralized manner. The problem is that this mechanism involves a lot of arbitrary computation. Miners are scrambling to solve the puzzle just to maintain network security and nothing else.

Moving on to proof of stake, the main idea is that participants can lock in tokens (their "pledged interest") and the protocol will randomly assign rights to one of them for verification of the next block at a given time interval. Usually, the probability of being selected is proportional to the number of tokens: the more tokens locked, the better the chances.

Staking & Mining
Staking & Mining

In the cryptocurrency market, mining has been gradually replaced by staking in recent years, with the benefit of lower power consumption and passive income than the former.

0x02 Speed Run Web3

Some students may not have experience with web3 related development, so here is a speed run on your behalf to facilitate a quick introduction to the development on Ether.

Prerequisites

  • metamask Wallet

Environment

  • nodejs
  • yarn
  • git
  • vscode

Tech Stack

  • Framework: scaffold-eth
  • Frontend: react
  • Contract Development: hardhat

Step 1: Scaffolding

git clone https://github.com/scaffold-eth/scaffold-eth.git
cd scaffold-eth
yarn install

Step 2: Start a local network

cd scaffold-eth
yarn chain

Step 3: Deploy Smart Contracts

cd scaffold-eth
yarn deploy

Step 4: Open the front-end page

cd scaffold-eth
yarn start
  • Technology Stack
    • Top Level Technology Stack
      • solidity contract programming language
      • hardhat local development test chain
      • react front-end
      • etherseth api sdk
      • antd front-end components ui
    • Libraries & Components & Services
      • Eth-components
      • Eth-services
      • Command Line
    • Underlying Foundation
      • the graph
      • tenderly
      • etherscan
      • rpc
      • blocknative
      • L2/Sidechain Services
        • Arbitrum
        • Optimism
        • Graph Node
    • Examples
      • General
        • simple dao
        • Diamond Standard
        • Meta-Multi-Sig Wallet
        • Minimal Proxy
        • Minimum Viable Payment Channel
        • PunkWallet.io
        • Push The Button - Multi-player Turn Based Game
        • radwallet.io
        • Signator.io
        • Simple Stream
        • Token Allocator
        • Streaming Meta Multi Sig
      • DeFi
        • Bonding Curve
        • rTokens
        • Quadratic Funding
        • Uniswapper
        • Lender
        • Aave Flash Loans Intro
        • Aave Ape
        • DeFi Subgraphs
      • NFT
        • Simple NFT
        • Simple ERC-1155 NFT
        • Chainlink VRF NFT
        • Merkle Mint NFT
        • Nifty Viewer
        • NFT Auction
        • NFT Signature Based Auction
      • Security
        • Honeypot
        • Re-entrancy Attack
        • Denial of Service
      • Infrastructure
        • ChainLink
      • Layer2
        • Optimism Starter Pack
        • Optimism NFT
        • xNFT.io

0x03 Staking Contract

We all know that the most important part of dApp development is writing smart contracts, so let's analyze the basic format of a Staking contract.

Pledge (stake) a certain amount of tokens (threshold) within a certain time (deadline).
After the expiration date, you can transfer (execute) the tokens to another contract, or withdraw (withdraw) the tokens.

So we abstracted out three key functions.

  1. stake()
  2. execute()
  3. withdraw()

scaffold-eth also provides us with such a scaffold, just pull down the code and we will build on it step by step this time.

git clone https://github.com/scaffold-eth/scaffold-eth-challenges.git
cd scaffold-eth-challenges
git checkout challenge-1-decentralized-staking
yarn install

Then open three terminal windows and execute the following three commands:

yarn chain
yarn start
yarn deploy --reset

0x04 Live Coding

4.1 stake

  • Key point 1: Stake a certain amount of eth at a time.
pragma solidity 0.8.4;

import "hardhat/console.sol";
import "./ExampleExternalContract.sol";

contract Staker {
    mapping(address => uint256) public balances;

    event Stake(address indexed staker, uint256 amount);

    function stake() public payable {
        balances[msg.sender] += msg.value;
        emit Stake(msg.sender, msg.value);
    }
}
  • Key point 2, deploy the script to remove the constructor's parameters
// deploy/01_deploy_staker.js
// ....

await deploy('Staker', {
  // Learn more about args here: https://www.npmjs.com/package/hardhat-deploy#deploymentsdeploy
  from: deployer,
  // args: [exampleExternalContract.address],
  log: true,
});

//...
  • Key Point 3, Deployment
yarn deploy --reset
  • Key point 4, airdrop some test coins
  • Key point 5, test staking

4.2 execute

The funds raised are transferred to another contract after certain conditions are met.

  • Key point 1, another contract
contract ExampleExternalContract {

  bool public completed;

  function complete() public payable {
    completed = true;
  }

}

It's simple, there is a flag to indicate whether it is finished or not.

  • Key point 2, the constructor

In the stake contract, to bring in this contract, there is a constructor.

ExampleExternalContract public exampleExternalContract;

constructor(address exampleExternalContractAddress) public {
        exampleExternalContract = ExampleExternalContract(
            exampleExternalContractAddress
        );
}
  • Key point 3, initialize when deploying
// deploy/01_deploy_staker.js
// ....

await deploy('Staker', {
  // Learn more about args here: https://www.npmjs.com/package/hardhat-deploy#deploymentsdeploy
  from: deployer,
  args: [exampleExternalContract.address],
  log: true,
});

//...
  • Key Point 4, Staking Limit
uint256 public constant threshold = 1 ether;
  • Key point 5, transfer to the second contract.
    function execute() public {
        uint256 contractBalance = address(this).balance;

        // check the contract has enough ETH to reach the treshold
        require(contractBalance >= threshold, "Threshold not reached");

        // Execute the external contract, transfer all the balance to the contract
        // (bool sent, bytes memory data) = exampleExternalContract.complete{value: contractBalance}();
        (bool sent, ) = address(exampleExternalContract).call{
            value: contractBalance
        }(abi.encodeWithSignature("complete()"));
        require(sent, "exampleExternalContract.complete failed");
    }
  • The final code is as follows
// SPDX-License-Identifier: MIT
pragma solidity 0.8.4;

import "hardhat/console.sol";
import "./ExampleExternalContract.sol";

contract Staker {
    ExampleExternalContract public exampleExternalContract;

    mapping(address => uint256) public balances;

    uint256 public constant threshold = 1 ether;

    event Stake(address indexed staker, uint256 amount);

    constructor(address exampleExternalContractAddress) public {
        exampleExternalContract = ExampleExternalContract(
            exampleExternalContractAddress
        );
    }

    function stake() public payable {
        balances[msg.sender] += msg.value;
        emit Stake(msg.sender, msg.value);
    }

    function execute() public {
        uint256 contractBalance = address(this).balance;

        require(contractBalance >= threshold, "Threshold not reached");

        (bool sent, ) = address(exampleExternalContract).call{
            value: contractBalance
        }(abi.encodeWithSignature("complete()"));
        require(sent, "exampleExternalContract.complete() failed");
    }
}
  • Deployment
yarn deploy --reset
  • Airdrop test coins
  • stake some coins reach the limit
  • test execute

4.3 withdraw

Withdrawing the staked money is relatively simple - just transfer the money out.

    function withdraw() public {
        uint256 userBalance = balances[msg.sender];

        require(userBalance > 0, "You don't have balance to withdraw");

        balances[msg.sender] = 0;

        (bool sent, ) = msg.sender.call{value: userBalance}("");
        require(sent, "Failed to send user balance back to the user");
    }
  • The complete code is as follows
// SPDX-License-Identifier: MIT
pragma solidity 0.8.4;

import "hardhat/console.sol";
import "./ExampleExternalContract.sol";

contract Staker {
    ExampleExternalContract public exampleExternalContract;

    mapping(address => uint256) public balances;

    uint256 public constant threshold = 1 ether;

    uint256 public deadline = block.timestamp + 30 seconds;

    event Stake(address indexed sender, uint256 amount);

    constructor(address exampleExternalContractAddress) public {
        exampleExternalContract = ExampleExternalContract(
            exampleExternalContractAddress
        );
    }

    function stake() public payable {
        balances[msg.sender] += msg.value;

        emit Stake(msg.sender, msg.value);
    }

    function execute() public {
        uint256 contractBalance = address(this).balance;

        require(contractBalance >= threshold, "Threshold not reached");

        (bool sent, ) = address(exampleExternalContract).call{
            value: contractBalance
        }(abi.encodeWithSignature("complete()"));
        require(sent, "exampleExternalContract.complete failed");
    }

    function withdraw() public {
        uint256 userBalance = balances[msg.sender];

        require(userBalance > 0, "You don't have balance to withdraw");

        balances[msg.sender] = 0;

        (bool sent, ) = msg.sender.call{value: userBalance}("");
        require(sent, "Failed to send user balance back to the user");
    }
}
  • Deployment
yarn deploy --reset
  • Airdrop test coins
  • stake some coins
  • test withdraw

4.4 Add time or staking expiration restrictions

There are two criteria to judge here

The first is whether the time has been reached, and the other is whether the pledge has been completed.

  • Whether the first one is completed or not, just go directly to the mark of the other contract
		modifier stakeNotCompleted() {
        bool completed = exampleExternalContract.completed();
        require(!completed, "staking process is already completed");
        _;
    }
  • Is the second one up to time?

    • The first is to have a deadline variable
    uint256 public deadline = block.timestamp + 60 seconds;
    
    • Then there is also a timeLeft function
    		function timeLeft() public view returns (uint256 timeleft) {
            if (block.timestamp >= deadline) {
                return 0;
            } else {
                return deadline - block.timestamp;
            }
        }
    
    • Next is the deadlineReached function
    		modifier deadlineReached(bool requireReached) {
            uint256 timeRemaining = timeLeft();
            if (requireReached) {
                require(timeRemaining == 0, "deadline is not reached yet");
            } else {
                require(timeRemaining > 0, "deadline has already reached");
            }
            _;
        }
    
  • How to modify these functions

    • stake
    	function stake() public payable deadlineReached(false) stakeNotCompleted {
            balances[msg.sender] += msg.value;
            emit Stake(msg.sender, msg.value);
        }
    
    • execute function
    	function execute() public stakeNotCompleted deadlineReached(false) {
            uint256 contractBalance = address(this).balance;
    
            require(contractBalance >= threshold, "Threshold not reached");
    
            (bool sent, ) = address(exampleExternalContract).call{
                value: contractBalance
            }(abi.encodeWithSignature("complete()"));
            require(sent, "exampleExternalContract.complete() failed");
        }
    
    • withdraw function
    	function withdraw() public deadlineReached(true) stakeNotCompleted {
            uint256 userBalance = balances[msg.sender];
    
            require(userBalance > 0, "You don't have balance to withdraw");
    
            balances[msg.sender] = 0;
    
            (bool sent, ) = msg.sender.call{value: userBalance}("");
            require(sent, "Failed to send user balance back to the user");
        }
    
  • Functions that can be called by external contracts

	receive() external payable {
        stake();
    }

The final code is as follows:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.4;

import "hardhat/console.sol";
import "./ExampleExternalContract.sol";

contract Staker {
    ExampleExternalContract public exampleExternalContract;

    mapping(address => uint256) public balances;

    uint256 public constant threshold = 1 ether;

    event Stake(address indexed staker, uint256 amount);

    uint256 public deadline = block.timestamp + 60 seconds;

    constructor(address exampleExternalContractAddress) public {
        exampleExternalContract = ExampleExternalContract(
            exampleExternalContractAddress
        );
    }

    modifier stakeNotCompleted() {
        bool completed = exampleExternalContract.completed();
        require(!completed, "staking process is already completed");
        _;
    }

    modifier deadlineReached(bool requireReached) {
        uint256 timeRemaining = timeLeft();
        if (requireReached) {
            require(timeRemaining == 0, "deadline is not reached yet");
        } else {
            require(timeRemaining > 0, "deadline has already reached");
        }
        _;
    }

    function timeLeft() public view returns (uint256 timeleft) {
        if (block.timestamp >= deadline) {
            return 0;
        } else {
            return deadline - block.timestamp;
        }
    }

    function stake() public payable deadlineReached(false) stakeNotCompleted {
        balances[msg.sender] += msg.value;
        emit Stake(msg.sender, msg.value);
    }

    function execute() public stakeNotCompleted deadlineReached(false) {
        uint256 contractBalance = address(this).balance;

        require(contractBalance >= threshold, "Threshold not reached");

        (bool sent, ) = address(exampleExternalContract).call{
            value: contractBalance
        }(abi.encodeWithSignature("complete()"));
        require(sent, "exampleExternalContract.complete() failed");
    }

    function withdraw() public deadlineReached(true) stakeNotCompleted {
        uint256 userBalance = balances[msg.sender];

        require(userBalance > 0, "You don't have balance to withdraw");

        balances[msg.sender] = 0;

        (bool sent, ) = msg.sender.call{value: userBalance}("");
        require(sent, "Failed to send user balance back to the user");
    }

    receive() external payable {
        stake();
    }
}
  • Deployment
yarn deploy --reset
  • Testing

0x05 Related Information

Subscribe to Web3dAppDevCamp
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.