Staker | Web3.0 dApp 开发(九)

如何开发质押功能?

作者 Fenix@NonceGeekDAO

0x00 目标

实现一个去中心化的Staking dApp。

本文实例亦对应 SpeedrunEthereum(一个 Ethereum 官方的开发者挑战项目)中的Challenge 0x01

其中一个完成的例子可见:

合约: dApp:

image-20220717121147667

0x01 What is Staking

Staking 可译为质押、质押挖矿或权益质押,是产生新区块的运算方式,由权益证明(Proof of Stake) 引伸出来。

如果您清楚比特币的运作方式,可能就会熟悉工作量证明 (PoW)。可通过该机制将交易收集到区块中。然后,这些区块会链接在一起,来创建区块链。

具体来说,矿工们会争相解出复杂的数学难题,谁先解出难题,谁就有权将下一个区块添加到区块链。事实证明,工作量证明是一种非常强大的机制,可以去中心化的方式促进达成共识。问题在于,这种机制涉及到大量的任意计算。矿工们争相解出的难题只是为了维护网络安全,别无其他目的。

然后再来看权益证明,主要理念是参与者可以锁定代币(他们的“质押权益”),在特定时间间隔内,协议会随机分配权利给其中一个人,以供验证下一个区块。通常,被选中的概率与代币数量成正比:锁定的代币越多,机会就越大。

质押挖矿Stacking 挖矿Mining
共识机制:权益机制(PoS) 共识机制:工作量证明(PoW)
内容:付出一定数量加密货币作抵押,将验证工作交由其他人,抵押的加密货币数量愈多愈有利 内容:利用电脑的演算力争取下一个区块的验证节点,演算力愈高愈有利
例子:以太币2.0、币安币、Solana、Cosmos (ATOM) 例子:比特币、以太币1.0

在加密货币市场中,近年以挖矿(mining) 赚取收入的方式逐渐被质押挖矿(staking) 取代,好处是比前者减低耗电,同时又可赚取被动收入。

0x02 Speed Run Web3

可能有些同学没有接触过web3相关的开发,这里代领大家做一个speed run,方便快速入门以太坊上相关的开发。

前置

  • metamask钱包

开发环境

  • nodejs
  • yarn
  • git
  • vscode

技术栈

  • 框架:scaffold-eth
  • 前端:react
  • 合约开发:hardhat

第一步:脚手架

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

第二步:启动一个本地网络

cd scaffold-eth
yarn chain

第三步:部署智能合约

cd scaffold-eth
yarn deploy

第四步:打开前端页面

cd scaffold-eth
yarn start
  • 技术栈
    • 顶层技术栈
    • solidity合约编程语言
    • hardhat本地开发测试链
    • react前端
    • etherseth的api sdk
    • antd前端组件ui
    • 库&组件&服务
    • Eth-components
    • Eth-services
    • 命令行
    • 底层基础
    • the graph
    • tenderly
    • etherscan
    • rpc
    • blocknative
    • L2/Sidechain Services
      • Arbitrum
      • Optimism
      • Graph Node
    • 例子
    • 通用
      • simple dao
      • Diamond Standard
      • Meta-Multi-Sig Wallet
      • Minimal Proxy
      • Minimum Viable Payment Channel
      • Push The Button - Multi-player Turn Based Game
      • 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
    • 安全
      • Honeypot
      • Re-entrancy Attack
      • Denial of Service
    • 基础设施
      • ChainLink
    • Layer2 扩展
      • Optimism Starter Pack
      • Optimism NFT

0x03 Staking合约

我们都知道,dApp开发最重要的就是编写智能合约,我们先来分析一下Staking合约的基本格式。

在一定时间(deadline)内,质押(stake)一定数量(threshold)的代币。 到期之后可以将代币转入(execute)到另一个合约,也可以将代币提取出来(withdraw)。

所以我们抽像出来了三个关键函数:

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

scaffold-eth也为我们提供了这样的一个脚手架,只需要把代码拉下来,我们本次就在这个基础上逐步来实现。

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

然后开三个终端窗口,执行如下三个命令:

yarn chain
yarn start
yarn deploy --reset

0x04 Live Coding

4.1 stake

  • 关键点1,每次质押一定数量的eth。
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);
    }
}
  • 关键点2,部署脚本移除构造函数的参数
// 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,
  });

//...
  • 关键点3,部署
yarn deploy --reset
  • 关键点4,空投一些测试币
  • 关键点5,测试stake

4.2 execute

筹集到的资金,在满足一定条件之后,转移给另一个合约中。

  • 关键点1,另一个合约
contract ExampleExternalContract {

  bool public completed;

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

}

很简单,有一个flag代表是否已经结束了。

  • 关键点2,构造函数

在stake合约中,要把这个合约引入进来,同时要有一个构造函数

ExampleExternalContract public exampleExternalContract;

constructor(address exampleExternalContractAddress) public {
        exampleExternalContract = ExampleExternalContract(
            exampleExternalContractAddress
        );
}
  • 关键点3,部署的时候要初始化
// 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,
  });

//...
  • 关键点4,质押上限
uint256 public constant threshold = 1 ether;
  • 关键点5,向第二个合约转账。
    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");
    }
  • 最终代码如下
// 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");
    }
}
  • 部署
yarn deploy --reset
  • 空投测试币
  • stake 一些币到达上限
  • 测试 execute

4.3 withdraw

将质押的钱提取出来,这个比较简单,就是将钱转移出来即可。

    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");
    }
  • 完整代码如下
// 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");
    }
}
  • 部署
yarn deploy --reset
  • 空投测试币
  • stake一些币
  • 测试withdraw

4.4 加上时间或质押到期限制

这里有两个判断的标准

首先是是否到达时间,另一个就是质押是否已经完成。

  • 第一个是否完成,直接去看另一个合约的标志即可
        modifier stakeNotCompleted() {
        bool completed = exampleExternalContract.completed();
        require(!completed, "staking process is already completed");
        _;
    }
  • 第二个是否到时间了

    • 首先要有一个deadline变量
    uint256 public deadline = block.timestamp + 60 seconds;
    • 然后还要有一个timeLeft函数
        function timeLeft() public view returns (uint256 timeleft) {
          if (block.timestamp >= deadline) {
              return 0;
          } else {
              return deadline - block.timestamp;
          }
      }
    • 接着是deadlineReached函数
        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");
          }
          _;
      }
  • 如何修饰这些函数

    • stake
    function stake() public payable deadlineReached(false) stakeNotCompleted {
          balances[msg.sender] += msg.value;
          emit Stake(msg.sender, msg.value);
      }
    • execute函数
    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 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();
    }

最终代码如下:

// 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();
    }
}
  • 部署
yarn deploy --reset
  • 测试

0x05 相关资料

  • 2022世界杯预选赛赛程直播社区

本文参与2022世界杯预选赛赛程直播社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
李大狗
李大狗

上海对外经贸大学区块链研究中心副主任

66 篇文章, 6784 学分