Yield Farming Tutorial — Part 1

Smart Contracts Using Solidity and Hardhat

Andrew Fleming
Coinmonks
Published in
8 min readJun 6, 2021

--

Introduction

In part one of this tutorial, we will build a yield farming decentralized application using the Hardhat development environment and Solidity. If you scour DuckDuckGo, you will find quite a few yield farming tutorials; however, I have neither found yield farming tutorials that utilize Hardhat and Ethers nor have I discovered any tutorials explaining how to create an automated yield calculator. Generally, these yield farms necessitate an owner running a script to deliver yield to the dApp’s users. This article aims to rectify that and provide you with the tools to create something amazing. I do recommend having some experience with Solidity in order to get the most out of this tutorial.

First, what is a yield farm? The whole idea of yield farming consists of incentivizing users with passive income in exchange for providing liquidity. To actually farm, in my opinion, requires that the user provides their earned yield into another liquidity pool; whereby, they receive passive income upon their passive income. This process can, of course, keep going to where the user receives passive income upon passive income ad nauseam.

Considering the above definition, we won’t technically be building a “farming” protocol; rather, we’re building the first requisite building block of a farming protocol. Once we understand the fundamentals, we can truly start playing with the DeFi money legos.

Let’s get into it.

Environment Setup/Dependencies

Let’s start with opening up your code editor and creating a new directory. For this project, I named mine pmkn-farm (yes, I’m farming PMKN). Make sure you have Node installed (or Yarn, if you prefer).

In the code editor terminal (I’m using a Mac), cd into your farm directory. Then, install the following dependencies (following Hardhat’s TypeScript configuration with some additions):

npm i --save-dev hardhat

Open up Hardhat with npx hardhat

Scroll one item down to Create an empty hardhat.config.js

Next, we need to install dependencies for TypeScript. Run the following:

npm i --save-dev ts-node typescript

And for testing:

npm i --save-dev chai @types/node @types/mocha @types/chai

Next, we’ll be using an ERC20 token as both the staking token and as the yield rewarded to users. OpenZeppelin hosts numerous libraries out of convenience for developers. They also offer excellent testing tools. During testing, we’ll need to simulate the passing of time. Let’s grab everything here:

npm i --save-dev @openzeppelin/contracts @openzeppelin/test-helpers

We’ll also need this for OpenZeppelin’s time.increase() function:

npm i --save-dev @nomiclabs/hardhat-web3 @nomiclabs/hardhat-waffle

Next, if you plan on posting your work on GitHub or anywhere else outside of your local environment, you’ll need dotenv:

npm i --save-dev dotenv

Change the hardhat.config to TypeScript:

mv hardhat.config.js hardhat.config.ts

Finally, we’ll change the Solidity version and reformat the hardhat-waffle import and include the hardhat-web3 import in the hardhat.config.ts:

Contracts

1. ERC20 PmknToken Contract

Because I like Pumpkins, this tutorial will reward users with PmknTokens. Feel free to change the name to whatever you wish.

You should still be in the root of the directory, and in your terminal:

mkdir contracts

touch contracts/PmknToken.sol

We’re going to make our ERC20 token contract first. Let’s import the ERC20 contract from OpenZeppelin while also importing OpenZeppelin’s Ownable.sol contract. You can look at these contracts for yourself in node_modules. After declaring the imports, we’re going to build two wrappers for the functions mint() and transferOwnership(). The purpose for these wrappers consists of controlling who can call these functions (hence, the onlyOwner modifier). The mint function allocates a specified number of tokens to a specified user address. Since we want to automate this process, we’re also including the transferOwnership() function to transfer ownership to the farm contract; therefore, only the contract itself can issue tokens.

2. PmknFarm Contract

touch contracts/PmknFarm.sol

In your PmknFarm contract, let’s build the skeleton of the project. We’re building a yield farming dApp; therefore, we’re going to need a function that allows users to stake their funds. We’re also going to need a function to unstake their funds. Further, users will want to withdraw their yield. So, three core functions. Import both the PmknToken contract and OpenZeppelin’s IERC20 contract. We also need to declare some state variable mappings and events for the front end. We’ll go over each aspect of the contract. First, let’s go over the constructor, state variables, and events.

The startTime and pmknBalance mappings may require a little explanation to better understand how they’ll be used in our functions. The startTime will keep track of the timestamp for the user’s address in order to track the user’s unrealized yield. The pmknBalance will point to the realized, or the stored amount waiting to be minted, PmknToken yield (not to be confused with actually minted PmknToken) associated with the user’s address. If you’re unfamiliar with mappings, they’re simply key/value pairs. For a more in-depth explanation, I encourage you to read the Solidity docs.

I always declare a name variable for testing; however, this is not required.

These state variable declarations precede with the type (ie IERC20, PmknToken) and visibility (public).

To avoid confusion, I encourage following this convention:

  • type => PascalCasing
  • state declaration => camelCasing
  • constructor parameter => _underscoreCamelCasing

When I first started working with Solidity, it took some time for me to understand what exactly was going on with utilizing ERC20 tokens. This is how I wish the concept was explained to me: The IERC20 and PmknToken consist of types; as in, the imported token type. The state variable declaration consists of the contract’s instance of the token type. Finally, the constructor’s parameter points to the address that fully creates a contract instance of the imported token.

The constructor, for those unfamiliar, is a function used once and only once during the deployment of the contract. A common use-case for a constructor includes setting constant addresses (just as we’re doing here). In order to deploy this contract, the user must input addresses for _daiToken and _pmknToken.

Onward to the heart of this article.

Core Functions

The stake() function first requires that the amount parameter is greater than 0 and that the user holds enough DAI to cover the transaction. The conditional if statement checks if the user already staked DAI. If so, the contract adds the unrealized yield to the pmknBalance. This ensures the accrued yield does not disappear. After, the contract calls the IERC20 transferFrom function. The user first must approve the contract’s request to move their funds. Thereafter, the user must sign the actual transaction. The function updates the stakingBalance, startTime, and isStaking mappings. Finally, it emits the Stake event to allow our frontend to easily listen for said event.

The unstake() function requires the isStaking mapping to equate to true (which only happens when the stake function is called) and requires that the requested amount to unstake isn’t greater than the user’s staked balance. I declared a local toTransfer variable equal to the calculateYieldTotal function (more on this function later) in order to ease my tests (the latency gave me problems in checking balances). Thereafter, we follow the checks-effects-transactions pattern by setting balanceTransfer to equal the amount and then setting the amount to 0. This prevents users from abusing the function with re-entrancy.

Further, the logic updates the stakingBalance mapping and transfers the DAI back to the user. Next, the logic updates the pmknBalance mapping. This mapping constitutes the user’s unrealized yield; therefore, if the user already held an unrealized yield balance, the new balance includes the prior balance with the current balance (again, more on this in the calculateYieldTotal section). Finally, we include a conditional statement that checks whether the user still holds staking funds. If the user does not, the isStaking mapping points to false.

*I should also note that Solidity version >= 0.8.0 includes SafeMath already integrated. If you’re using Solidity < 0.8.0, I highly encourage you to use a SafeMath library to prevent overflows.

**The original unstake() function failed to reset the startTime mapping. The code above reflects the bug fix.

The withdrawYield() function requires that either the calculateYieldTotal function or the pmknBalance holds a balance for the user. The if conditional statement checks the pmknBalance specifically. If this mapping points to a balance, this means that the user staked DAI more than once. The contract logic adds the old pmknBalance to the running yield total we received from the calculateYieldTotal. Notice that the logics follows the checks-effects-transactions pattern; where, oldBalance grabs the pmknBalance uint. Immediately thereafter, pmknBalance is assigned zero (again, to prevent re-entrancy). After, the startTime is assigned to the current timestamp in order to reset the accruing yield. Finally, the contract evokes the pmknToken.mint function which transfers PMKN directly to the user.

Helper Functions

The calculateYieldTime() function simply subtracts the startTime timestamp from the specified user’s address by the current timestamp. This function acts more like a helper function’s helper. The visibility for this function should be internal; however, I chose to give the public visibility for testing.

The calculateYieldTotal() function allows the automated staking process to occur. First, the logic takes the return value from the calculateYieldTime function and multiplies it by 10¹⁸. This proves necessary as Solidity does not handle floating point or fractional numbers. By turning the returned timestamp difference into a BigNumber, Solidity can provide much more precision. The rate variable equates to 86,400 which equals the number of seconds in a single day. The idea being: the user receives 100% of their staked DAI every 24 hours.

*In a more traditional yield farm, the rate is determined by the user’s percentage of the pool instead of time.

Further, the BigNumber time variable is divided by the hardcoded rate (86400). The function takes the quotient and multiplies it by the DAI staking balance of the user which is then divided by 10¹⁸. When the frontend fetches the raw yield, it must divide by 10¹⁸ again to display the actual yield.

Conclusion

Here’s the final contract:

This concludes the contracts portion of the yield farming dApp. If you have any questions, feel free to reach out. I hope this helps you on your Solidity journey. Thank you so much for reading!

Part 2 : Testing Smart Contracts Using Hardhat and Chai

*To see the full repo with tests, scripts, and the frontend, here’s the repo: https://github.com/andrew-fleming/pmkn-farm

*Tips are greatly appreciated!
ETH address: 0xD300fAeD55AE89229f7d725e0D710551927b5B15

Join Coinmonks Telegram Channel and learn about crypto trading and investing

Also, Read

--

--

Andrew Fleming
Coinmonks

Writer, programmer, boating accident enthusiast