DEV Community

Cover image for NatSpec: The Right way to comment Ethereum smart contracts.
Perelyn-sama
Perelyn-sama

Posted on

NatSpec: The Right way to comment Ethereum smart contracts.

Introduction

While writing any sort of program, it is recommended that the code should be kept clean and properly documented as the saying goes “clean code is good code”. Smart contracts are not exempted from this and, they are needed more in smart contracts, this is because of several reasons including the fact that the code of smart contracts are open-sourced making them accessible to everyone who would desire to read them. Since the code of your smart contract is open to the general public, properly commenting on your code is very important because it makes it easier for other developers and curious users to understand your code.

Another more crucial reason for commenting on your smart contracts is to make it easier for auditing companies to audit your smart contract since they are provided with in-depth information about what each contract, library and function is expected to do. With this information, auditors can spend more time trying to spot vulnerabilities in your smart contract rather than trying to figure out the intent behind your code.

By now you should see the importance of documenting your smart contracts and you’re properly wondering how you can get started and what principles you are to follow. Thankfully the solidity team has introduced a format that solidity smart contract developers can follow called the Ethereum Natural Language Specification Format (NatSpec) and for the rest of this article we will be taking a deep dive into what NatSpec is and how we can use it effectively while writing smart contracts.

What is NatSpec?

NatSpec is a documentation format created by the solidity team which was inspired by Doxygen. it helps you describe the intent behind the code of your solidity smart contracts which could be usefully for the author themselves and other people who might be interested in your code like other developers, users and auditors. NatSpec does not only help describe the intent of your code, it also tells the readers how to interact with it too.

If your solidity smart contract is commented with NatSpec you could also use it to automatically generate documentation for your code and some tags like @notice can be displayed in the frontend of your smart contract while users are calling particular functions.

NatSpec Tags

Tags are an essential part of NatSpec, they are used to provide information on particular areas in your smart contract. Tags in NatSpec are as follow:

  • @title: Should provide information on the title of a contract or interface.
  • @author: Should provide information about the author of a contract, library or interface.
  • @notice: Should provide information to the end-user about what a contract, library, interface, function, public state variable or event does.
  • @dev: Should provide extra information about the contract, library, interface, function, state variable or event to a developer.
  • @param: Should provide information about the parameters of functions or events followed by the parameter name.
  • @return: Should provide information about the return variables of a contract’s function.
  • @inheritdoc: Copies all missing tags from the base function (must be followed by the contract name)
  • @custom:…: Custom tag, semantics is application-defined

Here’s a cheat sheet of the above tags from the solidity docs:

FIW5QVIUUAMzLg2.jfif

NatSpec Example

In another one of my articles where I write about Foundry, I employ the use of an ERC20 contract as a test subject to show readers how to use Foundry, admittedly the code for that contract was poorly commented so let’s fix that.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

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

/// @title An ERC20 contract named MyToken
/// @author Perelyn
/// @notice Serves as a fungible token
/// @dev Inherits the OpenZepplin ERC20 implentation
contract MyToken is ERC20 {

    /// @notice Deployer of the smart contract
    /// @dev Only the owner can call the mint and burn functions
    /// @return owner the address of this smart contract's deployer
    address public owner;

    /// @notice Deploys the smart contract and creates 1000 tokens
    /// @dev Assigns `msg.sender` to the owner state variable
    constructor() ERC20("My Token", "MTKN") {
        owner = msg.sender;
        _mint(msg.sender, 1000 * 10**decimals());
    }

    /// @notice Creates `MyToken` tokens and increases the total supply
    /// @dev Function can only be called by the contract's deployer
    /// @param account The address of the account that will receive the newly created tokens
    /// @param amount The amount of tokens `account` will receive
    function mint(address account, uint256 amount) public {
        require(msg.sender == owner, "Only Owner can mint");
        _mint(account, amount);
    }

    /// @notice Destorys `MyToken` tokens and decreases the total supply
    /// @dev Function can only be called by the contract's deployer
    /// @param account The address of the account that will receive the newly created tokens
    /// @param amount The amount of tokens `account` will receive
    function burn(address account, uint256 amount) public {
        require(msg.sender == owner, "Only Owner can burn");
        _burn(account, amount);
    }
}
Enter fullscreen mode Exit fullscreen mode

Looks good, doesn’t it? For extra context, in a situation when @notice is not available /// could serve as a substitute for it.

Generating Documentation

Remember I said we could generate documentation from NatSpec comments for our smart contracts, It's time we put that to action.

The first thing you need to do to be able to generate documentation is to have the solidity compiler solc on your machine. Follow this link for guides on how you can install solc on your machine.

Run this command to be sure you have solc on your machine:

# on Linux 
solc --version

# on windows 
solcjs -V
Enter fullscreen mode Exit fullscreen mode

Now that you have solc on your machine let’s get started. Remember that @notice is supposed to be shown to users and @dev provides information for other developers, this division gives us the ability to create two types of documentation. We can generate documentation for users which would only include information from the @notice comments and another for developers which would only include information from the @dev comments.

The following command can generate user documentation of a smart contract:

solc --userdoc --pretty-json <contract_file>
Enter fullscreen mode Exit fullscreen mode

