In my previous job as a Product Manager in QuantumBlack, I worked on designing Kedro, a machine learning framework for developers.
I remember looking for articles on Product Management for open source projects and there was very little out there.
Designing an open source product takes far more than good “API Design”.
The code itself is the product.
Smart contracts work the same way.
They get consumed in many code-sensitive ways:
Their events are consumed by indexing tools and developers building a monitoring stack
They are read by security researchers and power-users
They are used from block explorers, multisig and governance dapps
They are used as dependencies in other live contracts
They are used from transaction simulators
I recently finished a relatively long smart contract design project and wanted to reflect on some of the principles we used in the process.
There seem to be 4 dominant objectives guiding smart contract design:
Usability (ease of using the contracts from other contracts and directly by users)
Composability (the ability to extend and incorporate the contract in other systems)
Readability (ease of understanding the contracts)
Security (reducing the security surface and minimizing the probability of errors).
1. Simplicity
Simplicity affects everything else.
The contracts get easier to read, easier to secure and easier to consume.
This should be the starting point for everything else.
Achieving simplicity often means cutting out the necessary while also cleaning up the design to be as simple as possible.
Note that simplicity doesn't always mean using the most naive implementation.
This is best explained in Rich Hickey's talk “Simple Made Easy”:
In fact simplicity can be quite complex to engineer.
If you're looking to dive deeper into the rabbit hole, you may want to explore Algebra-Driven Design as a way of leveraging mathematical simplicity to design simpler software.
2. Standards
You can think of ERC standards as pre-certified design decisions.
They relieve decision paralysis, facilitate composability, and usually create clear security properties.
You should use them where it makes sense.
PoolTogether V5 is a great example of using ERC4626 both as a means of extensibility and also as their own protocol interface.
It can be seen as a simple box that takes in ERC4626 yield sources and creates an ERC4626 PoolTogether vault for that yield source.
3. Uniformity
There is no universal design language for smart contracts.
But consistency within a repository is very important.
For example, if you use the FREI-PI pattern, you should use it everywhere.
If you use Solady ERC20 contracts, you should avoid using OpenZeppelin ERC20 contracts, etc.
The code should derive from a uniform set of principles/assumptions and use the same design language.
4. Documentation
Do you think you could figure out what this function does from Sablier V2?
If the answer is “Yes”, that's likely because of documentation.
But, it's also worth remembering that having lots of documentation is not an excuse for not having self-documenting code.
The variable names and function names in this code are so clear that ChatGPT probably could have annotated the code.
It's both self-documenting and heavily documented.
5. Extensibility
Because security concerns make Solidity contracts effectively immutable, extensibility is the best way to insure against further changes in scope.
It's also a good way to simplify and incentivize integration.
Patterns like Uniswap V4 hooks and Gnosis Safe{Core} are pioneering new highly extensible protocols.
6. Verifiability
Well designed contracts are written in a way that facilitates understanding why it is secure.
This is different from just being readable.
Smart contracts can be readable but difficult to reason about.
Verifiable contracts are explicitly organized to make their security properties easier to infer and understand.
Maple Finance invariants are a good example of this:
7. Aesthetics
Last but not least, in my opinion, protocol aesthetics matter too.
Zora V3 is a delightful protocol to read:
This encourages more eyes on the code and makes it less of a chore to read it.
A win in many aspects.
Use these principles and it will go a long way towards helping you build a protocol that achieves Usability, Composability, Readability and Security for all users.