Skip to content

Commit

Permalink
Merge pull request #11 from OpenZeppelin/initial-commit
Browse files Browse the repository at this point in the history
initial cookbook
  • Loading branch information
peersky authored Mar 26, 2024
2 parents 7c8fd97 + 15e3f6c commit 9b6dff7
Show file tree
Hide file tree
Showing 9 changed files with 332 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/flat-emus-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openzeppelin/secure-development-cookbook': minor
---

Initial cookbook
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# The Secure Smart Contract Development Roadmap

_The essential blueprint for crafting secure protocols_

In 2023 alone, decentralized protocols were hacked for a combined value of $1.8 billion. The persistence of security breaches, even in systems that have gone through multiple security audits, raises critical questions.

Why do these vulnerabilities persist, and how can developers minimize the risk of these issues?

Smart contract protocols, as ultra-critical pieces of immutable software, require a more thorough and carefully designed development process than traditional applications. A minor oversight can lead to repercussions of monumental scale, with potential losses reaching into the billions. This high-stakes environment demands a paradigm shift in development practices, one that mirrors the rigor applied in the creation of the mission-critical systems like aviation and healthcare. Embracing this approach from day one enhances the effectiveness of each subsequent stage, serving as the secret to ensuring an exponential reduction in the likelihood of errors throughout the development process and culminating in a clean and secure codebase.

The answer to creating secure protocols lies not in the frequency of security audits but in their effectiveness, as well as in the ability to conduct effective failure-based stress testing . A common misconception is that a security audit is the silver bullet for all potential security flaws. However, the reality is more nuanced. Audits depend heavily on humans reading code and the efficacy of that process can vary greatly depending on the condition of the codebase. By following a series of steps and thought processes, you can significantly enhance the auditability of your protocol, thereby reducing the likelihood of missed issues.

The purpose of this guide is to provide a structured approach to ensure that your protocol is properly tested and optimized for a thorough and effective examination. The guide will also cover what to do after an audit in order to interpret its results correctly and safely deploy and monitor your contracts. The secure development lifecycle can be segmented into six main phases: Plan, Code, Test, Audit, Deploy, and Monitor. These are the thought processes that distinguish secure protocols from insecure ones.

## Contents

### [1. Planning](./chapters//01_planning.md)

- 1.1. Technical specifications
- 1.2. Threat Modeling

### [2. Code](./chapters//02_code.md)

- 2.1. Code clarity
- 2.2. Code analysis
- 2.3. Documentation
- 2.4. Defensive Programming

### [3. Testing](./chapters//03_testing.md)

- 3.1 Functional testing
- 3.2. Security testing
- 3.3. Testing Processes
- 3.4. Fuzzing

### [4. Auditing](./chapters//04_auditing.md)

- 4.1. Internal reviews
- 4.2. Before the audit
- 4.3. During the audit
- 4.4. After the audit

### [5. Deployments](./chapters/05_deployments.md)

- 5.1. Deployment Scripts
- 5.2. Testing your deployments on a fork
- 5.3. Secure Upgrades
- 5.4. Choosing the right deployment and upgrade stack

### [6. Operating](./chapters//06_operating.md)

- 6.1. Before touching web3
- 6.2. Incident response plan
- 6.3. Plan monitoring in advance
- 6.4. Deploying Monitoring
25 changes: 25 additions & 0 deletions chapters/01_planning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Plan

## 1. Technical specifications

Creating a high-quality specification for code involves clear definition of the requirements, functionalities, features, and constraints of the protocol before the actual coding begins. It serves as source documentation, a guide for developers and stakeholders to ensure that the final product meets the initial vision and requirements.

Architecture Overview: Provide a high-level overview of the protocol architecture, including major components and their interactions. Define every functionality along with its input and output requirements. To do this, you can use a framework of pre-conditions, post-conditions, and actions in a logical format as outlined in this guide.

Constraints: Outline any limitations such as time, budget, or technology, that could impact the development. Include major milestones and plans for future audits.

Dependencies: List any external systems, libraries, or services that your project will rely on.

