Smart contract framework – The Brownie tutorial series–Part 1
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.
- Part 1 – Introduction. <– You are here.
- Part 2 – Scripts, tests, and testnets.
- Part 3 – Brownie deep dive.
- Part 4 – Move forward with 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.
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:
Directory | Content |
/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.
- Discover how you can save thousands in infra costs every month with our unbeatable pricing on the most complete Web3 development platform.
- Input your workload and see how affordable Chainstack is compared to other RPC providers.
- Connect to Ethereum, Solana, BNB Smart Chain, Polygon, Arbitrum, Base, Optimism, Avalanche, TON, Ronin, zkSync Era, Starknet, Scroll, Aptos, Fantom, Cronos, Gnosis Chain, Klaytn, Moonbeam, Celo, Aurora, Oasis Sapphire, Polygon zkEVM, Bitcoin, Tezos and Harmony mainnet or testnets through an interface designed to help you get the job done.
- To learn more about Chainstack, visit our Developer Portal or join our Discord server and Telegram group.
- Are you in need of testnet tokens? Request some from our faucets. Multi-chain faucet, Sepolia faucet, Holesky faucet, BNB faucet, zkSync faucet, Scroll faucet.
Have you already explored what you can achieve with Chainstack? Get started for free today.