Estimated time: 10 minutes read
How to Document Smart Contracts in Solidity?
Solidity source code comments can be added using “//” for single-line comments and “/* … */” for multi-line blocks. This convention is common in other programming languages like JavaScript.
Documenting a smart contract helps instill trust and transparency in blockchain transactions. It’s a process that complements smart contract verification.
NatSpec (Ethereum Natural Language Specification Format) is a standard for generating rich comments for specific contract elements such as functions, input parameters, return values, etc.
It’s a standard derived from Doxygen with its own particularities.
This type of documentation is created for two types of recipients:
- An end-user of the contract: Typically interacts through a wallet.
- A developer: Integrates the contract into a decentralized application or another smart contract.
It’s recommended that all public declarations of a contract (those that appear in the ABI) be documented in this way.
How to add NatSpec comments to the source code?
NatSpec comments must be preceded by “///” for single-line comments or “/** … */” for multi-line blocks.
They can be tagged to categorize their purpose. The most common tags, with their context of use in parentheses, are:
- @title (contract, library, interface, struct and enum): Describes the documented element.
- @author (contract, library, interface, struct and enum): Author of the documented element.
- @notice (contract, library, interface, function, public state variable, event, struct, enum and error): Details for the end-user about the documented element.
- @dev (contract, library, interface, function, public state variable, event, struct, enum and error): Details for the developer about the documented element.
- @param (function, event and error): Documents a parameter. Must be followed by the parameter name.
- @return (function): Documents a return value of a function. Must be repeated for each existing return value.
Of all the types of comments presented, the only one generated for the end-user is the one tagged with “@notice“. If a comment has no tag, it is considered of this type.
On the other hand, if a comment spans multiple lines, it is categorized according to the last defined tag. For example:
/// @dev This is a multiline comment
/// for a developer
Or using a block comment:
/**
* @dev This is a multiline comment
* for a developer
*/
The following illustrates these tags for the sample contract TestingContract.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/Ownable.sol";
/// @dev Error details thrown within TestingContractError
enum TestingContractErrorDetail {
INVALID_VALUE,
INVALID_ADDRESS,
NOT_ALLOWED,
UNAVAILABLE_BALANCE,
TRANSFER_FAILURE
}
/**
* @notice For end user, this is a contract to test some NatSpec comments issues.
* @title TestingContract has some simple funtions.
* @dev Every NatSpec tag is tested in this contract
*/
contract TestingContract is Ownable {
/// @dev Emitted when currentValue is updated
event ValueUpdated(uint256 indexed newValue, uint256 indexed oldValue, address indexed sender);
/// @dev Emitted when allowedAddress is updated
event AddressUpdated(address indexed newValue, address indexed oldValue);
/// @dev Custom error to revert when some error condition happens
error TestingContractError(TestingContractErrorDetail details);
/// @dev value to update
uint256 public currentValue;
/// @dev restricted value to update
address private allowedAddress;
/**
* @notice For end user, constructor called from contract owner.
* @dev Constructor called from contract owner.
* Owner is set to caller.
*/
constructor() Ownable(msg.sender) {
currentValue = 0;
}
/**
* @notice For end user, changes current value
* @param currentValue_ New value to update.
* @dev Changes current value
*
* Throws TestingContractError if new value is not allowed.
*
*/
function setCurrentValue(uint256 currentValue_) external {
// Value not allowed
if(currentValue_==0)
revert TestingContractError(TestingContractErrorDetail.INVALID_VALUE);
// Success
emit ValueUpdated(currentValue_, currentValue, msg.sender);
currentValue = currentValue_;
}
/**
* For end user, changes current value if requested by allowed address
* @dev Changes current value if requested by allowed address
*
* Throws TestingContractError if requested by not allowed address.
*
* @param currentValue_ New value to update.
*/
function setCurrentValueRestricted(uint256 currentValue_) external {
// Requestes by not allowed address
if(msg.sender!=allowedAddress)
revert TestingContractError(TestingContractErrorDetail.NOT_ALLOWED);
// Success
emit ValueUpdated(currentValue_, currentValue, msg.sender);
currentValue = currentValue_;
}
/**
* For end user, changes allowed address
* @dev Changes allowed address
*
* Throws TestingContractError if new address is not allowed.
* Throws OwnableUnauthorizedAccount if caller is not contract owner.
*
* @param allowedAddress_ New address to update.
*/
function setAllowedAddress(address allowedAddress_) external onlyOwner {
// Not allowed address
if(allowedAddress_== address(0))
revert TestingContractError(TestingContractErrorDetail.INVALID_ADDRESS);
// Success
emit AddressUpdated(allowedAddress_, allowedAddress);
allowedAddress = allowedAddress_;
}
/**
* For end user, withdraws all contract balance.
* @dev Function to withdraw all contract balance.
*
* Throws OwnableUnauthorizedAccount if caller is not allowed.
* Throws TestingContractError if there is not balance to transfer or transfer process fails.
* See [TestingContractErrorDetail](#testingcontracterrordetail).
*
* @param receiver Address to trasfer balance to.
*/
function withdraw(address receiver) external onlyOwner {
// No balance
if(address(this).balance == 0)
revert TestingContractError(TestingContractErrorDetail.UNAVAILABLE_BALANCE);
// Transfers balance
bool success = payable(receiver).send(address(this).balance);
// Some error happened
if(!success)
revert TestingContractError(TestingContractErrorDetail.TRANSFER_FAILURE);
}
/// @dev To allow receiving balance
receive() external payable {}
}
How are NatSpec documentation files generated?
As discussed, NatSpec documentation is applied in the context of contract, library, interface, function (external or public), public state variable, event, struct, enum and error elements.
In other contexts, for example, in private functions, comments are omitted by the compiler and do not produce any output.
To generate the documentation files, you need to invoke the Solidity compiler with the “–userdoc” modifier for the file containing comments for the end user (with @notice tags) and “–devdoc” for the file with comments for the developer (the rest of the tags).
As a specific example for Windows, you would need to download the appropriate version of the Solidity compiler from the official repository and execute this command:
.\solc-windows.exe --devdoc --userdoc --base-path ".\" --include-path "node_modules" --output-dir "docs" --overwrite --pretty-json .\contracts\TestingContract.sol
The “include-path” parameter must reference a path where the libraries of imported contracts can be found, such as OpenZeppelin’s “Ownable.sol” for the example contract.
When the command is executed, the two expected JSON files are generated in the directory defined by the “output-dir” parameter.
What other alternatives are there for documenting a smart contract?
While the files generated by the Solidity compiler from NatSpec comments provide a starting point for creating enriched documentation for smart contracts, they still have certain limitations:
- Their JSON format makes visual analysis difficult.
- Certain comments, such as those for private functions, are omitted.
- If the contract inherits from others, the documentation mixes the inherited public functions.
To address these particularities and improve the results, various open-source projects have emerged. In this article, I present the solidoc tool, an adaptation I made of the neptune-mutual/solidoc2 project, to create documentation from NatSpec comments.
To generate the documentation, the contracts must be compiled using Truffle. In the output directory (by default located at the path “build/contracts”), a JSON file is generated for each compiled contract with an Abstract Syntax Tree (AST) section that contains nodes with “StructuredDocumentation” elements. The “solidoc” tool extracts the contract’s documentation from these structures and outputs a Markdown (md) file for each contract, without omitting any NatSpec comments (even for private functions) and without mixing inherited functions from other contracts.
The format of the generated documentation is in the style of a development API, as is commonly used by programmers.
This is illustrated by the following screenshot:
At this moment, the biggest limitation of the tool is the need to compile contracts with the outdated Truffle. This is because it is adapted to the AST format sections of the JSON output file generated by Truffle with all the comments from the source code.
How is the Solidoc tool used?
The first step is to install it in the project:
yarn add --dev @jplsaez/solidoc
Since it implicitly depends on the Truffle package, it is not necessary to install Truffle separately.
To compile the contracts with Truffle, you need to configure a “truffle-config.js” file in the root of the project. You could use the one included in the GitHub repository of the ‘solidoc’ tool as a base:
module.exports = {
// Configure your compilers
compilers: {
solc: {
version: "0.8.24",
}
},
};
It is generally sufficient to define the compiler version to be used.
The compilation command can be invoked this way if Truffle is not defined globally (example for Windows):
.\node_modules\.bin\truffle compile
By default, contracts are searched for in the “contracts” folder. Compilation files are generated by default in the “build/contracts” path.
Finally, before generating the documentation, a “solidoc.json” file needs to be configured in the root of the project. Similarly, you can use the one included in the GitHub repository of the “solidoc” tool as a base:
{
"pathToRoot": "./",
"outputPath": "./docs",
"includedContracts": "TestingContract, Ownable"
}
Where:
- pathToRoot: The root of the project where the “contracts” directory with the source code of the contracts to document is located.
- outputPath: The path to the directory where the generated documentation files will be stored.
- includedContracts: A comma-separated list of the compiled contracts for which you want to generate documentation. As shown in the example, inherited contracts can also be included to generate their documentation as well.
To invoke the tool, run:
yarn solidoc
Finally, here are some usage specifics of the tool:
- Do not use the NatSpec “@author” tag to avoid errors in the AST sections generated by Truffle when compiling contracts.
- To properly visualize line breaks in the output Markdown file, at least two spaces must be added before the line break in the source code comment.
- To create cross-references from one document to another (links), you should include text in the following format in the corresponding comment:
[$text_of_link]($name_of_linked_file.md#$anchor)
The anchor ($anchor) should be written in lowercase and must match a section in the linked Markdown file that begins with #.
If the reference is to a section within the same document, you can omit the linked file name ($name_of_linked_file.md), leaving only the anchor.
For example, in the comments of the TestingContract contract, a reference has been created to another section within the same document:
/**
* Withdraws all contract balance.
* @dev Function to withdraw all contract balance.
*
* Throws OwnableUnauthorizedAccount if caller is not allowed.
* Throws TestingContractError if there is not balance to transfer or transfer process fails.
* See [TestingContractErrorDetail](#testingcontracterrordetail).
*
* @param receiver Address to trasfer balance to.
*/
Conclusion: More than Just Good Programming Practices
Documenting the source code of smart contracts in Solidity is not only a good programming practice, as it is with any other language, but it is even more important as it contributes to providing security and trust to the web3 communities interacting with the blockchain (users and developers).
Ideally, not only should public interface elements be properly documented using NatSpec, but also the detailed operation of functions and other components.
This article has presented the open-source tool Solidoc, which takes the use of Solidity’s NatSpec comments a step further. It not only generates user and developer JSON documentation but also creates readable Markdown documents as a development API, without the limitations imposed by the documentation generated by the Solidity compiler.
In this way, very comprehensive documentation can be provided to help developers integrate contracts more easily.
How do you document your smart contracts in Solidity? Do you commonly use the NatSpec format or other supporting tools for documentation generation? Leave your comments if you want to share your experiences.
I can help if you need a smart contract programmer experienced in documenting source code to make it readable and reliable. Let’s talk.