6. How to Build a Staking Dapp
Welcome to week 6 of the R2W3 series. In this tutorial,you will learn how to develop a simple staking dApp with interest accural, slashing, and deposit/withdraw functionality.
Over the past 5 weeks of R2W3, we've learned a lot of different Solidity and Javascript primitives that give us the basic building blocks for Web3 development.
We've learned how to use Hardhat from scratch, build our own frontends, and even write Solidity.
While all of these skills are extremely valuable for developers looking to build a solid base, there are also tools that help abstract away some of the complexities of environment setups and dependencies that allow builders to tinker a little easier!
One of these tools we recommend is Scaffold-eth!
At its core, Scaffold-eth provides an off-the-shelf stack for rapid prototyping on Ethereum, giving developers access to state-of-the-art tools to quickly learn/ship an Ethereum-based dApp.
Using Scaffold-eth and Alchemy, you can easily synthesize and deploy code on the blockchain.
In this tutorial, we'll be using the base code from Challenge #1 of SpeedRunEthereum and work together to build a simple staking dApp.
If you're not familiar with crypto staking, it's best summarized as the process of locking up/depositing crypto holdings into a DeFi protocol or smart contract to earn interest.
Staking crypto has become a cornerstone of many DeFi protocols and allows developers to create complex financial derivative products. While most DeFi staking contracts are extremely complex, we'll be working through one of the most basic ones so that we can learn key concepts. Together, we'll learn the following building blocks for staking:
  1. 1.
    Building with Scaffold-Eth
    • Hacking together frontends
    • Crafting Solidity "backends"
  2. 2.
    Transferring ETH from wallets to smart contracts & vice versa
  3. 3.
    Utilizing Solidity modifers
Let’s start by understanding how Scaffold-Eth works!
If you want to follow along, watch this guided video.

1. Download Scaffold-Eth

In this tutorial, we're going to use the Scaffold-Eth developer environment to craft our smart contracts and put together our frontend UI.
Before jumping in, I want to convey a few important details to keep in mind!
Scaffold-Eth is awesome at abstracting away environment set-ups and frontend dependencies which makes it a powerful tool.
While there are lots of functionalities that are handled by Scaffold-Eth automatically, it is important to dive into the code to understand how some of these features are generated when you have a more solid grasp of the developer environment as a whole.
Let's begin by forking the base code repository from Challenge #1 of SpeedRunEthereum:
1
git clone https://github.com/scaffold-eth/scaffold-eth-challenges.git challenge-1-decentralized-staking
2
3
cd challenge-1-decentralized-staking
4
5
git checkout challenge-1-decentralized-staking
6
7
yarn install
Copied!
If you've followed along successfully, you'll be able to a new folder titled challenge-1-decentralized-staking in your base file directory.
After running the commands above, we're left with a large folder full of files.
Even before moving on to the code, we should familiarize ourselves with where key files are stored in Scaffold-Eth so we know where to focus on.
In this tutorial, we'll be primarily working on Staker.sol and App.jsx.

2. Set Up Your Environment

Next, you'll need three separate terminals up for the three following commands:
Start your React frontend:
1
yarn start
Copied!
Start your Hardhat backend:
1
yarn chain
Copied!
Compile, deploy, and publish all contracts in your packages/contracts file:
1
yarn deploy
Copied!
Whenever you update your contracts, run yarn deploy --reset to "refresh" your contracts in Scaffold-Eth.
Nice! You should now be able to see this repository's UI frontend at http://localhost:3000/

3. Get Familiar with Scaffold-Eth

While I know you're dying to get started with code, only a few more details to take care of!
In our default view, we have two tabs—Staker UI & Debug Contracts.
Go ahead and switch back and forth on your frontend to take a look at the different features.
Staker UI contains all the frontend components we'll be primarily interacting with.
If you click on the provided buttons, you'll notice that most of them aren't quite hooked up yet and you'll immediately run into errors.
Take a look at Staker UI. You'll notice that it's painfully spartan! That's what we'll be primarily flushing out.
Since any on-chain interaction on Ethereum requires testnet ETH, you'll need local testnet ETH to begin hacking away.
First, grab your localhost wallet address.
Click on the "Copy" button on the upper right-hand corner.
Next, head to the lower left-hand corner. You'll be able to access the local faucet here.
  • Either copy/paste in your address in the open field or click on the "Wallet" icon
  • Paste in your address in the expanded view
  • Send yourself some test ETH
