国内用户请加微信:19726581
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/
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.
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.
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
Environment
Tech Stack
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
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.
stake()
execute()
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
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
The funds raised are transferred to another contract after certain conditions are met.
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.
In the stake contract, to bring in this contract, there is a constructor.
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
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");
}
// 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
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.
modifier stakeNotCompleted() {
bool completed = exampleExternalContract.completed();
require(!completed, "staking process is already completed");
_;
}
Is the second one up to time?
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");
}
_;
}
How to modify these functions
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");
}
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();
}
}
yarn deploy --reset