如何开发质押功能?
作者 Fenix@NonceGeekDAO
实现一个去中心化的Staking dApp。
本文实例亦对应 SpeedrunEthereum
(一个 Ethereum 官方的开发者挑战项目)中的Challenge 0x01
:
https://speedrunethereum.com/challenge/decentralized-staking
其中一个完成的例子可见:
合约:https://rinkeby.etherscan.io/address/0x4707468C95558E9B2F339c2A41DAEF483Ce11104 dApp:https://duckgo_staking.surge.sh/
Staking 可译为质押、质押挖矿或权益质押,是产生新区块的运算方式,由权益证明(Proof of Stake) 引伸出来。
如果您清楚比特币的运作方式,可能就会熟悉工作量证明 (PoW)。可通过该机制将交易收集到区块中。然后,这些区块会链接在一起,来创建区块链。
具体来说,矿工们会争相解出复杂的数学难题,谁先解出难题,谁就有权将下一个区块添加到区块链。事实证明,工作量证明是一种非常强大的机制,可以去中心化的方式促进达成共识。问题在于,这种机制涉及到大量的任意计算。矿工们争相解出的难题只是为了维护网络安全,别无其他目的。
然后再来看权益证明,主要理念是参与者可以锁定代币(他们的“质押权益”),在特定时间间隔内,协议会随机分配权利给其中一个人,以供验证下一个区块。通常,被选中的概率与代币数量成正比:锁定的代币越多,机会就越大。
质押挖矿Stacking | 挖矿Mining |
---|---|
共识机制:权益机制(PoS) | 共识机制:工作量证明(PoW) |
内容:付出一定数量加密货币作抵押,将验证工作交由其他人,抵押的加密货币数量愈多愈有利 | 内容:利用电脑的演算力争取下一个区块的验证节点,演算力愈高愈有利 |
例子:以太币2.0、币安币、Solana、Cosmos (ATOM) | 例子:比特币、以太币1.0 |
在加密货币市场中,近年以挖矿(mining) 赚取收入的方式逐渐被质押挖矿(staking) 取代,好处是比前者减低耗电,同时又可赚取被动收入。
可能有些同学没有接触过web3相关的开发,这里代领大家做一个speed run,方便快速入门以太坊上相关的开发。
前置
开发环境
技术栈
第一步:脚手架
https://github.com/scaffold-eth/scaffold-eth
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
我们都知道,dApp开发最重要的就是编写智能合约,我们先来分析一下Staking合约的基本格式。
在一定时间(deadline)内,质押(stake)一定数量(threshold)的代币。 到期之后可以将代币转入(execute)到另一个合约,也可以将代币提取出来(withdraw)。
所以我们抽像出来了三个关键函数:
stake()
execute()
withdraw()
scaffold-eth也为我们提供了这样的一个脚手架,只需要把代码拉下来,我们本次就在这个基础上逐步来实现。
https://github.com/scaffold-eth/scaffold-eth-challenges
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
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);
}
}
// 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,
});
//...
yarn deploy --reset
筹集到的资金,在满足一定条件之后,转移给另一个合约中。
contract ExampleExternalContract {
bool public completed;
function complete() public payable {
completed = true;
}
}
很简单,有一个flag代表是否已经结束了。
在stake合约中,要把这个合约引入进来,同时要有一个构造函数
ExampleExternalContract public exampleExternalContract;
constructor(address exampleExternalContractAddress) public {
exampleExternalContract = ExampleExternalContract(
exampleExternalContractAddress
);
}
// 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,
});
//...
uint256 public constant threshold = 1 ether;
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
将质押的钱提取出来,这个比较简单,就是将钱转移出来即可。
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
这里有两个判断的标准
首先是是否到达时间,另一个就是质押是否已经完成。
modifier stakeNotCompleted() {
bool completed = exampleExternalContract.completed();
require(!completed, "staking process is already completed");
_;
}
第二个是否到时间了
uint256 public deadline = block.timestamp + 60 seconds;
function timeLeft() public view returns (uint256 timeleft) {
if (block.timestamp >= deadline) {
return 0;
} else {
return deadline - block.timestamp;
}
}
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 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();
}
最终代码如下:
// 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
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!