After topping your your local wallet, you'll be able to interact with your contracts!
The second tab, Debug Contracts, is another frontend display that contains one of Scaffold-Eth's superpowers!
Once you yarn deploy your contracts and configure it to read the contract data properly, it'll automatically generate a barebones UI allowing you to interact with your contract's functions. For example, in the sample below, we can read and write information via our smart contract by simply dropping in parameters and clicking "Send". With Scaffold-Eth, we don't need to only use CLI commands and have a more intuitive way for prototyping.
If you want to store and view a variable in the Debug Contractstab, make sure to set the variable as public!
Awesome!
Now that we're familiar with Scaffold-Eth, we can dive in deeper into the code.

4. Dive into Solidity

Looking at our Staker.sol file, we see that we have quite an empty Solidity file with a bunch of comments that dictate what needs to be filled out.
Since R2W3 tutorial deviates from Scaffold-Eth's Challenge #1, we can go ahead and ignore the comments and start off with the following code.
1
pragma solidity >=0.6.0 <0.7.0;
2
3
import "hardhat/console.sol";
4
import "./ExampleExternalContract.sol";
5
6
contract Staker {
7
ExampleExternalContract public exampleExternalContract;
8
9
constructor(address exampleExternalContractAddress) public {
10
exampleExternalContract = ExampleExternalContract(exampleExternalContractAddress);
11
}
12
13
}
Copied!

Project Parameters

Before writing out our smart contract code, let's go over how we expect our staking dApp to work!
  1. 1.
    For simplicity, we only expect a single user to interact with our staking dApp
  2. 2.
    We need to be able to deposit and withdraw from the Staker Contract
    • Staking is a single-use action, meaning once we stake we cannot re-stake again
    • Withdraws from the contract removes the entire principal balance and any accrued interest
  3. 3.
    The Staker contract has an interest payout rate of 0.1 ETH for every second that the deposited ETH is eligible for interest accrument
  4. 4.
    Upon contract deployment, the Staker contract should begin 2 timestamp counters. The first deadline should be set to 2 minutes and the second set to 4 minutes
    • The 2-minute deadline dictates the period in which the staking user is able to deposit funds. (Between t=0 minutes and t=2 minutes, the staking user can deposit)
    • All blocks that take place between the deposit of funds to the 2-minute deadline are valid for interest accrual
    • After the 2-minute withdrawal deadline has passed, the staking user is able to withdraw the entire principal balance and any accrued interest until the 4-minute deadline hits
    • After the additional 2-minute window for withdraws has passed, the user is blocked from withdrawing their funds since they timed out.
  5. 5.
    If a staking user has funds left, we have one last function which we can call to "lock" the funds in an external contract that is already pre-installed in our Scaffold-Eth environment, ExampleExternalContract.sol
While the staking parameters listed above may seem a bit convoluted, many real-life staking dApps feature similar primitives where users have a limited period for deposits and withdraws.
And, many dApps will disincentivize "unproductive" capital that is simply sitting around after staking periods have ended.
Sometimes, the DeFi protocol may even absorb the outstanding deposits after a waiting period ends similar to the last parameter we stated in our tutorial.

Solidity Mappings

In our smart contract, we'll need two mappings to help us store some data.
In particular, we need something to keep track of:
  1. 1.
    how much ETH is deposited into the contract
  2. 2.
    the time that the deposit happened
We can achieve this with:
1
mapping(address => uint256) public balances;
2
mapping(address => uint256) public depositTimestamps;
Copied!

Public Variables

