Solidity development guide¶
Summary¶
This is the Atix's Development guide for Smart Contracts, in particular anything that has to do with Solidity. This document will try to go from outlining the more high level aspects of the Smart Contracts development to give some specific detail that will have to be checked by anyone at Atix when developing such pieces of software.
This last part is planned to evolve to a more comprehensive guide to internally audit contracts, which will have to be reviewed before starting to develop any contract, before submitting any smart contract to an internal and/or external audit and while doing an internal audit.
Responsibility over this document¶
This document will be the responsibility of the leader of the Solidity Hub and will be checked with the CTO for any major change. However, anyone at the Solidity Hub is not only expected to read this but also to maintain it doing fixes, proposing new tools, proposing changes to the tools, and whatever it thinks is good for this document.
Tools¶
All of the tools that follow are expected to be used by any new project:
-
Hardhat as framework
-
Hardhat Network as local node
-
Solidity 0.7.6 as the language we write in
-
Solhint as linter
-
ZOS Hardhat plugin as Upgradeability framework (every project should have GSN unless specified otherwise)
-
Openzeppelin Contracts as a source of audited base SC
-
Mocha(structure), Chai(assertion library), and Waffle(fixtures and advanced assertion) as our main tools to test
-
Gherkin as a language to describe the test cases
-
NVM as the version control manager for node (with its respective .nvmrc)
-
Solidity-coverage as a tool to measure coverage
-
Every function, contract, event and modifier (except for the mocked ones) should be documented according to this specification
-
A human readable doc should be able to be generated using an npm script through harhat-docgen
Important
All of these previous tools are available and installed within this template.When starting a new project, please fork from that template.
The following tools are only a recommendation as they are only used by every developer separately:
- VSCode
- Solidity extension by JuanBlanco
- Solidity visual developer by tintinweb
- Sol-profiler by Aniket-Engg
- Mocha Test Explorer by Holger Benl
- Tutorial run tests from VSCode
General guidelines¶
The following guide should be the standard approach for building Smart Contracts within Atix:
-
Favor composability over inheritance(Smart Contracts have a max size, so Smart Contracts size should be periodically controlled as it adds great risk to the project). In this link you can find some really interesting hints to reduce contract size.
-
Use console.logs in as many parts of the Smart Contract as possible, it will help debug any local issue and it does not have any drawback
-
Try to keep functions small and with few params and internal variables(any function with more than 10 of these two, combined, should be a big redflag)
-
Do not set the exact version of solidity within the pragma expression BUT do it on the framework config file, with as much specificity as possible(version, optimizations, evmVersion)
-
Export an NPM package with the ABI in Typescript for other develops to integrate with our SC with ease
-
DO NOT test manually any smart contract feature directly(this is usually wasted time, the time spent doing this is better spent coding an automated test, only acceptable as a side resource if an automatic test is not behaving as expected)
-
All of the functions should be tested automatically and the coverage should be 100%
-
All of the complex functions should be unit tested through inheriting Mock contracts
-
All of the UATs should have at least one integration test associated
-
Every contract suite should have been deployed to a public testnet(for at least a month), have a pre-audit made, a formal audit made by a third party before being deployed in the mainnet.
-
If the contract suite has a privileged address to perform admin operations, use a multisig for the production development and keep the priv keys of said wallets secure(do NOT transfer them over slack, mail or any other chat, use a proper tool for that)
-
Create typescript bindings out of the Smart contracts with Typechain and if possible create an NPM package for other teams that want to integrate with your Smart Contracts Suite
-
Consider writing an NPM script for things you do often. It will probably be of use for other team members and any other person that comes across the SC suite
-
When building a new protocol, consider your whole ecosystem. Some protocols may be of help to be used as a dependency, base or integration system; others may put risk in your suite(give special attention to pools that offer flash loan pools).
-
When integrating with another protocol, understand and document the risks of using it, and make sure that the integration is worth the risk
-
Use standards as much as possible. The use of ERC20 for anything that resembles a balance is what gives DeFi most of its power(however, understand the implications of using it). Using ERC721 may be useful in some other cases too.
-
Try not to expose enums to other contracts if doing something upgradeable
-
The Solidity Naming Convention should be followed
-
To send ethers to another account use .call() with a reentrancy guard and/or a Checks-Effects-Interactions Pattern. Reasons explained here and here
-
To versionate builds use Semantic Versioning.In order to this automatically use Standard version. Standard version reads the commit history(so you have to use Conventional Commits to make it parseable) and automatically generates tags, a changelog and redefines the versions in the package* files.
-
Remember to check the evmVersion. It should be the correct one. In truffle or hardhat config should be specified. Keep in mind each version support different features and compatibility between them is not guarantee.
-
For every project, setup CI so that it at least runs the tests defined for the project, the default security check defined in the template through Slither and the linter for every PR. The PR should not be mergeable if the CI pipeline fails.
-
Structure the tests so that they follow the Gherkin syntax, using only one WHEN(using describe) nested per GIVEN(describe)(the GIVEN definition could be repeated, use fixtures in that case) and one or more THEN(it) per WHEN. To have a clear picture of this please look at the solidity template.
-
Avoid deep inheritance usage i.e. the height of an inheritance tree should not be very tall, ideally it should not have a height of more than 3 contracts. This is a common measure in OOP and it is specially relevant here because it too much height in said graphs adds too much complexity in a piece of software that should be super clear and easy to audit/maintain.
-
Use explicit return statements in every function that returns something
-
Name the returned variables in the signature of the function if the function returns two or more results
-
Do not write/use modifiers that have side-effects
-
Do not write empty functions(including constructors), if you want a placeholder build it so that it reverts every time it is called, they often lead to wrong assumptions(the sole exception is where you are building a Template pattern, but be very clear about it)
-
Every time you exclude some linting/static analysis rule be as specific as possible(try to disable it line by line first, if that becomes cumbersome try to disable it by file, and if that also becomes cumbersome, which shouldn't be that often, disable it in the project) and add a explanation that supports your decision.
-
If adding some kind of randomness to your suite of contracts, add a well-proven battle-tested source of entropy as well as an alike mechanism to build on top of said source.
-
Remember that the data you upload to the (Ethereum, RSK, BSC) blockchain is never private.
-
Avoid using timestamps to measure short periods of time. The miners have some kind of malleability over it
-
Do not make assumptions over the order of the transactions(keep an eye out for front running attacks)(keep in mind that allowance/approve is flawed and can be frontrun, use other mechanisms like incrementAllowance/decreaseaAllowance)
-
Multiply before diving to avoid loss of precision but keep in mind that integers have a maximum(about 70 digits)
-
Do not make assumptions about your balance, anyone can send you ethers even if you don't write any payable functions and restricted the fallback function
-
Avoid using tx.origin, phishing attacks become problematic
-
Generate a gas report for every release(use
npm run report:gas
if you are using the template, but remember to update tha params in the hardhat config file) -
Do not optimize based on feelings, optimized based on data. A gas report is your friend with this type of tasks
-
Prefer a pull mechanism over a push mechanism to send payments from your Smart Contract suite. It is the best way to eliminate for certain some problems like this and this at the same time, among others.
-
If you are integrating with an external token, keep in mind that there are risks associated. You should know the implementation of said tokens and check the following checklists: crytic and consensys. A great talk around this topic can be found here
-
Keep in mind this checklist: https://secureum.substack.com/p/smart-contract-security-101-secureum
-
At least include a documentation about migration process. It could be upgradability or not. Maybe there are project where no owner can do the upgrade so the migration process is a re-deployment of the contract with new parameters. Whatever the approach is, it should be documented.
Security details¶
The idea behind this section is that it serves as a guide when a lightweight security audit is being made or when a codebase is being submitted to a full-fledged one. Before starting the audit check that every point in the previous section is met, specifically the ones regarding testing and linting process; and that you know how to run slither(the recommended way is through the docker container, sharing the whole project through a volume to that docker container, ENTERING the project root directory, and run slither from there; you can check the solidity template repo to get a more detailed picture of this).
Once you have everything setup, create a new spreadsheet with the From template option and select the SC security audit template - Atix template. There you should:
-
Set the project name in the Project field and in the title of > the document
-
In the Execution 1, fill all the necessary initial fields(except > for the end date, obviously). All of them are important!
-
For each of the items there, verify it and select the appropriate > result. PLEASE, DO NOT REMOVE LINES, if one of the lines does > not apply just select Not applicable. If one of the fields is > disapproved please, provide as much detail as you can in the > Comments section, explaining what > function/functions/variable/struct/type fails this item, why does > it fail, and, if you can, a possible solution.
-
Once all of the items are either Approved/Disapproved/Not > applicable, fill the end date and make the team know that the > audit has been completed.
If you have to redo an audit for a project that already has one, in the file of the previous audit, create a new Execution column and start from point 2.
Happy hacking!
If you want to do a security audit more thoroughly go through every point in https://swcregistry.io/ and https://secureum.substack.com/p/smart-contract-security-101-secureum