Tiempo estimado: 10 minutos de lectura
¿Cómo documentar smart contracts en Solidity?
Los comentarios al código fuente Solidity se pueden añadir mediante «//» para los que ocupan una única línea y «/* … */» para los bloques. Es un convenio común al usado en otros lenguajes de programación como Javascript.
Documentar un smart contract contribuye a dotar de confianza y transparencia las transacciones efectuadas en la blockchain. Es un proceso que complementa la verificación del smart contrat.
Por otra parte existe la norma NatSpec (Ethereum Natural Language Specification Format) para generar comentarios enriquecidos para determinados elementos de los contratos como funciones, parámetros de entrada, valores de retorno…
Se trata de un estandar derivado de Doxygen con sus propias particularidades.
Ese tipo de documentación se crea para dos tipos de receptores:
- Un usuario final del contrato. Interactuará habitualmente a través de un wallet.
- Un desarrollador que integra el contrato en alguna aplicación descentralizada o en otro smart contract.
La recomendación es que todas las declaraciones públicas de un contrato (las que aparecen en el ABI) sean convenientemente documentadas de esa forma.
¿Cómo añadir los comentarios NatSpec al código fuente?
Los comentarios NatSpec deben venir precedidos por «///» para los que ocupan una única línea o en bloques «/** … */» para los multilínea.
Se pueden etiquetar para categorizar su propósito. Las etiquetas más comunes, su contexto de uso entre paréntesis, son las siguientes:
- @title (contrato, librería, interfaz, estructura y enum): describe el elemento documentado.
- @author (contrato, librería, interfaz, estructura y enum): autor del elemento documentado.
- @notice (contrato, librería, interfaz, función, variable de estado pública, evento, estructura, enum y error): detalles para el usuario final sobre el elemento documentado.
- @dev (contrato, librería, interfaz, función, variable de estado pública, evento, estructura, enum y error): detalles para el desarrollador sobre el elemento documentado.
- @param (función, evento y error): documenta un parámetro. Debe estar seguido por el nombre del parámetro.
- @return (función): documenta un valor de retorno de una función. Debe repetirse por cada valor de retorno existente.
De todos los tipos de comentarios presentados, el único que se genera para el usuario final es el etiquetado con «@notice«. Si un comentario no lleva etiqueta se considera de este tipo.
Por otra parte si un comentario se extiende varias líneas, se cataloga de acuerdo a la última etiqueta definida. Por ejemplo:
/// @dev This is a multiline comment
/// for a developer
O mediante un comentario de bloque:
/**
* @dev This is a multiline comment
* for a developer
*/
A continuación se ilustran estas etiquetas para el contrato de ejemplo 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 {}
}
¿Cómo se generan los ficheros de documentación NatSpec?
Como se ha visto la documentación NatSpec se aplica en el contexto de los elementos contrato, librería, interfaz, función (externa o pública), variable de estado pública, evento, estructura, enum y error.
En otros contextos, por ejemplo en funciones privadas, los comentarios son omitidos por el compilador y no producen ningún resultado.
Para generar los ficheros de documentación hay que invocar al compilador de Solidity con los modificadores «–userdoc» para el fichero con los comentarios para el usuario final (etiquetas @notice) y «–devdoc» para el fichero con los comentarios para el desarrollador (el resto de etiquetas).
Como ejemplo particular para Windows, habría que descargar la versión oportuna del compilador de Solidity del repositorio oficial y ejecutar este comando:
.\solc-windows.exe --devdoc --userdoc --base-path ".\" --include-path "node_modules" --output-dir "docs" --overwrite --pretty-json .\contracts\TestingContract.sol
El parámetro «include-path» debe referenciar una ruta donde encontrar las librerías de contratos importadas, «Ownable.sol» de OpenZeppelin para el contrato del ejemplo.
Al ejecutar el comando se generan los dos ficheros JSON esperados en el directorio definido en el parámetro «output-dir».
¿Qué otras alternativas hay para documentar un smart contract?
Si bien los ficheros generados por el compilador de Solidity a partir de los comentarios NatSpec suponen un punto de partida en la creación de una documentación enriquecida para los smart contracts, no dejan de adolecer de ciertas limitaciones:
- Su formato JSON que dificulta su análisis visual.
- Se omiten determinados comentarios, por ejemplo los de las funciones privadas.
- Si el contrato hereda de otros, en su documentación se mezclan las funciones públicas heredadas.
Para solventar estas particularidades y mejorar los resultados han surgido diversos proyectos de código abierto. En este artículo presento la herramienta solidoc, una adaptación que he efectuado del proyecto neptune-mutual/solidoc2 , para crear documentación a partir de los comentarios NatSpec.
Para generar la documentación se deben compilar los contratos mediante Truffle. En el directorio de salida (ubicado por defecto en la ruta «build/contracts») se genera un archivo JSON por cada contrato compilado con una sección AST (Abstract Syntax Tree) que contiene nodos con elementos «StructuredDocumentation». La herramienta «solidoc» extrae la documentación del contrato de esas estructuras y genera como salida un fichero en formato Markdown (md) para cada contrato, sin omitir ningún comentario NatSpec (incluso para funciones privadas) y sin mezclar las funciones heredadas desde otros contratos.
El formato de la documentación generada es al estilo de un API de desarrollo, tal y como la utilizan habitualmente los programadores.
Se ilustra con la siguiente captura de pantalla:
En este momento la mayor limitación de la herramienta es la necesidad de compilar los contratos con el obsoleto Truffle. Es así porque está adaptada a las secciones en formato AST del fichero JSON de salida que genera Truffle con todos los comentarios del código fuente.
¿Cómo se utiliza la herramienta Solidoc?
El primer paso es instalarla en el proyecto:
yarn add --dev @jplsaez/solidoc
Como lleva implícita la dependencia con el paquete Truffle, no es preciso instalarlo por separado.
Para compilar los contratos con Truffle se debe configurar un fichero «truffle-config.js» en el raíz del proyecto. Se podría tomar como base el que se incluye en el repositorio GitHub de la herramienta «solidoc»:
module.exports = {
// Configure your compilers
compilers: {
solc: {
version: "0.8.24",
}
},
};
Se aprecia que en general basta con definir la versión del compilador a utilizar.
El comando de compilación se podría invocar de esta forma si Truffle no está definido a nivel global (ejemplo para Windows):
.\node_modules\.bin\truffle compile
Por defecto se buscan los contratos en la carpeta «contracts». Los ficheros de la compilación se generan por defecto en la ruta «build/contracts».
Por último antes de generar la documentación se debe parametrizar un fichero «solidoc.json» en el raíz del proyecto. Igualmente como base se podría emplear el que se incluye en el repositorio GitHub de la herramienta «solidoc»:
{
"pathToRoot": "./",
"outputPath": "./docs",
"includedContracts": "TestingContract, Ownable"
}
Donde:
- pathToRoot: raíz del proyecto donde se ubica el directorio «contracts» con el código fuente de los contratos a documentar.
- outputPath: ruta al directorio donde almacenar los ficheros de documentación generados.
- includedContracts: lista separada por comas de los contratos compilados para los que queremos genererar la documentación. Como se ve en el ejemplo, se pueden incluir los contratos heredados para generar también su documentación.
Para invocar la herramienta se ejecuta:
yarn solidoc
Para finalizar se detallan algunas particularidades de uso de la herramienta:
- No se debe usar la etiqueta NatSpec «@author» para evitar errores en las secciones AST que genera Truffle al compilar los contratos.
Para visualizar correctamente saltos de línea en el archivo Markdown de salida hay que anteponer al menos dos espacios antes del salto de línea en el comentario del código fuente.
Para crear referencias cruzadas de un documento a otro (enlaces), se debe añadir un texto con el siguiente formato en el comentario correspondiente:
[$text_of_link]($name_of_linked_file.md#$anchor)
El ancla ($anchor) se debe escribir en minúsculas y debe conincidir con alguna sección del fichero de Markdown enlazado que comience con «#».
Si la referencia es a una sección dentro del mismo documento, se puede omitir el nombre del archivo enlazado ($name_of_linked_file.md), dejando tan sólo el ancla.
Por ejemplo en los comentarios del contrato TestingContract se ha creado una referencia a otra sección del propio documento:
/**
* 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.
*/
Conclusión: más que buenas prácticas de programación
La documentación del código fuente de los smart contracts en Solidity es, además de una buena práctica de programación como sucede con cualquier otro lenguaje, más importante si cabe ya que contribuye a dar seguridad y confianza a las comunidades web3 que interactúan con la blockchain (usuarios y desarrolladores).
Idealmente deberían documentarse correctamente no sólo los elementos públicos de la interfaz mediante NatSpec sino también el funcionamiento detallado de las funciones y el resto de componentes.
En este artículo se ha presentado la herramienta solidoc de código abierto que permite llegar un paso más allá en el uso de los comentarios NatSpec de Solidity, para además de generar la documentación JSON de usuario y de desarrollador, poder crear documentos legibles en formato Markdown a modo de API de desarrollo, sin las limitaciones que impone la documentación generada por el compilador de Solidity.
De este modo se puede suministrar una documentación muy completa para que los desarrolladores integren los contratos con más facilidad.
¿Cómo documentas tus smart contracts en Solidity? ¿Usas habitualmente el formato NatSpec u otras herramientas de apoyo para la generación de documentación? Deja tus comentarios si quieres compartir tus experiencias.
Puedo ayudarte si necesitas un programador de smart contracts habituado a documentar el código fuente para hacerlo legible y confiable. Hablemos.