Smart contract framework – The Brownie tutorial series–Part 1

Blog Banner

Before we begin, I would like to extend my sincere apologies to any and all epicures who may have stumbled upon this tutorial series. I am afraid you have been tricked by a wicked tradition that names smart contract development frameworks and tools after delicious desserts. This tutorial series does not talk about food. It talks about a development framework. You are more than welcome to check it out though.


This is the first of four articles that gives you a thorough walk-through of the smart contract development framework, Brownie. 

These articles will show you how to use the Brownie framework for building, testing, and deploying Solidity smart contracts. 

The Pythonista problem 

A development framework is a developer’s best friend. A framework helps accelerate the application development process by providing things like a base structure for the project, reusable code, useful libraries, testing and debugging functionalities, and, of course, easy shipping (deployment) methodologies. The more intricate a technology, the more useful a framework becomes. So, when we are dealing with a paradigm-shifting idea like Web3, it is imperative that there be some good frameworks out there that help ease the pain of Web3 application development. To be fair, there are a lot of amazing JavaScript/Typescript-based frameworks that do the job, and that is precisely the problem. When we scan the whole Web3 framework scene, we can see there is strong leniency towards JavaScript/Typescript. I love JavaScript, it is an amazing language. But I am a Pythonista, meaning I love Python more. When others like me try to start their Web3 development journey, how can we escape the async/await entanglement and use simple readable code for developing Web3 applications? What frameworks can we use? Well, let me introduce you to Brownie.

brownie logo

The Brownie solution 

Brownie is a Python-based smart contract development and testing framework. We can use Brownie to develop smart contracts that are compatible with Ethereum and other EVM-based networks. Why the leniency towards Ethereum, you may ask. Well, Brownie is built on top of the web3.py library. Now, for those who are new, web3.py is the Python library that we use in order to interact with Ethereum. Brownie supports both Solidity and Vyper (a Pythonic programming language) contracts. 

Note: If you are new, I highly recommend that you check out the web3.py library and familiarize yourself with the web3.py-based smart contract deployment and interaction. This will help you gain a better understanding of the overall process.

Brownie uses the pytest framework for unit testing. This enables the developers to leverage the potential of this feature-rich testing framework and write elaborate and powerful test cases for smart contracts. Brownie also comes with transaction debugging features that provide detailed insights into transaction failures or reversions.  

Note: The transaction debugging feature uses the debug_traceTransaction RPC method and the availability of this feature relies on your node provider. As of now, only a select few platforms like Chainstack support this RPC method. 

That’s it for an overview, now let us dive right in and develop some contracts using Brownie. 

How to install Brownie?

Before we start working with Brownie, we need to install certain dependencies. Since Brownie is a Python-based framework, the most obvious dependency would be a Python interpreter. So, here’s what I want you to do: 

  • Make sure you have Python (version ^3.6) installed on your system. 
  • Install the corresponding version of the python package manager (pip v21.3.1) .

Great, now that you have python3, you can install Brownie using the following command:

pip3 install eth-brownie  

To check if everything is installed, open the terminal and type brownie. If everything went well, it will display all the Brownie commands: 

$ brownie
Brownie v1.19.0 - Python development framework for Ethereum
Usage:  brownie <command> [<args>...] [options <args>]
Commands:
  init               Initialize a new brownie project
  bake               Initialize from a brownie-mix template
  pm                 Install and manage external packages
  compile            Compile the contract source files
  console            Load the console
  test               Run test cases in the tests/ folder
  run                Run a script in the scripts/ folder
  accounts           Manage local accounts
  networks           Manage network settings
  gui                Load the GUI to view opcodes and test coverage
  analyze            Find security vulnerabilities using the MythX API
Options:
  --help -h          Display this message
  --version          Show version and exit
Type 'brownie <command> --help' for specific options and more information about
each command.

Note: According to the official Brownie doc, a cleaner way of installing Brownie would be to use pipx. It helps install Brownie into a virtual environment. You can check the official doc for the pipx-based installation guide. 


Now, we need one more thing before we can use Brownie. We need Node.js support! (yes, the irony is not lost on me). So, make sure you have Node.js and npm installed on your system.

Once you have that installed, use the following command to install something called Ganache CLI: 

$ npm install -g ganache-cli 

Ganache helps you set up a local (Ethereum) blockchain network on which you can deploy and test smart contracts. It has both a GUI version and a CLI version. Brownie, by default, uses Ganache CLI as the local development environment. The Ganache CLI also provides you with 10 test accounts with 100 test ethers each. We can use these accounts for contract deployment and testing.

To check the Ganache CLI installation, use the following command:

$ ganache-cli --version 
#returns the Ganache version 

Now that we have everything, let us set up a project. 

How to set up a Brownie project?

To set up a new Brownie project,  create a new project folder and initialize the project using the following command:

brownie init 

This will set up an empty project structure in your folder:

├── build
│   ├── contracts
│   ├── deployments
│   └── interfaces
├── contracts
├── interfaces
├── reports
├── scripts
└── tests

Each of these directories is named according to the type of data that they will hold: 

DirectoryContent
/build Stores the compilation outputs and deployment information
(we will discuss this later).
/contracts Stores the contract files.
/interfaces Stores the interface files.
/reports Stores test coverage data and contract analysis reports.
/scripts Stores contract deployment and interaction scripts.
/tests Contains testing scripts.

This project structure helps us easily organize and manage our files while working with smart contracts. 


Now that we have set up a Brownie project, we can try and run a simple smart contract. For that, let us create a basic Solidity contract 

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract BasicContract {
    //a variable for storing numbers
    uint256 number;
    //function for storing a number
    function storeNumber(uint256 _number) public {
        number = _number;
    }
    //function for reading the number
    function readNumber() public view returns (uint256) {
        return number;
    }
}

Here is the link to the GitHub repository for code reference.

This contract stores a number and retrieves it upon user invocation. 

You can create a new file, basic-contract.sol, in the /contracts directory and copy and save the above code in that file. 

Once you have the contract file, you can compile the code by opening a terminal in the root directory of the project and typing the following command:

brownie compile 

This command will automatically pick up the smart contracts from the /contracts folder and compile them. The compilation outputs (or artifacts) like the contract ABI, bytecode, etc are stored in a JSON file (<contract-name>.json) inside the /build/contracts directory. You can check out the complete compiler artifacts file structure here

If you have multiple smart contracts in the /contracts directory, you can specify the contract that you want to compile using the following command:

brownie compile <contract-file-name.sol> 

Note: Brownie is smart. It uses the contract source hash (sha1 field in the compiler artifact file) to check for changes in the smart contract and only recompiles a contract if it detects any changes in the source file. So, even if you do not specify the contract files, Brownie will only compile the new files or the ones that are modified. 

How to deploy a smart contract with Brownie?

Now that we have created and compiled a contract, all that is left is to deploy the contract onto a network and test its functionality. In Brownie, there are two ways in which we can deploy and interact with a contract:

  • We can create Python scripts that automate the whole contract deployment and interaction.
  • We can use the Brownie console for quick testing and debugging.

As developers, learning to create powerful Python scripts that handle smart contract deployment and interaction is our end goal, but since we are just starting out, I think it would be much more helpful if we can understand the Brownie functionalities better before we jump into the scripts. To do that, we can try and interact with our smart contracts using the Brownie console. 

To spin up the Brownie console, open the terminal and type:

brownie console 

The output will look something like this:

Brownie v1.19.0 - Python development framework for Ethereum
Project is the active project.
Launching 'ganache-cli --port 8545 --gasLimit 12000000 --accounts 10 --hardfork istanbul --mnemonic brownie'...
Brownie environment is ready.
>>>

The console command will:

  • Compile all the contracts (only if they are not already compiled).
  • Launch a local blockchain network using ganache-cli.
  • Provide us with a command prompt, using which we can deploy and interact with the contract.

Now, to deploy a contract, we need, 

  • Contract ABI 
  • Contract bytecode 
  • An Ethereum account address 

The ABI and the bytecode are already there in the compiler artifact file (inside build/contracts) and as I mentioned previously, Ganache CLI provides 10 test accounts. So how do we access all these and deploy the contract? 

Well, in Brownie, when smart contracts are compiled, it creates a contractContainer object for each of the deployable contracts. The object can be accessed using the name of the contract (BasicContract, in our case). This object encapsulates all the necessary information like the contract ABI and bytecode. The object also comes with a deploy function that we can use in order to deploy the contract.

Here’s a quick look at how we can view the contract ABI details in the Brownie console using the <contract-name>.abi command:

>>> BasicContract.abi
[
    {
        'inputs': [],
        'name': "readNumber",
        'outputs': [
            {
                'internalType': "uint256",
                'name': "",
                'type': "uint256"
            }
        ],
        'stateMutability': "view",
        'type': "function"
    },
    {
        'inputs': [
            {
                'internalType': "uint256",
                'name': "_number",
                'type': "uint256"
            }
        ],
        'name': "storeNumber",
        'outputs': [],
        'stateMutability': "nonpayable",
        'type': "function"
    }
]

To deploy the contract, we also need to provide an account. In Brownie, we can use the accounts object for accessing the local accounts. Here, we will use the object to access one of the accounts provided by the Ganache CLI. 

Note: We can add our own accounts in Brownie using the accounts object and our account private key. We will discuss this in later articles.

You can see the details of all the accessible accounts using the accounts command:

>>> accounts
[<Account '0x66aB6D9362d4F35596279692F0251Db635165871'>, <Account '0x33A4622B82D4c04a53e170c638B944ce27cffce3'>, <Account '0x0063046686E46Dc6F15918b61AE2B121458534a5'>, <Account '0x21b42413bA931038f35e7A5224FaDb065d297Ba3'>, <Account '0x46C0a5326E643E4f71D3149d50B48216e174Ae84'>, <Account '0x807c47A89F720fe4Ee9b8343c286Fc886f43191b'>, <Account '0x844ec86426F076647A5362706a04570A5965473B'>, <Account '0x23BB2Bb6c340D4C91cAa478EdF6593fC5c4a6d4B'>, <Account '0xA868bC7c1AF08B8831795FAC946025557369F69C'>, <Account '0x1CEE82EEd89Bd5Be5bf2507a92a755dcF1D8e8dc'>]

Now that we know how to access these details, let us try and deploy a smart contract using the Brownie console.

To do that:

  • Get an account using the accounts object.
  • Use the contractContainer object of our smart contract.
  • Call the deploy function.
  • Pass the account as a parameter.
# get the account at index 0
account = accounts[0]
# use the BasicContract.deploy function to deploy the contract
# pass the account as the parameter
deployed_contract = BasicContract.deploy({"from":account})
Transaction sent: 0xfd8dfcecaacd20740a0f00a7f6e5ce8890e5dfc62c53d9f48ae8fd84694d6371
  Gas price: 0.0 gwei   Gas limit: 12000000   Nonce: 0
  BasicContract.constructor confirmed   Block: 1   Gas used: 90551 (0.75%)
  BasicContract deployed at: 0x3194cBDC3dbcd3E11a07892e7bA5c3394048Cc87
# display the contract address
deployed_contract
<BasicContract Contract '0x3194cBDC3dbcd3E11a07892e7bA5c3394048Cc87'>

The deploy function returns a ProjectContract object. This object helps us call or send transactions to the contract. In the above sample, we returned the ProjectContract object to the deployed_contract variable. Now we can use that variable in order to invoke the contract functions:

>>> deployed_contract.readNumber()
0
>>> deployed_contract.storeNumber(10,{"from":account})
Transaction sent: 0x1e2fca512bcddd5efc47d4620874632d2a7a0c185f09e2dc0140c7a04bdecd2a
  Gas price: 0.0 gwei   Gas limit: 12000000   Nonce: 1
  BasicContract.storeNumber confirmed   Block: 2   Gas used: 41394 (0.34%)
<Transaction '0x1e2fca512bcddd5efc47d4620874632d2a7a0c185f09e2dc0140c7a04bdecd2a'>
>>> deployed_contract.readNumber()
10

The Brownie console provides a quick and easy way to test and debug your contract. You can create more complex contracts and deploy them using the Brownie console in order to test their functionality. Once you are done with the testing, you can use CTRL-D to close the Brownie console. Do understand that once we close the console, Brownie will automatically teardown our local Ganache network, meaning that all the data we created during that session will be gone

Wrap up 

Tinkering with the Brownie console will help you better understand the Brownie functionalities. 

To learn more elaborate development and testing features of Brownie, we need to create more complex smart contracts, build powerful Python scripts and work with actual testnets. This might seem like a lot of work, but Brownie got you covered. 

In the coming articles, we will see how we can use Python scripts to deploy and interact with a smart contract, we will create some testing scripts, and we will also see if we can deploy our contract onto an Ethereum testnet.

Have you already explored what you can achieve with Chainstack? Get started for free today.

Hyperledger Global Forum 2020 Recap

ConsenSys, Microsoft, and EY launch the Baseline protocol. ScanTrust and Unilever provide end-to-end traceability for millions of units. NTT Data, Hitachi, and Accenture move their use cases to production.

Ashlie Chin
Mar 20
Chainstack uses cookies to provide you with a secure and
personalized experience on its website. Learn more.