The following command can generate developer documentation of a smart contract:

solc --devdoc --pretty-json <contract_file>
Enter fullscreen mode Exit fullscreen mode

Yeah, it's that easy.

💡 Note: The —pretty-json tag is not needed to generate the documentation but to format the output in the terminal.

For a better illustration let’s try testing out these commands. First, you have to create a directory called NatSpec and then create a file inside it called EtherWallet.sol inside this file, paste in the code below:

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.0 <0.9.0;

contract EtherWallet {
    address public owner;

    constructor(){
        owner = msg.sender;
    }

    receive() external payable {}

    function withdraw(uint _amount) external {
        require(msg.sender == owner, "Caller is not owner");
        payable(msg.sender).transfer(_amount);
    }

    function getBalance( ) external view returns (uint) {
        return address(this).balance;
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice that the code does not have comments, I did this on purpose so you can try out commenting it yourself as a way of practising what you have learned so far. All this contract does is try to act like a wallet for storing Ether and also give users the ability to withdraw their Ether and check the balance of the contract. If you do not want to go through this exercise it's okay, the commented version will be at the bottom of the article.

💡 Note: The Author of the above code is Smart contract Programmer and I got the code from his solidity by example.

Once you are done with that, open up your terminal and run the following command:

solc --userdoc --pretty-json EtherWallet.sol
Enter fullscreen mode Exit fullscreen mode

natspec-user-doc

======= EtherWallet.sol:EtherWallet =======
User Documentation
{
  "kind": "user",
  "methods":
  {
    "constructor":
    {
      "notice": "Deploys the contract"
    },
    "getBalance()":
    {
      "notice": "Get the balance of the contract"
    },
    "owner()":
    {
      "notice": "Get the address of owner of the contract"
    },
    "withdraw(uint256)":
    {
      "notice": "Withdraw Ether from the contract"
    }
  },
  "notice": "A Smart contract for storing Ether",
  "version": 1
}
Enter fullscreen mode Exit fullscreen mode

Remember when using the —userdoc command, the documentation provided will only display the @notice comments and ignore the @dev ones.

Now let's try generating the developer documentation.

solc --devdoc --pretty-json EtherWallet.sol
Enter fullscreen mode Exit fullscreen mode

natspec-dev-doc

Here’s a clearer version incase the screenshot was not good enough:

======= EtherWallet.sol:EtherWallet =======
Developer Documentation
{
  "author": "smart contract programmer",
  "details": "No deposit function, Ether has to be sent directly",
  "kind": "dev",
  "methods":
  {
    "constructor":
    {
      "details": "sets `msg.sender` as the owner of the contract"
    },
    "getBalance()":
    {
      "details": "Uses address(this) to get the address of the contract and .balance to get the balance of the address",
      "returns":
      {
        "_0": "balance the amount of Ether in the contract"
      }
    },
    "withdraw(uint256)":
    {
      "details": "Function can only be called by the deployer of the contract",
      "params":
      {
        "_amount": "the amount"
      }
    }
  },
  "stateVariables":
  {
    "owner":
    {
      "details": "Owner is set at deployment",
      "return": "owner the deployer of the Ether wallet",
      "returns":
      {
        "_0": "owner the deployer of the Ether wallet"
      }
    }
  },
  "title": "Ether Wallet",
  "version": 1
}
Enter fullscreen mode Exit fullscreen mode

Notice how the developer documentation contains the @dev, @param and @returns but excludes @notice.

Conclusion

Blockchain technology is a very interesting innovation. It has opened ways for humans to interact and work together with each other without worry and fear. You do not have to worry about trusting people because the system itself is trustless. To maintain this amazing feat, we developers have to play our part in contributing to a truly decentralized future and properly documenting the code of our smart contracts for others to quickly understand whether they might be users, developers or auditors. This might seem insignificant but trust me, it goes a long way.

Till next time, Fren^-^.

Here’s the commented version of EtherWallet I promised.

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.0 <0.9.0;

/// @title Ether Wallet
/// @author smart contract programmer
/// @notice A Smart contract for storing Ether
/// @dev No deposit function, Ether has to be sent directly
contract EtherWallet {
    /// @notice Get the address of owner of the contract
    /// @dev Owner is set at deployment
    /// @return owner the deployer of the Ether wallet
    address public owner;

    /// @notice Deploys the contract
    /// @dev sets `msg.sender` as the owner of the contract
    constructor() {
        owner = msg.sender;
    }

    /// @notice Receive Ether
    /// @dev alternative would be a deposit function
    receive() external payable {}

    /// @notice Withdraw Ether from the contract
    /// @dev Function can only be called by the deployer of the contract
    /// @param _amount the amount
    function withdraw(uint256 _amount) external {
        require(msg.sender == owner, "Caller is not owner");
        payable(msg.sender).transfer(_amount);
    }

    /// @notice Get the balance of the contract
    /// @dev Uses address(this) to get the address of the contract and .balance to get the balance of the address
    /// @return balance the amount of Ether in the contract
    function getBalance() external view returns (uint256) {
        return address(this).balance;
    }
}
Enter fullscreen mode Exit fullscreen mode

Discussion (0)