In pursuant of the guidelines outlined above, we'll also need a handful of different variables.
1
uint256 public constant rewardRatePerSecond = 0.1 ether;
2
uint256 public withdrawalDeadline = block.timestamp + 120 seconds;
3
uint256 public claimDeadline = block.timestamp + 240 seconds;
4
uint256 public currentBlock = 0;
Copied!
The reward rate sets the interest rate for the disbursement of ETH on the principal amount staked.
The withdrawal and claim deadlines help us set deadlines for the staking mechanics to begin/end.
And, lastly, we have a variable that we use to save the current block.
We use block.timestamp + XXX seconds to create deadlines exactly XXX seconds after our contract is initatiated. It's definitely a bit "naive" as a timing mechanism; can you think of a better way to implement this so its more generalizable for instance?

Events

Even though we will not push events to our frontend, we should still make sure we emit them in key parts of our contract to ensure that we maintain best programming practices.
1
event Stake(address indexed sender, uint256 amount);
2
event Received(address, uint);
3
event Execute(address indexed sender, uint256 amount);
Copied!
Now that we have key parameters/variables locked down, we can move on to craft the specific functions we'll be using in our tutorial.

READ ONLY Time Functions

As stated in the project parameters, many of the different staking dApp's functionalities are subject to "time-locks" which enable/prohibit certain actions are particular points in time. Here we have two different functions that govern the start and end of the withdrawal window.
1
function withdrawalTimeLeft() public view returns (uint256 withdrawalTimeLeft) {
2
if( block.timestamp >= withdrawalDeadline) {
3
return (0);
4
} else {
5
return (withdrawalDeadline - block.timestamp);
6
}
7
}
8
9
function claimPeriodLeft() public view returns (uint256 claimPeriodLeft) {
10
if( block.timestamp >= claimDeadline) {
11
return (0);
12
} else {
13
return (claimDeadline - block.timestamp);
14
}
15
}
Copied!
Both functions are actually very familiar in design.
They both feature a standard if -> else statement.
The conditional simply checks whether the current time is greater than or less than the deadlines dictated in the public variables section.
If the current time is greater than the pre-arranged deadlines, we know that the deadline has passed and we return 0 to signify that a "state change" has occurred.
Otherwise, we simply return the remaining time before the deadline is reached.

Modifiers

For a more in-depth example of a modifier take look at Solidity By Example.
As a gist, Solidity modifiers are pieces of code that can run before and/or after a function call.
While they have many different purposes, one of the most common and basic use cases is for restricting access to certain functions if particular conditions are not fully met. In this tutorial, we'll be precisely using modifiers to help gate key functions that dictate our stake, withdraw, and repatriation functionalities. Here are the three modifiers we use:
1
modifier withdrawalDeadlineReached( bool requireReached ) {
2
uint256 timeRemaining = withdrawalTimeLeft();
3
if( requireReached ) {
4
require(timeRemaining == 0, "Withdrawal period is not reached yet");
5
} else {
6
require(timeRemaining > 0, "Withdrawal period has been reached");
7
}
8
_;
9
}
10
11
modifier claimDeadlineReached( bool requireReached ) {
12
uint256 timeRemaining = claimPeriodLeft();
13
if( requireReached ) {
14
require(timeRemaining == 0, "Claim deadline is not reached yet");
15
} else {
16
require(timeRemaining > 0, "Claim deadline has been reached");
17
}
18
_;
19
}
20
21
modifier notCompleted() {
22
bool completed = exampleExternalContract.completed();
23
require(!completed, "Stake already completed!");
24
_;
25
}
Copied!
The modifiers withdrawalDeadlineReached(bool requireReached) & claimDeadlineReached(bool requireReached) both accept a boolean parameter and check to ensure that their respective deadlines are either true or false.
The modifier notCompleted() operates in a similar fashion but is actually a little bit more complex in nature even though it contains fewer lines of code.
It actually calls on a function completed() from an external contract outside of Staker and checks to see if it's returning true or false to confirm if that flag has been switched.
Now, let's implement the modifiers we just created on the next couple of functions to gate access.

Depositing/Staking Function