Battle-Tested Code: Use battle-tested libraries like OpenZeppelin contracts whenever possible in order to minimize the error surface.

Contracts Flow Diagrams: Visualize how the smart contracts will interact with each other.

Development Toolchain: Specify the development, testing, deployment, and monitoring tools necessary for the project.

## 2. Threat Modeling

**System-wide Invariants**: Define invariants across the system using a logical format. These assertions, which are expected to always hold during execution, are critical for effective testing, auditing, and monitoring. Clarifying these immutable properties is essential for planning security requirements.

**Assess Integration Risks**: Evaluate the security risks associated with integrating other protocols. Consider the security measures of these protocols and establish contingency plans for potential failures or loss of availability, such as utilizing external oracles or yield protocols.

**Document Potential Threats**: Identify and document potential threats, drawing inspiration from the [ImmuneFi severity classification system](https://immunefi.com/immunefi-vulnerability-severity-classification-system-v2-3/). This process involves a thorough analysis of vulnerabilities and their possible impact on your system.
56 changes: 56 additions & 0 deletions chapters/02_code.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
## Code

### 1. Code clarity

_“Code is clean if it can be understood easily – by everyone on the team. Clean code can be read and enhanced by a developer other than its original author. With understandability comes readability, changeability, extensibility, and maintainability”_

[Clean Code by Robert C. Martin](https://gist.github.com/wojteklu/73c6914cc446146b8b533c0988cf8d29)

**Structure and Style**: You want the general structure and style of your contracts to be extremely clear. You can achieve that by following conventional practices outlined in the Solidity style guide.

**Self-contained code**: Your code should aim to be self-contained, which means that functions should have a clear and singular purpose, avoiding the use of flag-type parameters. If necessary, consider breaking down a function into multiple smaller functions, each performing a distinct action. This same principle should be applied to contracts themselves. Strive to keep contracts as concise as possible, and if complexity arises, consider dividing them into multiple contracts.

**Naming**:Use declarative, clear, and consistent naming. If some naming is unclear or uses terminology of your protocol, explain it in the comments. Replace magic numbers with named constants.

**Dependencies**: Whenever possible, follow the principle of least knowledge. Ensure you only inherit from battle-tested libraries. Include any unaudited dependencies in the audit scope and check for common vulnerabilities using Defender’s code module dependency checker.

**Input Validation**: Go over each input and define clear logical pre-conditions when needed.

- Always assume input can take any value.
- Always assume functions can be called in any order.
- Always assume frontrunning is possible and an attacker can do any call right before a user with knowledge of their actions.

## 2. Code analysis

Use a static analysis tool like [OpenZeppelin Defender Code Inspector](https://docs.openzeppelin.com/defender/v2/module/code) to automatically verify for common vulnerabilities and style violations. Defender integrates with GitHub to be a part of your CI/CD process, giving you feedback on every PR.

## 3. Documentation

Your code needs to be properly documented to ensure auditors understand its purpose and intended functionality. Poor documentation leads to auditors spending more time understanding your code vs trying to break it, hurting the effectiveness of the audit.

Use the NatSpec format to document every contract, library, function, event, and storage variable.

- Document all your mathematical and financial models with clear explanations and/or proofs.
- Document all your assembly blocks in detail, explaining what they are supposed to do.
- Make use of diagrams as much as possible, to give an overview of the system and visualize the flow of contract interactions.
- Document your threat modeling and assumptions.

## 4. Defensive Programming

When developing your protocol, adopt the perspective of an attacker to audit your code. This process encompasses several crucial aspects:

1. Review audit reports and issues identified in similar protocols. Often, systems with similarities are vulnerable to the same types of human error, stemming from analogous reasoning about their mechanics.
2. Evaluate every user-controlled parameter. Is there a potential for a function to be called with an unexpected parameter?
3. Consider the implications of every external call. Are you delegating execution to an untrusted contract? Does this delegation occur in a state that is not consistent?
4. Evaluate the sequence and circumstances of function executions. Despite the system's intended use, there's potential for manipulation in ways not originally anticipated. Functions could be executed in various orders or combined into a single transaction, leading to unintended outcomes. Additionally, consider scenarios where external factors, such as the availability of flash loans, introduce new dimensions of risk. Reflect on the potential consequences of such manipulations, taking into account both the internal mechanics and external influences like market volatility and frontrunning.
5. If your protocol involves financial transactions, carefully examine the flow of value. How could value be diverted? What factors does it rely on? Are the incentives correctly aligned?

In summary, a key strategy in preparing for an audit is to proactively attempt to audit your own protocol, with a mindset geared towards identifying and exploiting system vulnerabilities.

To learn more about this type of thinking, we recommend reading samczsun’s blog.

**Use the check-effects-interactions (CEI) pattern**: Despite being the most known and documented vulnerability type in blockchain, reentrancy is still one of the most common causes of hacks in 2023 with read-only reentrancy taking the lead.

Always use CEI when possible and in the uncommon case when it’s impossible, document it clearly and ensure all functions, including view ones, are [protected against reentrancy](https://docs.openzeppelin.com/contracts/4.x/api/security#ReentrancyGuard).

If you read or use other protocols, ensure they apply CEI and are not vulnerable to read-only reentrancy. If you are not sure, document it and explain the concern to your auditors.
49 changes: 49 additions & 0 deletions chapters/03_testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
## Test

There are three categories of tests, all of which should have a fully-fledged test suite with a unique set of quality criteria:

### 1. Functional testing

Ensures that every happy path through the software system works as intended. This encompasses both state changes and return values.

- **Unit tests**: Test one function invocation or multiple functions on a single contract (while optionally mocking other parts of the system).
- **Integration tests**: Evaluate the interactions between multiple contracts in the codebase and with already deployed external contracts. Establishing a forked blockchain state is essential for accurately simulating realistic integration states, allowing for the exploration of different scenarios, such as the failure of an oracle. Furthermore, mock versions of integrations can be employed for simplicity, according to one's preference.
- **Deployment tests**: Test the entire set of contracts to be deployed, including the
deployment scripts, ensuring correct order and parameterization.

### 2. Security testing

Assures the absence of undesired functionality that could be misused to steal funds or bring the system into an undefined state.
**Negative testing**: Typically, a test case asserts that a certain transaction reverts.
**Authorization**: The test ensures that arbitrary senders cannot call the respective
function.
**Authentication**: Test ensures that the message sender is identified correctly.
**Re-entrancy**: The test ensures that re-entrancy cannot occur or is considered safe.
**Path equivalence**: If two different sets of state changes lead to the same semantic end state, the test case checks for technical equivalence (e.g., staking rewards).
Known exploits: Tests inspired by known exploits are adapted to the codebase.

### 3. Testing Processes: [Shift-Left Testing](https://en.wikipedia.org/wiki/Shift-left_testing)

**Test Suite Trigger**: Tests are automatically initiated by specific events in the CI/CD pipeline, such as commits, merges to the main branch, or other CI/CD-related occurrences. This ensures that testing is an integral, continuous part of the development process, facilitating early detection and resolution of issues. OpenZeppelin Defender’s code inspector provides code analysis on every PR, using dozens of detectors to enhance security, efficiency, and code quality.

**Test-Driven Development (TDD)**: Aligning with the Shift Left principle, tests are crafted prior to the development of code. This practice ensures that the system's intentions are thoroughly understood and addressed from the outset. Optionally, this process can be managed by a dedicated team, separate from the one responsible for development, to further ensure objectivity and comprehensive coverage of test cases.

**Behaviour-Driven Development (BDD)**: Following the principles of TDD, Behavior-Driven Development (BDD) takes a step further by integrating business and technical perspectives. BDD involves writing tests in a language that mirrors real-world scenarios, allowing non-technical stakeholders to participate actively in the development process. This approach not only clarifies the system's behavior from the user's viewpoint but also fosters collaboration across the team, ensuring the developed features precisely meet the project's requirements.

**Invariant & Spec Design**: Before the onset of implementation and testing phases, the design of formal invariants and specifications is critical. These serve as foundational elements that maintain their truth across the entirety of the implementation process.

By incorporating these strategies, the Shift Left principle emphasizes the importance of moving testing and quality assurance earlier in the development lifecycle. This not only helps in identifying and addressing defects sooner, when they are less complex and costly to fix but also aligns development efforts more closely with the intended design and specifications, ensuring a higher quality and more secure final codebase.

### 4. Fuzzing

Fuzzing is an automated testing technique that involves executing tests with thousands or millions of computer-generated inputs. Fuzzing is recommended to test parts of the code that are hard to reason for the human mind like integer arithmetic prone to rounding errors, assembly blocks, and complex data compression.

There are three types of fuzzing:

**Stateless**: This approach involves randomly generating inputs and immediately checking the results. After each test, the system is reset before proceeding with the next set of inputs.

**Stateful**: In this method, the fuzzer is given the autonomy to decide which functions to call and with what inputs. The tester defines an invariant that must be maintained. For example, the total sum of balances should not exceed the total supply.

**Differential**: Differential testing involves using fuzzing on two different implementations of the same functionality and comparing the results. If libraries A and B implement the same feature, their outputs should match any given input. This is especially useful for math libraries, where you can compare your math against a Python implementation to check for rounding errors.

When choosing input ranges for fuzzing, think about limiting compute by removing value ranges you already know with certainty will revert or produce a successful result. Also, think about covering enough input ranges that will cover all branches of the control flow.
23 changes: 23 additions & 0 deletions chapters/04_auditing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Audits

## 1. Internal reviews

Internal audit processes play a crucial role in enhancing the security posture of a project before it undergoes external scrutiny. While many teams may not have dedicated security researchers in-house, fostering a culture of security-mindedness among developers during the internal review stages is invaluable. By implementing rigorous peer review practices, such as thorough examination of pull requests (PRs), teams can identify and mitigate potential vulnerabilities early in the development cycle. This internal layer of defense, although optional, complements external audits by ensuring that the codebase is as robust as possible from the onset. Encouraging developers to adopt an auditor's mindset helps bridge the gap between development and security, paving the way for a more resilient protocol.

## 2. Before the audit

Preparing thoroughly for an audit can significantly enhance its effectiveness and efficiency. A key step in this preparation is defining the scope of the audit at least two weeks in advance. This foresight allows auditors to familiarize themselves with the project's intricacies and tailor their strategies accordingly. It's crucial to adhere strictly to this predefined scope; any alterations can disrupt the audit's timeline and undermine the auditors' preparation, potentially affecting the quality of the audit. Additionally, providing a frozen commit of the codebase that remains untouched during the audit until the review of the fixes phase ensures consistency and clarity in the audit process. This approach minimizes confusion and allows for a focused and thorough examination of the code at its state during the time of freeze, leading to a more effective identification and subsequent remediation of vulnerabilities.

## 3. During the audit

A security audit is a collaborative endeavor involving both developers and auditors. To facilitate auditors in performing their duties, it's imperative to supply them with comprehensive details about your protocol's functionality, edge cases, integrations, and any areas of concern. Auditors aim to rigorously test every aspect, as errors frequently lurk in the most unforeseen places. However, being informed about the areas developers are most uncertain about can still be beneficial.

Throughout the engagement, it is crucial to respond to the auditors' inquiries promptly and with precision to maximize the efficiency of the audit time and ensure a thorough understanding of the protocol.

## 4. After the audit

After an audit, your team has to assess if the code is ready to be deployed to the blockchain. Sometimes when systematic critical issues are found, the code is deemed not ready and needs to go back to the development stage. To determine if your code is ready, see if the high/critical severity issues found are systematic, meaning they arise from a flawed architecture of your system and can’t be easily fixed.

Depending on the findings, consider whether the test suite is complete. If issues could have been found through testing, make sure to apply stronger QA before going live. If the audit yielded many critical issues, you’re sometimes recommended to get another audit.

After deployment, set up on-chain monitoring against invariants to ensure your team is quickly alerted of any abnormal behavior and set up an incident response protocol to act in case of a breach.
Loading

0 comments on commit 9b6dff7

Please sign in to comment.