# Bulletproof SolidityπŸ›‘οΈ Source: https://flawsomedev.com/blog/ideal-defi-protocol-architecture-solidity-template Kiwi News: https://news.kiwistand.com/stories/Bulletproof-Solidity?index=0x6687c6386da29b4f9c54dc70208e7765c72fa67d0dae00ccfc889829730baedddf1436ec Submitted by wongssh.eth on 2024-07-05 UTC, 1 upvote, 0 comments ## Summary The article proposes a framework for organizing Solidity smart contracts and their tests so that codebases stay clean, modular and easy to navigate as projects grow. It is inspired by Aave V2's repository and is presented for DeFi protocols, though the author says it can be adapted to other projects with minor adjustments. Contracts are split into interfaces, mocks and protocol directories, with the protocol directory further divided into common, libraries, misc, core, integrations and tokenization. Tests are grouped by type (common, unit, fork, fuzz, invariant) and then by feature, with contract names suffixed by the test type so CI can run selected suites, such as forge test --mt UnitTest. ## Article Often, I see developers struggling to organize their smart contracts. While this may not seem like a big deal initially, it can lead to maintenance challenges in the future. As the complexity of a project grows, poorly organized code can slow down development. Proper planning in the early stages of a project can prevent a lot of technical debt, making the codebase clean, modular, and easy to navigate. It not only simplifies the development process but also facilitates collaboration among team members, as a well-structured codebase is easier for new contributors to understand and work with. Inspired by Aave V2's repository, I am going to share a simple yet efficient framework for organizing smart contracts in the context of a DeFi protocol. With minor adjustments, this framework can also be adapted for organizing other types of projects as well. Checkout the foundry template here. Directory structure for Contracts β”œβ”€β”€ interfaces // All the interfaces used in the project goes here β”‚ └── external // Group external interfaces in this dir β”œβ”€β”€ mocks // Mock contracts for testing and simulations └── protocol // Core protocol specific contracts β”œβ”€β”€ common // Common shared contracts like AccessControl, etc., β”œβ”€β”€ oracles // Price oracles. β”œβ”€β”€ libraries // Utilities like Math.sol, Errors.sol, goes here. β”œβ”€β”€ misc // Miscallaneous contracts like RewardManager, etc., β”œβ”€β”€ core // Core protcol contracts like Vaults, Pools, etc., β”œβ”€β”€ integrations // External integration logic should be seperated. | β”œβ”€β”€ aave β”‚ └── uniswap └── tokenization // Protocol specific tokens like ERC20, ERC1155. Interfaces The interfaces directory houses all the interfaces utilized throughout the codebase. I recommened using interfaces for almost all the core contracts since it helps define the required functions and params before hand. External Interfaces: Grouping external interfaces within the external subdirectory for a clear distinction, when integrating with other protocols. Mocks The mocks directory is dedicated to mock contracts used primarily for testing and simulations. Mocks replicate the behavior of real contracts, allowing us to test various scenarios and interactions without the need for actual deployments. Protocol The protocol directory hosts the core logic and components of the protocol. We can further divide it into subdirectories as follows: Common: This subdirectory contains shared abstract contracts such as AccessControl, etc., that are supposed to be inherited by almost all the contracts across the protocol. Libraries: Utilities like DataTypes, Errors, Math, etc., should reside in the libraries subdirectory. These contracts provide essential functions and definitions that are reused across the protocol. Miscellaneous (Misc): The misc subdirectory includes independant contracts that do not fit neatly into other categories. For instance, a RewardManager contract might manage reward distribution mechanisms. We need not have a seperate directory for a such contracts. Core: Central to the protocol, the core subdirectory contains the main protocol contracts such as Vaults, Pools, etc., These contracts implement the primary business logic of the protocol. Integrations: The integrations subdirectory is designated for contracts that manage external integration logic. This is applicable for composed protocols. By isolating external integration-related code, we can efficiently manage and update logic related to external protocols. Further grouping it based on the protocols within this directory, like aave and uniswap, enhances maintainability as we can easily add or remove integrations. Tokenization: Finally, the tokenization subdirectory hosts protocol-specific ERC20/ERC721/ERC1155 tokens. For example, proof-of-deposit tokens, share tokens, position token, etc., By incorporating the above structure, the code becomes easily approachable. Due to seperation of concern, addition of new features will become hassle-free. Most importantly when the project grows, we can easily figure out what goes where. Also it makes the codebase very easy to test. Shameless plug πŸ”Œ: When using this framework, file import and paths could be difficult to manually type the path to import files. If you use VSCode IDE, you can leverage Solidity Inspector extension that helps you with auto-file import suggestions Directory structure for Tests Tests can be organized in the same way as above as well. We can group tests by its type then by feature. └── test/ β”œβ”€β”€ common/ β”œβ”€β”€ unit/ β”œβ”€β”€ fork/ β”œβ”€β”€ fuzz/ └── invariant/ The common directory should contain the harness contracts and other base test setup contracts that can be shared among other test contracts. . └── common β”œβ”€β”€ harnesses β”‚ β”œβ”€β”€ VaultHarness.sol β”‚ └── PoolHarness.sol β”œβ”€β”€ BaseTestSetup.sol └── BaseTestSetupLive.sol Other directories can have test files grouped by feature. └── unit/ β”œβ”€β”€ vault/ β”‚ β”œβ”€β”€ trees/ β”‚ β”‚ β”œβ”€β”€ Deposit.tree β”‚ β”‚ └── Withdraw.tree β”‚ β”œβ”€β”€ Deposit.t.sol β”‚ └── Withdraw.t.sol └── oracle/ ... You can see that we grouped tests based on the type then the feature. This helps to keep track of features and functions that are tested by just looking at the file names. If we try to add all the tests in the same file, say Vault.t.sol, there are couple of issues: Test files will become too big. Difficult to keep track of the logic that has been tested/not-tested. πŸ’‘ Note that we have a seperate directory for trees. It is used to laydown the testcases in a tree format before implementing the actual tests and its called Branching Tree Technique (BTT). It was first introduced by PaulRBerg from Sablier and it soon became an industry standard. β€” Merlin Egalite | Morpho | Woke up Based (Hiring)πŸ’™ (@MerlinEgalite) August 5, 2023 As the tests are seperated based on the type, the test contracts should be named appropriately to enhance the testing pipeline. This can be done by just appending the type of the test to the test contract name. For ex, Vault tests can be named as: contract Vault_UnitTest is BaseTestSetup {} contract Vault_ForkTest is BaseTestSetupLive {} By doing so, we can selectively run the required tests during development. Also it would be easier to configure the CI test pipeline as well. If we wanna just run the unit tests, we can just run the command: forge t --mc UnitTest. This will become handy when we want to run selective test suites. CI Workflow config example: ... - name: Run Unit tests run: | forge test --mt UnitTest -vvv id: test - name: Run Fork tests run: | forge test --mt ForkTest -vvv id: test ... Conclusion In this piece, we looked into a framework to organize smart contracts in a structured way to avoid clutter. This will reduce the mental burden of remembering what goes where when adding or removing features. I've also added a Foundry template based on the above points to help you get started easily. Happy coding! Acknowledgements This piece of article is inspired from "Bulletproof Node.js"