In our stake function, we use the modifiers created earlier by setting the params within withdrawalDeadlineReached() to be false and claimDeadlineReached() to be false since we don't want either deadline to have passed yet.
1
// Stake function for a user to stake ETH in our contract
2
3
function stake() public payable withdrawalDeadlineReached(false) claimDeadlineReached(false) {
4
balances[msg.sender] = balances[msg.sender] + msg.value;
5
depositTimestamps[msg.sender] = block.timestamp;
6
emit Stake(msg.sender, msg.value);
7
}
Copied!
The rest of the function is fairly standard in a typical "deposit" scenario where our balance mapping is updated to include the money sent in.
We also set our deposit timestamp with the current time of the deposit so that we access that stored value for interest calculations later.

Withdrawal Function

In our withdrawal function, we again use the modifiers created earlier but this time we wantwithdrawalDeadlineReached() to be true and claimDeadlineReached() to be false.
This set of modifiers/parameters means that we are in the sweet spot for the withdrawal window since its time for the withdrawal to take place without any penalties and we get interest as well.
1
/*
2
Withdraw function for a user to remove their staked ETH inclusive
3
of both the principle balance and any accrued interest
4
*/
5
6
function withdraw() public withdrawalDeadlineReached(true) claimDeadlineReached(false) notCompleted{
7
require(balances[msg.sender] > 0, "You have no balance to withdraw!");
8
uint256 individualBalance = balances[msg.sender];
9
uint256 indBalanceRewards = individualBalance + ((block.timestamp-depositTimestamps[msg.sender])*rewardRatePerBlock);
10
balances[msg.sender] = 0;
11
12
// Transfer all ETH via call! (not transfer) cc: https://solidity-by-example.org/sending-ether
13
(bool sent, bytes memory data) = msg.sender.call{value: indBalanceRewards}("");
14
require(sent, "RIP; withdrawal failed :( ");
15
}
16
Copied!
The rest of the function does a few important steps.
  1. 1.
    It checks to ensure that the person trying to withdraw ETH actually has a non-zero stake.
  2. 2.
    It calculates the amount of ETH owed in interest by taking the number of blocks that passed from deposit to withdrawal and multiplying that by our interest constant.
  3. 3.
    It sets the user's balance staked ETH to 0 so that no double counting can occur.
  4. 4.
    It transfers the ETH from the smart contract back to the user's wallet.

Execute Repatriation Function

Here, we want claimDeadlineReached() to be true since the repatriation of unproductive funds can only happen after the 4-minute mark.
Likewise, we want notCompleted to be true since this dApp is only designed for a single use.
1
/*
2
Allows any user to repatriate "unproductive" funds that are left in the staking contract
3
past the defined withdrawal period
4
*/
5
6
function execute() public claimDeadlineReached(true) notCompleted {
7
uint256 contractBalance = address(this).balance;
8
exampleExternalContract.complete{value: address(this).balance}();
9
}
Copied!
The rest of the function:
  1. 1.
    Grabs the current balance of ETH in the Staker contract
  2. 2.
    Sends the ETH to the repo's exampleExternalContract
If you've followed along with the Solidity so far, your Staker.sol should look like the following:
Staker.sol

5. Foray into the Frontend

Awesome! We just went through a bunch of Solidity. When it comes to frontend displays, Scaffold-Eth tries to keep things nice and simple. It contains a lof of different react components that give users low code solutions for awesome UIs! I encourage you to play around with the different components available to you but, for the meantime, we are going to lean more on the spartan side.
Looking at our App.jsx file, specifically at the code block around link 573, we see a block of code used to capture emitted events from our Solidity contracts and display them as a list.
Effectively, it allows us to log the different actions fired off from our smart contracts, parse the information stored, and then visually allow dApp users to look at their on-chain history. While we will practice good programming standards by emitting events in our Solidity contract, this time, we'll be ignoring events in our frontend for simplicity so let's remove that code block entirely.
If you look at your Staker UI tab, you'll notice that the events box has been erased.

Frontend Edits

For the most part, a lot of the frontend code will remain the same as the default! In the previous step, we already removed the events react component.
Our final goal will be to have a nice, simple UI that looks like the following:
Note that the default frontend is missing a few of the visual elements from the default repo.

Reward Rate Per Second

