In this blog post, I’m going to cover the first scaffold-eth speed run project: creating a Staking dApp. If you want to know more about scaffold-eth and my current journey in the web3 world you should read my previous article: My journey in Web3 development: scaffold-eth.
The goal of the dApp
The end goal of the project is to mimic the Ethereum 2.0 staking contract. The requirements are pretty simple:
- allow anyone to stack ether and track their balance
- if a time and stack amount deadline have reached don’t allow users to withdraw their fund (those found are used for a future project, like the Ethereum PoS)
What are you going to learn?
- Setup the scaffold-eth project
- Write a Staking Contract
- Call an external Contract
- Create unit test for your Solidity Contract
- Use and test your Contract with a React app on your local machine
- Deploy the staking contract on Ethereum Test Net!
Maybe it’s not so much but you can see this as the first stepping stone of your (and mine) journey.
Some always useful links that you should always have in mind:
- Solidity by Example
- Solidity Documentation
- Hardhat Documentation
- Ethers-js Documentation
- OpenZeppelin Documentation
- OpenZeppelin Ethernaut tutorial
- CryptoZombies Tutorial
Setup the project
First of all, we need to set up it. Clone the scaffold-eth repository, switch to the challenge 1 branch and install all the needed dependencies.
git clone https://github.com/austintgriffith/scaffold-eth.git challenge-1-decentralized-staking
git checkout challenge-1-decentralized-staking
Overview of the available CLI commands
These commands are not specific for this challenge but are in common with every scaffold-eth project!
This command will launch your local hardhat network and configure it to run on http://localhost:8545
This command will launch your local react website on http://localhost:3000
So, open three different Terminals and launch those commands. Every time you make a change to your contracts you just need to re-launch your deploy command.
Exercise Part 1: Implement the stake() method
In this part of the exercise, we want to allow users to stake some ETH in our contract and track their balances.
Important Concepts to master
- Payable methods — when a function is declared as payable it means that allows users to send ETH to it.
- Mapping — it’s one of the variable types supported by Solidity. It allows you to associate a key with a value.
- Events — events allow the contract to notify other entities (contracts, web3 applications, etc) that something has happened. When you declare an event you can specify at max 3 indexed parameters. When a parameter is declared as indexed it allows 3rd-party apps to filter events for that specific parameter.
- Declare a mapping to track balances
- Declare constant threshold of 1 ether
- Declare a Stake event that will log the staker address and staking amount
- Implement a payable
stake()function that will update the staker’s balance
Contract code updated
uint256are the same (it’s just an alias)
- when a variable is declared public, Solidity will automagically create a getter method for you. This means that it will expose a
yourVariableName()method to be called
- when you declare a variable without initializing it, it will be initialized to its default value based on the variable type
- Solidity exposes some utility units like wei, ethers, or time units.
Let’s review it:
- We have declared our balances that will track for each user’s address his stacked balance
- We have declared our threshold
- We have declared our Stake event that will notify the blockchain that a user has stacked an amount
- We have implemented our Stake function as a public payable method that will update the user’s balance and will emit the Stake event.
One thing that could be strange is that we are simply updating the value without initializing the default value of
balances[msg.sender]. That’s possible because when a variable is not initialized it will be created with its type default value. In this case (uint256) it will be 0.
Now, deploy the contract, get some funds from the Faucet and try to stake some ETH to the contract.
- Can you get some funds from the Faucet?
- Can you send 0.5 ETH to the Contract by clicking the Stake button?
- Is the event triggered on the UI?
- Is your staking balance updated?
- Is the contract’s balance updated?
If you have checked all these marks we can continue to part 2 of the exercise.
Exercise Part 2: Implement the lock mechanism and withdraw
As we previously said the final goal of this Contract is to create a Staking dApp that will allow public users to stack some ETH if some conditions are met. If those conditions are not met they will be able to withdraw their funds.
These conditions are:
- At least 1 ETH needs to be stacked on the Staker Contract
- The 1 ETH stack threshold is reached within a time deadline of 30 seconds
Important Concepts to master
- Call external contract — every contract on the blockchain is like a public REST API. You can call them from your web3 app or directly from another contract if they are declared as
- Function Modifier — Modifiers are code that can be run before and/or after a function call. They can be used to restrict access, validate inputs, or guard against reentrancy hacks.
- Error handling — Error handling is important because allows you to revert the state (to be precise to not apply) of the smart contract. You can think at the revert like a database
rollback. Errors also allow you to notify the user of the reason for the reversion.
- Sending Ether (transfer, send, call) — Solidity has native methods to transfer ETH from a contract to another contract/user address. TLDR: use call ;)
- Declare a 30 seconds deadline from the deploy time of the Contract
- Create a public
timeLeft()function that will return the amount of time remaining until the deadline is reached
- Create a modifier that will check if the external contract is completed
- Create a modifier that will check dynamically (with a parameter) if the deadline is reached
- Allow users to stake ETH only if the deadline is not reached yet and we have not executed the external contract
- Allow users to withdraw funds only if the deadline is not reached with the balance threshold
- Create an execute() method that will transfer funds from the Staker contract to the external contract and execute an external function from another Contract
An important note to remember when you are testing your Contract locally. The blockchain state is only updated when blocks are mined. The block number and block timestamp are updated only when a transaction is done. This means that the timeLeft() will be updated only after a transaction. If you want to simulate a “real-life” experience you could change the hardhat config to simulate block auto-mine. Check out their mining-mode documentation if you want to learn more.
Contract code updated
Why is the code different between the one from the original challenge?
- I think that the variable
openForWithdrawit’s unnecessary in this case. Withdraw can be enabled directly from the status of the Staker Contract and External Contract
- In this case our
withdrawfunction is not taking an external address for simplification. You will be the only one to be able to withdraw!
- We have updated both Solidity to version
0.8.4and Hardhat to version
2.6.1. Some scaffold-eth (like this one) could still rely on an old version of Solidity and I think that’s important to use the most updated one for security, optimization, and feature-complete reasons.
Let’s review some code
Function Modifiers: First of all you can see that we have created two modifiers. As you have already learned from Solidity by Example, function modifiers are code that can be run before and / or after a function call. In our case we have even added parametric function modifier!
When you have defined a function modifier you can use them appending the name of the modifier after the function name. If the modifier rever, the function is reverted before even running it!
stake() function: is the same one as before
timeLeft() function: it’s pretty easy, we use the
block.timestamp value to calculate the seconds remaining before the deadline.
withdraw() function: after our modifiers flag pass, we check if the user has balance otherwise we revert. To prevent from re-entrancy attack you should always modify the state of your contract before any call. That’s why we are saving user’s balance in a variable and we update the user’s balance to 0.
execute() function: after our modifiers flag pass we call the external contract
complete() function and we check if everything is successful.
Now deploy the updated contract with
yarn deploy and test it locally.
- Do you see the timeLeft changing as soon as you make a transaction?
- Can you Stake ETH after the deadline?
- Can you withdraw before the deadline or after the deadline if the contract is executed?
- Can you execute the contract even if the threshold is not met?
- Can you execute the contract more than once?
Exercise Part 3: Test Coverage
I know I know, you would like to just deploy your Contract and frontend and start testing it right now on your test-net of choice but… we need to be sure that everything works as expected without monkey-clicking on the UI!
So in the next part of the post, I’m going to cover something that every developer should do: cover your Contract logic with tests!
Waffle is a library for writing and testing smart contracts that work with ethers-js like a charm.
To test our contract we will use Chai matchers that will verify that the conditions we are expecting have been met.
After you have written all your test you just need to type
yarn test and all your tests will be automatically run against your contract.
I’m not gonna explain how to use the library (you can simply take a look at the code below to have an overview), I will be more focused on the “what should we test”.
We have implemented our Smart Contract with some logic:
- we are tracking user balance in
mapping(address => uint256) public balances
- we have a minimum
uint256 public constant threshold = 1 ether
- we have a maximum
uint256 public deadline = block.timestamp + 120 seconds
- user can call the
stake()function if the external contract is not
deadlineis not reached yet
- user can call the
executemethod if the external contract is not
deadlineis not reached yet
- user can withdraw their funds if
deadlinehas been reached and the external contract is not
timeLeft()is returning the remaining seconds until
deadlineis reached, after that it should always return
What you should cover in your test
PS: this is my personal approach to testing, if you have suggestions, hit me up on Twitter!
When I write tests what I have in mind is to take an individual function and cover all the edge cases. Try to write your tests answering these questions:
- Have I covered all the edge cases?
- Is the function reverting when expected?
- Is the function emitting the needed events?
- With a specific input, will the function produce the expected output? Will the new state of the Contract be shaped as we expect?
- Will the function returns (if it returns something) what we are expecting?
How to simulate blockchain mining in your test
Remember when we said that to correctly simulate
timeLeft() we had to create transactions or just ask for funds from the Faucet (that’s a transaction as well)? Well, to solve this issue on our test I have implemented a little utility (that you can simply copy/paste in other projects) that do the same thing:
When you call
increaseWorldTimeInSeconds(10, true) it will increase the EVM internal timestamp 10 seconds ahead of the current time. After that, if you specify it, it will also mine a block to create a transaction.
The next time that your contract will be called the
block.timestamp used in
timeLeft() should be updated.
Testing the execute() function
Let’s review one test and then I’ll post the entire code explaining only some specific code. The code about is covering the
execute() function of our code
- The first test check that if the
execute()function is called when the threshold is not reached it will revert the transaction with the correct error message
- The second test is calling two consecutive times the
execute()function. The staking process is already done and the transaction should be reverted, preventing doing it again.
- The third test is trying to call the
execute()function after the time deadline. The transaction should revert because you can call the
execute()function only before the deadline is reached.
- The last test is testing that if all the requirements are met the
execute()function does not revert and the desired out is reached. After the function call the external contract
completedvariable should be
true, the External Contract
balanceshould be equal to the users staked amount and our contract balance should be equal to
0(we have transferred all the balance to the external contract).
If everything goes as expected, running
yarn test should give you this output
Test coverage complete code
Here we go with the whole test coverage code
Have you noticed that the test code coverage is far bigger than the Contract itself? That’s what we want to see! Test all the things!
Final step: deploy your Contract to the moon (testnet)
Ok, now it’s time. We have implemented our Smart Contract, we have tested the frontend UI, we have covered every edge case with our tests. We are ready to deploy it on the testnet.
Following the scaffold-eth documentation, these are the steps we need to follow:
- Change the
packages/hardhat/hardhat.config.jsto the testnet you would like to use (in my case rinkeby)
- Updated the
infuriaProjectIdwith one created on Infuria
- Generate a deployer account
with yarn generate. This command should generate two
.txtfile. One that will represent the account address and one with the seed phrase of the generated account.
yarn accountto see details of the account like eth balances across different networks.
- Make sure that the mnemonic.txt and relative account files are not pushed with your git repository, otherwise, anyone could get ownership of your Contract!
- Fund your deployer account with some funds. You can use an instant wallet to send funds to the QR code you just saw on your console.
- Deploy your contract with
If everything goes well you should see something like this on your console
yarn run v1.22.10
$ yarn workspace @scaffold-eth/hardhat deploy
$ hardhat run scripts/deploy.js && hardhat run scripts/publish.js📡 Deploying...🛰 Deploying: ExampleExternalContract
📄 ExampleExternalContract deployed to: 0x96918Bd0EeAF5BBe10deD67f796ef44b2f5cb2A3
🛰 Deploying: Staker
📄 Staker deployed to: 0x96918Bd0EeAF5BBe10deD67f796ef44b2f5cb2A3
💾 Artifacts (address, abi, and args) saved to: packages/hardhat/artifacts/
💽 Publishing ExampleExternalContract to ../react-app/src/contracts
📠 Published ExampleExternalContract to the frontend.
💽 Publishing Staker to ../react-app/src/contracts
📠 Published Staker to the frontend.
✨ Done in 11.09s.
Deployment metadata is stored in the
/deploymentsfolder, and automatically copied to
--export-allflag in the
yarn deploycommand (see
If you want to check the deployed contract you can search for them on the Etherscan Rinkeby site:
Update your frontend app and deploy it on Surge!
We are going to use the Surge method but you could also deploy your app on AWS S3 or on IPFS, that’s up to you!
In the near future I will try also to add some basic methods to also deploy it to Vercel both manually (via CLI) and via GitHub Actions CI/CD.
The scaffold-eth documentations always come in hand but I will summarize what you should do:
- If you are deploying on mainnet you should verify your contract on Etherscan. This procedure will add credibility and trust to your application. If you are interested in doing so just follow this guide for scaffold-eth.
- Turn off Debug Mode (it prints an awful lot of console.log, something that you don’t want to see in Chrome Developer Console, trust me!). Open
const DEBUG = true;and turn it to
- Take a look at
App.jsxand remove all unused code, just be sure to ship only what you really need!
- Make sure that your React app is pointing to the correct network (the one you just used to deploy your Contract). Look for
const targetNetwork = NETWORKS[“localhost”];and replace
localhostwith the network of your contract. In our case, it will be
- Make sure you are using your own nodes and not the ones in Scaffold-eth as they are public and there’s no guarantee they will be taken down or rate limited. Review lines 58 and 59 of
constants.jsand swap Infura, Etherscan, and Blocknative API Keys if you want to use their services.
Are we ready? Let’s go!
Now build your React App with
yarn build and when the build script has finished deploy it to Surge with
If everything goes well you should see something like this. Your dApp is now live on Surge!
You can check out our deployed dApp here: https://woozy-cable.surge.sh/
Recap and Conclusions
That’s what we have learned and done so far
- Clone scaffold-eth challenge repo
- Learned a couple of fundamental concepts (remember to keep reading Solidity by Example, Hardhat documentation, Solidity documentation, Waffle documentation)
- Create a Smart Contract from zero
- Create a full Test Suite for our Contract
- Tested our Contract locally on hardhat network
- Deployed our contract on Rinkeby
- Deployed our dApp on Surge
If everything works as expected, you are ready to make the big jump and deploy everything on Ethereum main net!
GitHub Repo for this project: scaffold-eth-challenge-1-decentralized-staking