Smart Contracts

Smart contracts are self-executing contracts with the terms of the agreement directly written into code. They serve as the decentralized backend of any DApp, automating and enforcing the business logic within the system.

If coming from traditional web development, smart contracts are meant to replace the backend with important caveats: the user must have the native currency (GLMR, MOVR, DEV, etc.) to make state-changing requests, storing information can be expensive, and no information stored is private.

When you deploy a smart contract onto Moonbeam, you upload a series of instructions that can be understood by the EVM, or the Ethereum Virtual Machine. Whenever someone interacts with a smart contract, these transparent, tamper-proof, and immutable instructions are executed by the EVM to change the blockchain's state. Writing the instructions in a smart contract properly is very important since the blockchain's state defines the most crucial information about your DApp, such as who has what amount of money.

Since the instructions are difficult to write and make sense of at a low (assembly) level, we have smart contract languages such as Solidity to make it easier to write them. To help write, debug, test, and compile these smart contract languages, developers in the Ethereum community have created developer environments such as Hardhat and Foundry. Moonbeam's developer site provides information on a plethora of developer environments.

This tutorial will use Hardhat for managing smart contracts.

Create a Hardhat Project

You can initialize a project with Hardhat using the following command:

npx hardhat init

When creating a JavaScript or TypeScript Hardhat project, you will be asked if you want to install the sample project's dependencies, which will install Hardhat and the Hardhat Toolbox plugin. You don't need all of the plugins that come wrapped up in the Toolbox, so instead you can install Hardhat, Ethers, and the Hardhat Ethers plugin, which is all you'll need for this tutorial:

npm install --save-dev hardhat @nomicfoundation/hardhat-ethers ethers@6

Before we start writing the smart contract, let's add a JSON-RPC URL to the config. Set the hardhat.config.js file with the following code, and replace INSERT_YOUR_PRIVATE_KEY with your funded account's private key.

Remember

This is for testing purposes, never store your private key in plain text with real funds.

require('@nomicfoundation/hardhat-ethers');
module.exports = {
  solidity: '0.8.20',
  networks: {
    moonbase: {
      url: 'https://rpc.api.moonbase.moonbeam.network',
      chainId: 1287,
      accounts: ['INSERT_YOUR_PRIVATE_KEY']
    }
  }
};

Write Smart Contracts

Recall that we're making a DApp that allows you to mint a token for a price. Let's write a smart contract that reflects this functionality!

Once you've initialized a Hardhat project, you'll be able to write smart contracts in its contracts folder. This folder will have an initial smart contract, likely called Lock.sol, but you should delete it and add a new smart file called MintableERC20.sol.

The standard for tokens is called ERC-20, where ERC stands for "Ethereum Request for Comment". A long time ago, this standard was defined, and now most smart contracts that work with tokens expect tokens to have all of the functionality defined by ERC-20. Fortunately, you don't have to know it from memory since the OpenZeppelin smart contract team provides us with smart contract bases to use.

Install OpenZeppelin smart contracts:

npm install @openzeppelin/contracts

Now, in your MintableERC20.sol, add the following code:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MintableERC20 is ERC20, Ownable {
    constructor(address initialOwner) ERC20("Mintable ERC 20", "MERC") Ownable(initialOwner) {}
}

When writing smart contracts, you're going to have to compile them eventually. Every developer environment for smart contracts will have this functionality. In Hardhat, you can compile with:

npx hardhat compile

Everything should compile well, which should cause two new folders to pop up: artifacts and cache. These two folders hold information about the compiled smart contracts.