To construct this part, we insert the following piece of code right under the Staker contract block:
1
<div style={{ padding: 8, marginTop: 16 }}>
2
<div>Reward Rate Per Second:</div>
3
<Balance balance={rewardRatePerSecond} fontSize={64} /> ETH
4
</div>
Copied!
Here, we take advantage of Scaffold-Eth's balance react component to help with formatting!

Deadline UI Elements

To construct the deadline visual component, we use the following 2 snippets of code:
1
// ** keep track of a variable from the contract in the local React state:
2
const claimPeriodLeft = useContractReader(readContracts, "Staker", "claimPeriodLeft");
3
console.log("⏳ Claim Period Left:", claimPeriodLeft);
4
5
const withdrawalTimeLeft = useContractReader(readContracts, "Staker", "withdrawalTimeLeft");
6
console.log("⏳ Withdrawal Time Left:", withdrawalTimeLeft);
Copied!
1
<div style={{ padding: 8, marginTop: 16, fontWeight: "bold" }}>
2
<div>Claim Period Left:</div>
3
{claimPeriodLeft && humanizeDuration(claimPeriodLeft.toNumber() * 1000)}
4
</div>
5
6
<div style={{ padding: 8, marginTop: 16, fontWeight: "bold"}}>
7
<div>Withdrawal Period Left:</div>
8
{withdrawalTimeLeft && humanizeDuration(withdrawalTimeLeft.toNumber() * 1000)}
9
</div>
Copied!
While the second code snippet is familiar and resembles what we have already seen, the first block of code is a bit unique. Not that we call claimPeriodLeft and withdrawalTimeLeft to access the stored variable values in the frontend.
However, we must actually read the values themselves first from the smart contract. The first code snippet handles this logic!

Misc. Edits to Other Existing UI components

Now that you've seen 2 different examples of frontend UI components, can you figure out how to make the remaining changes to the frontend so that it'll look like the sample provided above!
You should only need to adjust a few parameters here so don't think too much!

Freestyling on the UI

While Scaffold-Eth has a lot of default components that empower users to simply leverage "low-code" solutions, it also gives users access larger frontend libraries.
By default, it has a hook into Ant Design react components (https://ant.design/components/overview/) which allows anyone to pull components from there!
In our sample frontend, we actually see "lines" dividing each large block of visual components.
Recreate these lines by exploring the different options available in Ant Design
If you want a hint, take a look at Ant Dividers! Don't forget that we need to import the component we plan on using at the top of our App.jsx file!
import { Alert, Button, Col, Menu, Row, List, Divider } from "antd";
If you've followed along with the frontend code, your App.jsx should look like the following:
App.jsx
Awesome!
We've worked through a lot of new components together both in terms of developer environments, Solidity, and frontend code.
Verify that your dApp functions as expected!
  1. 1.
    Does the dApp feature single-use staking?
  2. 2.
    Are the withdrawal/fund repatriation conditions respected? Go ahead of hit yarn deploy --reset a few times to check each window of time.

Challenge Time!

Okay, now time for the best part. I'm going to leave you with a few extension challenges to try on your own, to see if you fully understand what you've learned here!
  1. 1.
    Update the interest mechanism in the Staker.sol contract so that you receive a "non-linear" amount of ETH based on the blocks between deposit and withdrawal
I suggest implementing a basic exponential function!
2. Allow users to deposit any arbitrary amount of ETH into the smart contract, not just 0.5 ETH.
3. Instead of using the vanilla ExampleExternalContract contract, implement a function in Staker.sol that allows you to retrieve the ETH locked up in ExampleExternalContract and re-deposit it back into the Staker contract.
  • Make sure to only "white-list" a single address to call this new function to gate its usage!
  • Make sure that you create logic/remove existing code to ensure that users are able to interact with the Staker contract over and over again! We want to be able to ping-pong from Staker -> ExampleExternalContract repeatedly!
Once you're done with the challenge, tweet about it by tagging @AlchemyPlatform on Twitter and the author @crypt0zeke using the hashtag #roadtoweb3!
Peace & love. Happy building! 🏗️🚀