- We will follow this tutorial.
In smartContracts/
directory initialize a new truffle
project.
truffle init
Initialize a package.json
file and install the following dependencies:
# Initialize a `package.json`.
yarn init -y
# Install
# 1. @openzeppelin/contracts for creating ERC20 tokens with ease.
# 2. @truffle/hdwallet-provider for connecting with Ethereum accounts from CLI.
# 3. dotenv for environment variables.
yarn add @openzeppelin/contracts @truffle/hdwallet-provider dotenv
Edit your truffle-config.js
file as follows. Remember to change Solidity
compiler version to the latest version. Here, we are using some environment variables. Let's define them next.
require("dotenv").config({ path: "./.env" });
const HDWalletProvider = require("@truffle/hdwallet-provider");
const MNEMONIC = process.env.MNEMONIC;
const INFURA_PROJECT_ID = process.env.INFURA_PROJECT_ID;
const RPC_ENDPOINT = `https://ropsten.infura.io/v3/${INFURA_PROJECT_ID}`;
module.exports = {
// Configure your compilers
compilers: {
solc: {
version: "0.8.7",
},
},
// contracts_directory: "./contracts/",
// contracts_build_directory: "../react-app/src/contracts/abi/",
networks: {
development: {
host: "127.0.0.1",
port: 7545,
network_id: "*", // Any network
},
ropsten: {
provider: () => new HDWalletProvider(MNEMONIC, RPC_ENDPOINT),
network_id: 3,
gas: 5500000,
gasPrice: 10e9,
confirmations: 2,
timeoutBlocks: 200,
skipDryRun: true,
},
},
};
Create a .env
file. Add environment variables in it as shown in .env.example
.
MNEMONIC = REPLACE_WITH_YOUR_METAMASK_MNEMONIC
INFURA_PROJECT_ID = REPLACE_WITH_YOUR_INFURA_PROJECT_ID
In smartContracts/contracts/
directory, create MyToken.sol
. MyToken
is a simple ERC20 Smart Contract/Token. Note that we are inheriting MyToken
from the ERC20
smart contract defined in @openzeppelin library. In the constructor, we are giving our token the name MyToken
and the symbol MTKN
. Also, we are minting and sending 1 million MTKN
to the account which is used to deploy this smart contract.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor() ERC20("MyToken", "MTKN") {
// Mint 1 million MyToken tokens.
_mint(msg.sender, 1_000_000e18);
}
}
In smartContracts/contracts/
directory, create FarmToken.sol
. Here, we are creating our FarmToken
smart contract that also inherits from ERC20
contract from @openzeppelin
. We are giving our token the name of FarmToken
and the symbol of FRM
. We have also declared (and initialized in the constructor) a state variable to interact with MyToken
contract. This time, we are minting no tokens at build time.
We have some functions here as well.
- With
deposit
, a user can deposit a certain amount ofMTKN
tokens toFarmToken
contract. In return,FarmToken
contract mints and sends and equal amount ofFRM
tokens to the user's address. - With
withDraw
, a user can return a certain amoutn ofFRM
tokens back toFarmToken
contract and get an equal amount ofMTKN
fromFarmToken
contract.FarmToken
contract, then, burns theFRM
tokens returned by the user. - With
balance
, anyone can check the amount ofMTKN
tokens currently in possession ofFarmToken
contract.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract FarmToken is ERC20 {
using Address for address;
using SafeERC20 for IERC20;
IERC20 public token;
constructor(address _token) ERC20("FarmToken", "FRM") {
token = IERC20(_token);
}
function balance() public view returns(uint256) {
return token.balanceOf(address(this));
}
function deposit(uint256 amount) public returns(bool) {
require(amount > 0, "Deposit amount cannot be zero.");
token.safeTransferFrom(msg.sender, address(this), amount);
_mint(msg.sender, amount);
return true;
}
function withDraw(uint256 amount) public returns(bool) {
_burn(msg.sender, amount);
token.safeTransfer(msg.sender, amount);
return true;
}
}
Compile these contracts with the solidity compiler.
truffle compile
In smartContracts/migrations/1_initial_migration.js
file, write the following script to deploy the smart contracts. Note that after deploying MyToken
contract, we are passing its address to FarmToken
's constructor as an argument.
const MyToken = artifacts.require("MyToken");
const FarmToken = artifacts.require("FarmToken");
module.exports = async function (deployer, network, accounts) {
await deployer.deploy(MyToken, { from: accounts[0] });
const myToken = await MyToken.deployed();
await deployer.deploy(FarmToken, myToken.address, { from: accounts[0] });
};
Run Ganache
and deploy the smart contracts with the following command:
# If you don't pass any network argument, truffle will by default try to deploy
# to development network (localhost).
truffle migrate --reset --network development
In smartContracts/scripts/
directory, create getMyTokenBalance.js
, transferMyTokenToFarmToken.js
and withdrawMyTokenFromTokenFarm.js
scripts to interact with the deployed networks. Go through these scripts; the comments in the files are self explanatory. Run the scripts with these commands.
truffle exec scripts/getMyTokenBalance.js
truffle exec scripts/transferMyTokenToFarmToken.js
truffle exec scripts/withdrawMyTokenFromTokenFarm.js
In order to deploy to Ropsten through our MetaMask account, we will use our 12 word long MetaMask mnemonic and @truffle/hdwallet-provider
package. We, also, need an Infura project id to create our RPC endpoint. Save the mnemonic and the Infura project id in .env
. We have also covered these things in earlier steps. Run the following commands to deploy MyToken
and FarmToken
contracts to the Ropsten Public Testnet.
Note: Remember to copy the addresses to which our contracts are deployed. We will use them later.
truffle migrate --reset --network ropsten
In this step, we will verify our smart contract source code on EtherScan. So, it will be publicly available for auditing. For this, install truffle-plugin-verify
package.
yarn add -D truffle-plugin-verify
Now, create an account on EtherScan, and create an API key. Save this key in .env
file with other environment variables.
ETHERSCAN_API_KEY = REPLACE_WITH_YOUR_ETHERSCAN_API_KEY
In truffle-config.js
file, add these options.
module.exports = {
// ... other options
plugins: ["truffle-plugin-verify"],
api_keys: {
etherscan: process.env.ETHERSCAN_API_KEY,
},
};
Now, you can verify the source code with this single command.
npx truffle run verify MyToken FarmToken --network ropsten
You can read more on this:
In reactApp/
directory, initialize a boiler-plate React App with TypeScript template.
npx create-react-app . --template typescript
We will use TypeChain for generating TypeScript types for our smart contracts.
yarn add -D typechain @typechain/web3-v1 @types/bn.js
Add these scripts in react-app/package.json
.
"scripts": {
"generate-types": "typechain --target=web3-v1 --out-dir=\"src/contracts/types\" \"src/contracts/abi/*.json\"",
"postinstall": "yarn generate-types"
}
Go to the following links and copy the ABIs of your MyToken
and FarmToken
contracts. Save them in react-app/src/contracts/abi/MyToken.json
and react-app/src/contracts/abi/FarmToken.json
.
https://ropsten.etherscan.io/address/YOUR_MY_TOKEN_ADDRESS#code
https://ropsten.etherscan.io/address/YOUR_FARM_TOKEN_ADDRESS#code
Note: If you don't verify your smart contract source code on EtherScan, you won't be able to get the ABIs from there!
Now, generate TypeScript types for your smart contracts in react-app/src/contracts/types/
directory with the following command:
yarn generate-types
In react-app/
directory, install @web3-react.
# 1. @web3-react/core is the core @web3-react library.
# 2. @web3-react/injected-connector is for connecting with wallets that are
# installed as an extension in browsers like MetaMask.
# 2. web3 for interactions with ethereum.
yarn add @web3-react/core @web3-react/injected-connector web3
In react-app/src/components/wallet/index.ts
paste the following code:
import { InjectedConnector } from "@web3-react/injected-connector";
export const injector = new InjectedConnector({
supportedChainIds: [
// 1, // Mainnet
3, // Ropsten
4, // Rinkeby
5, // Goerli
42, // Kovan
],
});
InjectedConnector
helps us connect to Wallets which are installed as an extention in browser like MetaMask.
Now, in react-app/src/components/web3ReactProvider/index.tsx
file, create a Web3ReactProvider
component.
import { FC } from "react";
import { Web3ReactProvider } from "@web3-react/core";
import Web3 from "web3";
const getLibrary = (provider: any) => {
return new Web3(provider);
};
export const Web3React: FC = ({ children }) => (
<Web3ReactProvider getLibrary={getLibrary}>{children}</Web3ReactProvider>
);
Now, use this provider in react-app/src/index.tsx
to wrap our main App
component.
import React from "react";
import ReactDOM from "react-dom";
import { Web3React } from "./components/web3ReactProvider";
import App from "./components/app";
import "./index.css";
ReactDOM.render(
<React.StrictMode>
<Web3React>
<App />
</Web3React>
</React.StrictMode>,
document.getElementById("root")
);
Now, with the help of useWeb3React
hook, we can use web3
and connect to MetaMask
in an abstract way.
const { activate, deactivate, active, library } = useWeb3React();
I have created all the UI in App
component and moved all the functions and state management to a context. Feel free to implement the client side as you like.
In react-app/src/context/appContextProvider/index.tsx
, I have used useWeb3React
hook to create some functions to interact with our MyToken
and FarmToken
smart contracts. Read the comments in the file to understand what's going on. Some important functions are the following:
// Connect to MetaMask.
function connect();
// Disconnect from MetaMask.
function disconnect();
// Allow `FarmToken` contract to spend 'MTKN' tokens from user's address.
function allowFarm(tokensToAllow: number);
// Deposit `MTKN` tokens in `FarmToken` contract and get an equal amount of `FRM` tokens.
function depositInFarm(tokensToDeposit: number);
// Withdraw `MTKN` tokens from `FarmToken` contract by returning an equal amount of `FRM` tokens.
function withdrawFromFarm(tokensToWithdraw: number);
// Get the number of `MTKN` and `FRM` tokens in possession of the user and `FarmToken` contract.
function getBalances();
// Get the number of `MTKN` tokens `FarmToken` contract is allowed to spend from user's account.
function getFarmAllowance();
// Subscribe to `Approval` and `Transfer` events from `MyToken` contract.
function subscribeForBalanceUpdates();
Use the AppContextProvider
in react-app/src/index.tsx
to wrap our App
component.
import React from "react";
import ReactDOM from "react-dom";
import { Web3React } from "./components/web3ReactProvider";
import { AppContextProvider } from "./context/appContextProvider";
import App from "./components/app";
import "./index.css";
ReactDOM.render(
<React.StrictMode>
<Web3React>
<AppContextProvider>
<App />
</AppContextProvider>
</Web3React>
</React.StrictMode>,
document.getElementById("root")
);
Our AppContextProvider
component uses MyToken
and FarmToken
contract addresses from environment. Let's define them in react-app/.env
file. I have provided my MyToken
and FarmToken
contract addresses in react-app/.env.example
; feel free to use them or use your own contract addresses.
REACT_APP_MY_TOKEN_CONTRACT_ADDRESS = 0x0eF6D64170a6C430A671A8b518b95b535E5deE86
REACT_APP_FARM_TOKEN_CONTRACT_ADDRESS = 0xAdc8b25015856f52264F6119d72484091f66D8e7
Define types for these environment variables in react-app/src/react-app-env.d.ts
.
export declare global {
namespace NodeJS {
interface ProcessEnv {
REACT_APP_MY_TOKEN_CONTRACT_ADDRESS: string;
REACT_APP_FARM_TOKEN_CONTRACT_ADDRESS: string;
}
}
}
Finally, create the UI for our client side in react-app/src/components/app/index.tsx
. I have tried my best to keep this component simple. The code is mostly JSX. All the functions and state values come from AppContext
.
Now, start the react app.
yarn start
It will serve the following page on localhost. It shows a connect and a disconnect button for MetaMask, and the connection status.
After connecting your account to this DApp, you will see the following screen. It shows the number of MTKN
and FRM
tokens in possession of the user and the FarmToken
contract. It also shows the number MTKN
tokens FarmToken
contract is allowed to use from the user's account. There are also some buttons to interact with the contracts.
The UI updates the balances automatically when a transaction is performed using the buttons, thanks to Web3
subscriptions. The balances are also updated when the user changes his/her account from the MetaMask wallet, thanks to useEffect
hook.
You can read more on Web3 Subscriptions here: