Vyper and Brownie Contract Development on EVM Chains
For many of us, Solidity isn’t the most enjoyable language to start off with when learning about smart contract development, even though it seems like the only option. For those who came more from a security, scripting, and backend development side of things, we still prefer Pythonesque syntax to what Solidity tries to present with its Javascript-lookalike language and using Truffle and Javascript for smart contract development workstations.
Luckily, Vyper exists as an alternative to Solidity and admittedly being a much safer approach to smart contract development due to its higher restrictions and limitations and focus on security. Vyper’s ethos work very well with those of ETC. Hence, it made sense for the ETC Cooperative to help contribute to the codebase of Vyper in collaboration with Ben Hauser who implemented the changes necessary into Vyper in order to allow us to target multiple versions of the EVM in order to have it working with ETC. This whole thing also wouldn’t have been possible without the guidance and awesomeness of Bryant Eisenbach who helped guide the VIPs and their subsequent implementation Vyper that would benefit ETC and other EVM-chains.
To celebrate integration of ETC with Vyper, we decided to build a smart contract for Vyper to deploy to ETC. In order to commit to a Python-only Ethereum Stack based development environment, we wanted to check out Brownie, the Python-Ethereum development environment that’s an alternative to Truffle. In this tutorial, we will setup Brownie and Vyper and build up our smart contract and test suites as well as some helpful scripts. We will be rewriting my Solidity smart contract from previous guides for creating a Trademark Registration system to Vyper language on the decentralized web. The bonus cool thing about Brownie is that it is developed by the very same Ben Hauser who has implemented the VIPs that allow ETC to work with Vyper contracts! Later, we can deploy our contract to Kotti network either from Ethercluster or Hyperledger Besu running locally.
Note: While this tutorial uses Kotti and ETC as examples for working with Brownie and Vyper, you can also use any ETH testnet or mainnet while following this guide. The beauty of smart contract development is that it can be EVM-agnostic!
Setting Up Brownie and Vyper
We will first need to setup Brownie on our computer.
First, we will need to install ganache-cli
and python3
on our computers prior to setting up Brownie and Vyper.
Ganache-cli requires NPM for installation, so make sure you have Node installed on your computer.
$ npm install -g ganache-cli
Vyper relies on Python 3.6 or higher, so my favored way of going about install everything is to first have Vyper locally installed on a virtual-env like in their documentation.
Since I wanted to build something locally first, I’ve used MacOS instructions from their guide to installing Vyper. I’ve contained everything within a vyper-venv
virtual environment.
I then activate my vyper-venv
and can install anything locally within it.
Test Vyper is installed by running:
$ vyper -h
If it shows list of Vyper commands, then it is installed.
We can then install Brownie with the command here:
$ pip3 install eth-brownie
Which will install it in our virtual-environment.
Now we will make our new project here and initialize a Brownie work-environment in it.
$ mkdir trademark && cd trademark
$ brownie init
This initializes directories and config files for you to explore, which can be seen via ls
. Cool ones we will be focusing on are contracts
, scripts
, and tests
.
Now, let’s test the console for Brownie to see if its available.
Type:
$ brownie console
If Brownie is installed correctly, you will see the following:
>>>
You can safely type exit()
to exit console. You can learn more about the console on the installation page of Brownie.
Now, let’s head off to write up our Vyper smart contract!
Trademark Smart Contract in Vyper Lang
Note: The code for this project is found on Github here
The smart contract we will be writing up is small and simple, much cleaner than its Solidity equivalent. I’ll write up the Vyper smart contract in code chunks. If you want to learn more about the Vyper syntax, there’s a doc guide.
We will start by creating a trademark.vy
file that’ll be used for our smart contract. .vy
means its a Vyper file. We create it from the root directory in our Brownies work directory trademark
by running the following using VIM (you don’t have to use VIM, you can use any other IDE, just make sure to create the file in contracts/
directory):
$ vim contracts/trademark.vy
Inside the file, we will start writing up the Trademark smart contract. Its a smart contract that registers a trademark for its users if one hasn’t been registered. It also allows a user to retrieve information about a trademark such as the author, block timestamp of when it was registered and proof of the phrase that was submitted.
We write the following code chunk:
struct Trademark:
phrase: string[100]
authorName: string[100]
registrationTime: uint256(sec, positional)
proof: bytes32trademarkLookup: public(map(bytes32, bool))
trademarkRegistry: public(map(bytes32, Trademark))
Here, we are creating the struct Trademark
which is like the struct used in Solidity. Here we set phrase
to a string
type. The [100]
indicates the size of the string. For the uint256(sec, positional)
, this will be the type used for a timestamp in Vyper.
We also create two public mappings called trademarkLookup
and trademarkRegistry
. One is used to quickly check if a trademark is registered as a boolean value whole the other will be used to store all the values of the account.
The next code chunk we will write is the following:
@public
@constant
def checkTrademark(phrase: string[100]) -> bool:
proof: bytes32 = keccak256(phrase)
return self.trademarkLookup[proof]
Here, we create a public function that a user can use to check if a phrase has been registered. We use the @public
decorator so that it can be accessed outside the contract. We also use @constant
because checking to see if something is registered shouldn’t change the state of the network. In other words, it shouldn’t use gas.
Here, checkTrademark
takes in a string for the variable phrase and returns a boolean if its found or not.
Inside the function, we take the proof
of the phrase with keccak256
function and return the boolean of the lookup to trademarkLookup
mapping. Note how we use self
to access a global variable of the contract.
The next code chunk is here:
@public
def registerTrademark(phraseText: string[100], author: string[100]) -> bool:
trademark_check: bool = self.trademarkLookup[keccak256(phraseText)]
if trademark_check == False:
proofHash: bytes32 = keccak256(phraseText)
self.trademarkRegistry[proofHash] = Trademark({phrase: phraseText, authorName: author, registrationTime: block.timestamp, proof: proofHash})
self.trademarkLookup[proofHash] = True
return True
return False
Here, we create a @public
function called registerTrademark
that takes in 2 values, the phrase and author name to register. Notice how there’s no constant
decorator because this function will take up gas. Registering a trademark should incrue a cost on the user in order for them to not spam and DOS the network by entering a lot of trademark phrases at once.
In the function, we check if the trademark has been registered already. If it hasn’t, then we generate the proof of the phrase and entering it along with the phrase, the authorName, and the registration time in the current block’s timestamp and store them in the struct Trademark
We then enter it in our mapping for self.trademarkRegistry
. We then set its entry to True
in self.trademarkLookup
based on the proofHash
we generated. We then return a True
to indicate it has been registered.
If it has been entered already, we instead return a False
.
Our last code chunk is the following:
@public
@constant
def getTrademarkData(phrase: string[100]) -> (string[100], string[100], uint256(sec, positional), bytes32):
t: Trademark = self.trademarkRegistry[keccak256(phrase)]
return t.phrase, t.authorName, t.registrationTime, t.proof
Here, we will be retrieving back the data from the Trademark
struct stored in the self.trademarkRegistry
. We make this a @constant
since reading data shouldn’t cost the user gas for now.
Now that we have written our viper smart contract, its time to compile it with Brownie!
To do that, run the following command:
$ brownie compile
This will compile your brownie smart contract to the following directory build/contracts/
where you can access the JSON.
If you open the JSON file there trademark.json
, it shows you the ABI and Bytecode of the compiled smart contract which can be used to be deployed on its own to MyCrypto or other platforms.
However, the cool thing about Brownie is that we can do our entire deployment from the command line. Let’s build some scripts for that!
Hyperledger Besu for Kotti
In order to deploy to our network, we need to write some scripts for deployment. Here, we will work on two scripts, one for ganache-cli, and another for Kotti test net. For Kotti, you have one of two choices:
1) Use Ethercluster Kotti endpoint
2) Use Hyperledger Besu node running Kotti in Docker
If you decide to use 1), skip to the next section on Scripting.
Otherwise, continue on this short section.
We first need to have Docker installed, so make sure you follow the instructions here for your preferred OS.
After that, we run the following command:
docker run --name besu-kotti -p 30303:30303 -p 30304:8545 --mount type=bind,source=~/.besu/etc/kotti,target=/var/lib/besu hyperledger/besu:latest --network=kotti --data-path=/var/lib/besu --rpc-http-enabled
Notice the source
path will need you to create the directories for it, which can be done with:
$ mkdir -p ~/.besu/etc/kotti
After doing that, you’re good to go on the Docker command. It allows you to use port number 30304 to listen in on the Docker image, but you’ll need to wait for it to sync up before interacting with it.
Scripting in Brownie for Deployment
Whichever way you want to deploy to Kotti, with a locally syncing Hyperledger Besu node or via Ethercluster, the Hyperledger Labs project, we need to connect somehow to Brownie in order to deploy to the network.
We first open the file brownie-config.yaml
.
In it, we go to the following section for networks
. If you notice, under entries for Infura endpoints, we can add the following sections for our Ethercluster endpoints if they’re not added yet:
kotti:
host: https://www.ethercluster.com/kotti
classic:
host: https://www.ethercluster.com/
Now we have our Kotti and Classic Ethercluster endpoints. We can even include the following network called dev-kotti
for our local node:
dev-kotti:
host: http://localhost
persist: false
reverting_tx_gas_limit: 6721975
test_rpc:
port: 30304
gas_limit: 6721975
accounts: 10
evm_version: byzantium
mnemonic: brownie
Here it’s using http://localhost:30304 which is the endpoint of the Hyperledger Besu Docker node we are running for Kotti.
Finally, we need to create a Kotti keystore file with a password that can be accessed.
Brownie allows us to do just that!
$ brownie accounts generate kotti-key
Will generate a key and address called kotti-key
that’ll be stored in the path ~/.brownie/accounts/kotti-key.json
.
It asks you to input a password for your key and once you do that, it’ll give you the public address of that key in the output. You can request for Kotti to that address via DMing me on Twitter or coming to our Discord channel and requesting some Kotti from our community bot Bridgette.
Once you have Kotti in your address, you can use it to deploy the account.
Before we deploy our account to Kotti first, we will first deploy it to a local network using ganache-cli
. This allows us to use multiple sample addresses with test-ether on them.
For that, we will be using our script for ganache by creating the file:
$ vim scripts/ganache-deploy.py
Inside the file, we simply add the following code:
from brownie import *
import loggingdef main():
t = accounts[0].deploy(trademark)
This provides fixtures from Brownie like accounts
and trademark
which is the name of my contract trademark.vy
found in contracts/
directory. More can be found about deployment here.
In this example, we are deploying our trademark smart contract to ganache-cli network using the first account in the accounts
list provided by ganache-cli. If all is successful, we can then interact with our deployed contract inside the console.
We run our contract via the following:
$ brownie run scripts/ganache-deploy.py
It should give you the following output:
Brownie v1.4.0 - Python development framework for EthereumTutorialProject is the active project.
Launching 'ganache-cli --port 8545 --gasLimit 6721975 --accounts 10 --hardfork petersburg --mnemonic brownie'...Running 'scripts.ganache-deploy.main'...
Transaction sent: 0xb9881cbfcef50ad6227f89c30f0faa1a47d64125cf0eb384e9299905a58ce0dc
Gas price: 20.0 gwei Gas limit: 540688
trademark.constructor confirmed - Block: 1 Gas used: 540688 (100.00%)
trademark deployed at: 0x3194cBDC3dbcd3E11a07892e7bA5c3394048Cc87Terminating local RPC client...
This shows the contract has been deployed to ganache and shows the transaction ID and gas price used. It also shows the contract address.
Great! Now, let’s deploy it to Kotti.
$ vim scripts/kotti-deploy.py
Inside it, we spec out the following:
from brownie import *def main():
accounts.load('kotti-account')
t = accounts[-1].deploy(trademark)
t.balance()
t.registerTrademark("Hakuna Matata", "Simon and Pumba")
print(t.getTrademarkData("Hakuna Matata"))
Inside the `brownie-config.yaml`, we change `default` value to `kotti`. If using the local Kotti Hyperledger Besu node, then type `dev-kotti` instead.
Now, let’s test deployment. In the code we wrote, we loaded the account `kotti-key`. We then use it in the list `accounts` since its the last thing added to that list. We use that account to deploy the `trademark.vy` contract. We then check to see if the functionality of the account works by registering the phrase “Hakuna Matata” by the authors “Simon & Pumba” and then print out the response to getTrademarkData
for the phrase “Hakuna Matata”
This will ask us to enter our password for that private key. Then, it will deploy to Kotti Testnet and await a confirmation. That will take a few seconds to confirm before it returns back the contract address it deployed to and the transaction ID. It will also print out the response for registering.
We can use http://kotti.etccoopexplorer.com to see the transaction ID.
Testing the Smart Contract in Brownie
Now, we go to my favorite part, writing unit tests!
The amazing thing about Brownie and Python is that testing is so much better than on Truffle. The power of Python is its modules and automation of scripts so to be using it as part of the smart contract writing and testing strategy is awesome.
We have provided a testing file here. We will go over it after:
from brownie import accounts
import loggingINITIAL_PHRASE = 'ETC&Vyper'
INITIAL_AUTHOR = 'Satoshi Nakamoto'def test_trademark_deployment(trademark):
t = accounts[0].deploy(trademark)
assert t.address != None
logging.info("Trademark Smart Contract Deployed")def test_trademark_balance(trademark):
assert trademark[0].balance != 0
logging.info("Trademark Smart Contract Has Balance")def test_trademark_is_not_registered(trademark):
check = trademark[0].checkTrademark(INITIAL_PHRASE)
assert check == False
logging.info(f"Trademark {INITIAL_PHRASE} NOT Registered Check Complete")def test_register_trademark(trademark):
register_tx = trademark[0].registerTrademark(INITIAL_PHRASE, INITIAL_AUTHOR)
assert register_tx.return_value == True
logging.info(f"Trademark {INITIAL_PHRASE} Registration By {INITIAL_AUTHOR} Has Been Submitted")
assert register_tx.txid is not None
logging.info(f"Trademark Registration Transaction ID is {register_tx.txid}")def test_trademark_is_registered(trademark):
check = trademark[0].checkTrademark(INITIAL_PHRASE)
In this contract, we have written the following INITIAL_PHRASE and INITIAL_AUTHOR for the tests. We then specify a deployment strategy using the first account in the accounts list in test_trademark_deployment
. trademark
is the object of the smart contract. We can assert there is an address for the smart contract deployment after we deploy then we log the success.
The other tests test_trademark_balance
, test_trademark_is_not_registered
, and after all test basic functionalities by interacting with the contract and checking to see if the global variable phrase isn’t registered so they can register it after.
Those sorts of tests are very helpful in automation and ensuring smart contract is behaving as expected.
You can run it here:
$ pytest --log-cli-level info
Which shows you the tests and what happens in each step.
You can remove verbose by just running pytest
.
Looking Ahead
Vyper’s focus on security first along with Brownie’s killer automation tooling means it’s a huge leap in smart contract development for the larger cryptography community. Python is the number one language for many in cybersecurity and scripting and backend development as well as the scientific community. To have the option to integrate with the larger Python family while relying on killer automation features in scripting smart contract deployment in Brownie allows one to become a security-focused smart contract ninja in no time. Vyper and Brownie are simply awesome.
With its deployment now to Kotti and ETC, the ETC community and any network running the EVM-stack can enjoy using different versions of the EVM and reap the benefits that Vyper and Brownie has to offer. Can’t wait to see what everyone will build!