Wake Solidity Development and Testing Framework

repository·main·Indexed 18 days ago

https://github.com/ackee-blockchain/wake

Wake is a Python-based fuzzing and testing framework for Solidity smart contracts, available as the eth-wake package. It provides automated property-based fuzzing, manually-guided fuzzing, and static analysis detectors to identify vulnerabilities. The framework includes a comprehensive Intermediate Representation (IR) API for program analysis, a detector API for building security checks with confidence and impact levels, and tools for running tests in parallel.

Tokens
83.4K
Snippets
259
Records
368
Agent score
63%

What's inside Wake

  1. Overview of the Wake development and testing framework

    main

    Wake is a Python-based framework designed for testing and deploying Solidity smart contracts. It is built on top of pytest and uses pytypes (Python equivalents of Solidity types) to provide auto-completion and type checking for scripts. This allows developers to catch typing errors early and write more ergonomic deployment and test scripts.

    Key capabilities include:

    • Type Safety: Auto-completion and type checking via pytypes.
    • Fuzzing: A property-based fuzzer that uses multiprocessing for high-throughput input testing.
    • Debugging: Integrated ipdb debugger (attached on test failures), call traces, and console.log support.
    • Deployment: Support for deployment and mainnet interaction scripts.
    • Cross-chain: Support for testing across multiple chains.
    • Performance: Optimized for higher performance compared to many existing Python or JavaScript frameworks.
  2. Overview of Wake features

    main

    Wake is a Python-based Solidity development and testing framework designed for smart contract security and development. Key capabilities include:

    • Testing: A testing framework built on top of pytest.
    • Fuzzing: A property-based fuzzer for discovering edge cases.
    • Security Analysis: Built-in vulnerability and code quality detectors, plus a static analysis framework for creating custom detectors and printers.
    • Data Extraction: Printers for extracting specific information from Solidity code.
    • Deployment: Support for deployments and interacting with mainnet.
    • Tooling: Includes a solc version manager, a Language Server Protocol (LSP) server, and a VS Code extension called Tools for Solidity.
    • CI/CD: GitHub Actions available for setting up Wake and running detectors.
  3. Common Testing Patterns in Wake

    main

    Wake provides several standardized testing patterns for smart contract development. These patterns help developers focus on critical aspects of contract behavior such as balance tracking, state changes, and complex multi-token interactions.

    Key testing patterns include:

    • Account Balance Testing: Verifying changes in token or native currency balances.
    • Multi Token Interaction: Testing scenarios involving multiple different tokens.
    • State Change Tracking: Ensuring contract state variables update correctly after transactions.
    • Test Flow Branching: Managing complex test logic and conditional execution paths.
  4. LSP Features in Wake

    main

    The Wake LSP server provides several developer productivity features for Solidity development:

    • Navigation: Go to definition, Go to type definition, Go to implementation (for unimplemented functions/modifiers), and Find references.
    • Hierarchy & Structure: View Type hierarchy (for contracts and virtual functions) and Document symbols.
    • Information: Hover (includes OpenZeppelin documentation links), Document links, and Code lens (showing number of references above declarations).
    • Refactoring: Rename symbols.
    • Diagnostics: Displays compiler errors and results from Wake vulnerability detectors.
  5. How printer loading priorities work

    main

    Wake loads printers from multiple sources, and if multiple printers share the same name, they are prioritized according to the following hierarchy (from highest to lowest priority):

    1. Project-specific printers: Located in the ./printers directory of your project.
    2. Global printers: Located in $XDG_DATA_HOME/wake/global-printers.
    3. Printer packages (plugins): Such as wake_printers. If multiple packages provide the same printer, they are loaded in alphabetical order of their module names (the first package in alphabetical order has the lowest priority).

    To inspect which sources are providing which printers, run wake print list.

    # See the list of available sources for each printer
    wake print list
  6. Construct and manage Accounts

    main

    An Account is an Address bound to a specific Chain. If no chain is specified during construction, the global chain object is used.

    Important Constraints:

    • Address and Account instances cannot be compared.
    • Account instances from different chains cannot be compared using < or >.
    • Most API functions do not accept Account instances from different chains. To bypass this, use the .address property of the account.
    from wake.testing import Account, Chain, chain
    
    other_chain = Chain()
    
    # Account bound to global chain
    acc1 = Account(0)
    assert acc1 == Account(0, chain)
    
    # Account bound to different chain
    acc2 = Account(0, other_chain)
    assert acc1 != acc2
  7. How FuzzTest works in Wake

    main

    Fuzzing in Wake is implemented via the FuzzTest class. A fuzz test consists of multiple sequences, where each sequence is an independent test case (all connected chains are reset). Each sequence is composed of a series of flows (atomic test steps).

    To run a fuzz test, call the .run() method on an instance of your FuzzTest subclass with the following arguments:

    • sequences_count: The number of independent test sequences to execute.
    • flows_count: The number of flows to execute within each sequence.

    Inside the test class, you can access self.sequence_num and self.flow_num (both 0-indexed) to track the current progress.

    class CounterTest(FuzzTest):
        ...
    
    CounterTest().run(sequences_count=10, flows_count=100)
  8. Navigate the IR tree structure

    main

    The IR tree follows specific structural rules that define how nodes relate to one another:

    Hierarchy Rules

    • SourceUnit is the root node of the entire IR tree.
    • FunctionDefinition and ModifierDefinition nodes act as containers for statements.
    • Statements can contain other statements or expressions.

    Expressions outside of function/modifier bodies

    Expressions are typically nested within statements, but they can appear directly in these specific contexts:

    • InheritanceSpecifier argument lists (e.g., contract A is B(1, 2) {})
    • StorageLayoutSpecifier base slot expressions (e.g., contract C layout at (10 + 20) {})
    • ModifierInvocation argument lists (e.g., function foo() public onlyOwner(1, 2) {})
    • VariableDeclaration initial values (e.g., uint a = 1;)
    • ArrayTypeName fixed length values (e.g., uint[2] a;)

    Node Referencing

    Certain nodes reference other nodes (specifically declarations) using these mechanisms:

    • Identifier: A simple name reference (e.g., owner referencing a variable).
    • MemberAccess: A member access reference (e.g., owner.balance).
    • IdentifierPathPart: A helper used in IdentifierPath to describe dot-separated paths (e.g., Utils.IERC20).
    • UserDefinedTypeName: A reference to a user-defined type (e.g., MyContract in new MyContract()).
    • ExternalReference: A helper describing a YulIdentifier that references a Solidity VariableDeclaration (e.g., assembly { mstore(0, owner) }).
  9. Understand Wake configuration precedence

    main

    Wake loads configuration from multiple sources. If a setting is defined in multiple places, the source with the highest precedence wins. The order of precedence (from lowest to highest) is:

    1. Default values (built into Wake)
    2. Global configuration file
    3. Project configuration file
    4. Environment variables
    5. Command-line arguments
  10. Generate parameters based on deployment strategy

    main

    When testing factory contracts, parameters must be encoded to match the specific struct required by the chosen StrategyType. In the provided example, the following mapping is used:

    Strategy IndexStrategy TypeRequired Fields
    0BASICadmin (address), value (uint256)
    1ADVANCEDadmin (address), name (string), version (uint8), config (uint256)
    2UPGRADEABLEadmin (address), implementation (address), initData (bytes)
    3PROXYadmin (address), logic (address), proxy (address)

    Ensure that your helper methods (e.g., _encode_basic_params) correctly encode these fields into bytes before passing them to the contract's deploy function.

  11. Use the global `chain` variable for single chain tests

    main

    For single chain testing, Wake provides a global chain variable which is an instance of the Chain class. You can use this object to access chain data (like blocks and accounts) or modify chain parameters (like gas price or automine settings). For cross-chain testing, you should create additional Chain instances instead of relying on the global one.

    from wake.testing import chain
    
    def test_chain():
        # Use the global chain object
        print(chain.chain_id)
        print(chain.accounts)