Dev's Journal 3
NatSpec Documentation, Forge Script+, and Building in a Bear Market
Over the past week, I've mostly been working to finalized some contracts to go to audit alongside some other devs at Olympus. In this week's edition of the Dev's Journal, I'll cover some thoughts on NatSpec documentation, Forge Script and possible extensions, and Building in a Bear Market. This week is a little light on code examples since I've been mainly in a review and documentation cycle.
Building with NatSpec Documentation
Consistent, well-formatted documentation and clear variable names are important for all code to easy understood by others (unless you're goblintown and doing it for the memes). Smart contracts emphasize this even further because it's one of the only computing environments where (some) general users go to read code before they use a service.
Solidity features a special form of comments for documenting code in-line called the Ethereum Natural Language Specification Format (NatSpec). NatSpec is useful because it allows outside services, like Etherscan, to read the documentation from the code and display it in their interface where users can interact with the contract. Additionally, NatSpec promotes consistent documentation practices with the defined tags it allows you to use in your comments.
There are different ways to write NatSpec and a lot of developers/teams use different conventions. No one way is the "right" way as long as it conforms to the spec; however, there are some practices which I find help make documentation more consistent and provide information relevant to readers:
- Single-line NatSpec comments
///
make for a cleaner line indent and take fewer lines that wrapping into a multi-line comment/** ... */
, but some prefer the latter. Choose one format and stick to it. - If a contract inherits an interface, then the interface should hold the documentation for the functions/structs it defines and the implementing contract should inherit the documentation (
@inheritdoc
). In general, inheriting documentation from base contracts keeps documentation consistent across implementations. If needed you can add additional comments after inheriting.
abstract contract ExampleBase {
/// @notice Callback function for DEX flash swap
/// @param amountIn Amount of the token sent in
/// @param amountOut Expected amount of the token out
function callback(uint256 amountIn, amountOut) external virtual {
}
}
contract Example is ExampleBase {
/// @inheritdoc ExampleBase
/// @dev LP pool must be permissioned to call this function.
function callback(uint256 amountIn, uint256 amountOut) external override requiresAuth {
...
}
}
- Internal functions on a contract should be documented with NatSpec, even though it won't show up on external services (like Etherscan). This is so that developers reading your contracts have a consistent experience and aren't guessing what your random helper functions do.
- Gated functions should be specified with a
@notice
or@custom
tag. If you use a static access control system, specify the role or contract that can call the function. If you use a mutable access control system, specify access is restricted. - All public variables should have NatSpec comments explaining what they are for.
- Internal variables can have regular comments, but should be documented.
- Dev notes should be provided when there are non-standard interactions or expectations around calling a function. However, resist the urge to regurgitate code comments in dev notes above a function since it will be redundant.
Some people like to document the code as they go, and others like to do it at the end once the code is more solidified. I think there are merits to both approaches. I tend to use a hybrid where I make comments about important points along the way, but leave standard boilerplate to the end so that I'm not as bogged down making changes along the way.
Researching Forge Script and Bots
I've been reading about MEV a bit recently and have a couple ideas for some niche opportunities so I decided to explore a bit further. I know the general structure of certain off-chain + on-chain automated systems from building a bot for my auto-compounding app about 9 months ago, but I hadn't looked into it much since then.
My frame of reference was that a lot of bots are written in Javascript or Python and leverage the web3/ethers libraries to interact with the block chain. By connecting to a websocket RPC, you can have a service running perpetually (with some work on the DevOps/infrastructure side) to listen for events and then take actions by sending transactions directly to your own contracts or external ones. On the MEV front, Flashbots provides a provider library in these languages plus Go (for those optimizing for speed) for sending transaction bundles to their private mempool.
Those formats are adequate and likely still used by the majority of builders, but I wondered what else was out there, especially since I've gotten into using foundry as my Solidity development environment over the past few months.
As I wrote about last week, foundry recently released a new scripting feature for forge. The initial use cases I've seen mimic the contract deployment and configuration use cases that I would have previously employed a hardhat script for. When called from the command line, they execute a series of operations and provide a status on the completion. Forge script works great for these use cases and allows you to send individual Solidity commands as transactions without worrying about async/await function formats or other context switching penalties. Combining these two trains of thought, I began to wonder if you could build an off-chain bot using forge script-like contracts. Forge is built in Rust and leverages the rust fork of ethers (ethers-rs); therefore, the building blocks are there. The benefits would be the same as other scripts from a context switching perspective and the resulting bots could be fast (if well designed) since they leverage Rust.
However, there are a few shortcomings with this approach currently. First, the scripting environment mimics the blockchain and doesn't have access to event data. There is no way to listen to events or "wait" for blocks to mine and search transactions. Therefore, you need another service to act as the listener, which could be a Rust service using ethers-rs. Second, transactions cannot be submitted in bundles with the current script cheatcodes. There is an existing feature in foundry cast which lets users send transactions to the Flashbots RPC by passing a --flashbots
flag with your transaction or just using a Flashbots RPC. However, this only works for a single transaction and not a bundle. There is an active issue requesting support for transaction bundles, but I imagine this would be better done in a script with new cheatcodes for creating bundles. Something like this request for ape-safe style bundles for multi-sig execution might be dual purpose.
Based on this, a potential new architecture for automated systems could have three components instead of two:
- On-chain contracts that encapsulate logic that is required to be performed within a single transaction (e.g. flash loans and their callbacks)
- Contract scripts to perform a series of chain interactions via separate or bundled transactions.
- Non-contract code that listens for blockchain events and determines when to trigger contract scripts.
The difference from the current systems is that 2 is currently done in non-contract code and can benefit from all the forge niceties.
I don't know Rust past the basics, but I guess now is as good a time as any to start learning.
Thinking about Building in Bear Markets
This will sound a bit cliche at first, but bear(๐ ) with me.
I recently decided to shutdown an auto-compounding app that I built last year during the bull market. When token prices are up or at least steady, auto-compounding is a convenient way to capture additional yield. In a bear market, you're just pouring gasoline on a fire by adding more sell pressure to the rewarded token, contributing to a spiral.
As I was doing this, I started thinking about additional solutions to build that are more resistant to these market dynamics or counteract them. Part of my motivation was curiosity, but I'd also like to find ways to generate new income (as would everyone) considering the market conditions. Then, I realized these exact thoughts likely motivate a lot of people when times are tough and may be a reason why a lot of innovation comes out of a bear market. I typically work harder when my back is against the wall, and I imagine others do too. I'm not saying we should all be miserable to do good work, but market cycles are healthy, and we have to take the bad with the good that comes later. This parallels other seasons of life.
This probably sounds like coping and regurgitation of the running "bears are for building" motivational tweets you see across CT right now. I get that. I'm down like many others, but that's not the purpose of writing this. The point is that I found myself naturally engaging in the kind of behavior and thoughts that generates these anecdotes, which maybe provides a data point supporting a cyclical pattern of innovation related to the markets. Or, maybe I've been brainwashed as well. Only time will tell.
I wasn't actively working in crypto/web3 during the last bear market, but a large number of the current established projects were during this period. A couple other data points I saw on Twitter while thinking about this:
Currently Reading
- Mostly GitHub repositories and developer documentation this past week.
- Continuing to work my way through How To Take Smart Notes - Sonke Ahrens, which has helped me begin to more systematically capture and use information.