Let's continue by adding functionality. Add the following constants, errors, event, and function to your Solidity file:

    uint256 public constant MAX_TO_MINT = 1000 ether;

    event PurchaseOccurred(address minter, uint256 amount);
    error MustMintOverZero();
    error MintRequestOverMax();
    error FailedToSendEtherToOwner();

    /**Purchases some of the token with native currency. */
    function purchaseMint() payable external {
        // Calculate amount to mint
        uint256 amountToMint = msg.value;

        // Check for no errors
        if(amountToMint == 0) revert MustMintOverZero();
        if(amountToMint + totalSupply() > MAX_TO_MINT) revert MintRequestOverMax();

        // Send to owner
        (bool success, ) = owner().call{value: msg.value}("");
        if(!success) revert FailedToSendEtherToOwner();

        // Mint to user
        _mint(msg.sender, amountToMint);
        emit PurchaseOccurred(msg.sender, amountToMint);
    }
MintableERC20.sol file
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MintableERC20 is ERC20, Ownable {
    constructor(address initialOwner) ERC20("Mintable ERC 20", "MERC") Ownable(initialOwner) {}

    uint256 public constant MAX_TO_MINT = 1000 ether;

    event PurchaseOccurred(address minter, uint256 amount);
    error MustMintOverZero();
    error MintRequestOverMax();
    error FailedToSendEtherToOwner();

    /**Purchases some of the token with native gas currency. */
    function purchaseMint() external payable {
        // Calculate amount to mint
        uint256 amountToMint = msg.value;

        // Check for no errors
        if (amountToMint == 0) revert MustMintOverZero();
        if (amountToMint + totalSupply() > MAX_TO_MINT)
            revert MintRequestOverMax();

        // Send to owner
        (bool success, ) = owner().call{value: msg.value}("");
        if (!success) revert FailedToSendEtherToOwner();

        // Mint to user
        _mint(msg.sender, amountToMint);
        emit PurchaseOccurred(msg.sender, amountToMint);
    }
}

This function will allow a user to send the native Moonbeam currency (like GLMR, MOVR, or DEV) as value because it is a payable function. Let's break down the function section by section.

  1. It will figure out how much of the token to mint based on the value sent

  2. Then it will check to see if the amount minted is 0 or if the total amount minted is over the MAX_TO_MINT, giving a descriptive error in both cases

  3. The contract will then forward the value included with the function call to the owner of the contract (by default, the address that deployed the contract, which will be you)

  4. Finally, tokens will be minted to the user, and an event will be emitted to pick up on later

To make sure that this works, let's use Hardhat again:

npx hardhat compile

You've now written the smart contract for your DApp! If this were a production app, we would write tests for it, but that is out of the scope of this tutorial. Let's deploy it next.

Deploy Smart Contracts

Under the hood, Hardhat is a Node project that uses the Ethers.js library to interact with the blockchain. You can also use Ethers.js in conjunction with Hardhat's tool to create scripts to do things like deploy contracts.

Your Hardhat project should already come with a script in the scripts folder, called deploy.js. Let's replace it with a similar, albeit simpler, script.

const hre = require('hardhat');

async function main() {
  const [deployer] = await hre.ethers.getSigners();

  const MintableERC20 = await hre.ethers.getContractFactory('MintableERC20');
  const token = await MintableERC20.deploy(deployer.address);
  await token.waitForDeployment();

  // Get and print the contract address
  const myContractDeployedAddress = await token.getAddress();
  console.log(`Deployed to ${myContractDeployedAddress}`);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

This script uses Hardhat's instance of the Ethers library to get a contract factory of the MintableERC20.sol smart contract that we wrote earlier. It then deploys it and prints the resultant smart contract's address. Very simple to do with Hardhat and the Ethers.js library, but significantly more difficult using just JSON-RPC!

Let's run the contract on Moonbase Alpha (whose JSON-RPC endpoint we defined in the hardhat.config.js script earlier):

npx hardhat run scripts/deploy.js --network moonbase

You should see an output that displays the token address. Make sure to save it for use later!

Challenge

Hardhat has a poor built-in solution for deploying smart contracts. It doesn't automatically save the transactions and addresses related to the deployment! This is why the hardhat-deploy package was created. Can you implement it yourself? Or can you switch to a different developer environment, like Foundry?

Last updated