Compiling and testing Vyper contract using Foundry

Compiling and testing Vyper contract using Foundry

In this tutorial, we will use vyper to write our smart contract and Foundry as our Ethereum Development Environment. As we'll be using Foundry, we'll be writing our tests in Solidity.

What is Vyper?

Vyper is a contract-oriented, pythonic programming language that targets the Ethereum Virtual Machine. The aim of vyper is to make smart contract maximally human-readable. Any person with low prior experience with vyper or any particular language should understand vyper. Vyper follows python3 syntax. Thus, it should be noted that vyper syntax is a valid python3 syntax, however not all python3 syntax is valid in vyper.

Why Foundry?

Hardhat is definitely a nice Ethereum Development Environment but Foundry has some more cool benefits to offer.

  • Unlike Hardhat, Foundry allows us to write tests as well as scripts in Solidity
  • Foundry comes with the Forge Standard library, which allows using Dappsys Test to assert values.
  • We can simply log values using emit_log(string), emit_int(int_value), emit_uint(uint_value), emit_address(address_value), etc. to log values of different data types
  • To get more detailed information on the tests, we can simply use the -v flag after the test command. More the number of v will give us more detailed info. Example: forge test -vvv. We can add up to five v flags.
  • We can mock a user (or we can say an account) using .prank and .startPrank.
  • We can set nounce, block number, and timestamp for an account.
  • Foundry gives us the detailed gas report for our tests by running the below command.
    forge test --gas-report
    
  • Foundry provides us Fuzzing that allows us to define parameters for our test function and it will populate those parameters values on runtime

Getting Started

Create a new project using Foundry by running the following command.

Note: You can install and get vyper and foundry running locally by following vyper and foundry docs.

You can refer to this repo to follow along.

forge init vyper_foundry
cd vyper_foundry

Note: If you want to know and understand more about foundry and its structure, please refer to this article.

We have foundry up and running, let's write smart contract using Vyper. Navigate to the src folder in your root directory and rename Contract.sol to CountContract.vy as we will be writing our Contract in vyper. Paste the below Contract into that file.

count: public(uint256)

@external
def __init__(_count: uint256):
    self.count = 1

@external
def setCount(_count: uint256):
    self.count = _count

@external
def increment():
    self.count += 1

@external
def decrement():
    self.count -= 1

@external
def getCount() -> uint256: 
    return self.count

Let me give a quick overview of our Contract. We have defined a count variable that keeps track of our count. We have defined functions that lets us increment, decrement, set and get the value of our count variable. The __init__ function we have defined will run as a constructor for our contract, where we are setting the value for our count variable by taking it as an argument. Vyper follows Python3 syntax thus it's really easy to write and understand the smart contract.

Compiling vyper contracts

As we've written our smart contract, let's compile our smart contract. We can compile our smart contract written in vyper by running the following command in our terminal.

vyper src/CountContract.vy

You should see the following output in your terminal. This is the bytecode of our Contract. image.png

We are able to compile our Contract, but how we'll be able to compile it using foundry because foundry doesn't have a default script for compiling vyper contracts? For that, we'll be using ffi from foundry. ffi allows us to call an arbitrary command in foundry if enabled. Thus, using this feature we'll run vyper src/CountContract.vy command. So let's write a deployer contract that allows us to deploy our contract. Navigate to the root dir of our project and create a new folder named utils and under that create vyperDeployer.sol file. Paste the following code from below into that file.

As you can see, on lines 21 and 50, we've defined the deployContract function which we'll use to deploy our vyper contract. The first function defined on line 21 will get executed when we don't need to pass arguments to the contract's constructor otherwise we'll use the second function defined on line 50 when we need to pass arguments to our contract's constructor. Both these functions will return an address at which our contract will get deployed.

We have our deployer contract ready. So let's write tests for our smart contract. But before writing our test, we need an interface to interact with our vyper contract. This will allow Foundry to interact with your Vyper contract, enabling the full testing capabilities that Foundry has to offer. For that, create an interface folder inside the test directory and inside interface create a file named ICountContract.sol. In this file, paste the code below.

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.13;

interface ICountContract {
    function setCount(uint256 _count) external;

    function increment() external;

    function decrement() external;

    function getCount() external returns (uint256);
}

We'll use this interface to interact with our CountContract. We've our interface ready, so let's write tests for it in the Contract.t.sol file inside the test directory. For that, paste the below code in that file.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "../utils/vyperDeployer.sol";
import "./interface/ICountContract.sol";

contract ContractTest is Test {
    VyperDeployer deployer = new VyperDeployer();
    ICountContract countContract;

    function setUp() public {
        countContract = ICountContract(deployer.deployContract('CountContract', abi.encode(1)));
    }

    function testIncrement() public {
        assertEq(countContract.getCount(), 1);
        countContract.increment();
        assertEq(countContract.getCount(), 2);
    }

    function testDecrement() public {
        assertEq(countContract.getCount(), 1);
        countContract.decrement();
        assertEq(countContract.getCount(), 0);
    }

    function testSetCount() public {
        assertEq(countContract.getCount(), 1);
        countContract.setCount(5);
        assertEq(countContract.getCount(), 5);
    }
}

As you can see, in our setUp() function, we are deploying a new instance of our CountContract and as a second argument to our deployContract method from our deployer, we are passing abi.encode(1). This value will be passed as an argument to our constructor. Thus, the default value for our count variable in our contract will be 1 and we will be asserting values for our tests on the basis of this initial value.

Let's understand what is happening here and how our deployer works. We are creating a new instance of VyperDepolyer and we're calling the deployer.deployContract(fileName) method and we're passing the name of the contract we want to deploy. Our contract requires constructor arguments so we'll pass them in the abi encoded representation of the arguments which looks like this deployer.deployContract('CountContract', abi.encode(1)). The deployContract function compiles the Vyper contract and deploys the newly compiled bytecode, returning the address that the contract was deployed to. The deployed address is then used to initialize the ICountContract interface. Once the interface has been initialized, your Vyper contract can be used within Foundry like any other Solidity contract.

Now, we have everything ready required for our vyper contract. So, now let's run the tests and see how it goes. Run the following command in your terminal for that.

forge test

You should see an error like below in your terminal image.png

The reason for this is that we have not enabled ffi in the foundry. To enable that we should run our test command with the ffi flag. So now run the following command.

forge test --ffi

Now, you should see all our tests passing in our terminal as shown below image.png

Deploying a vyper contract

Once you are done with writing and testing the vyper contract, you can deploy it to any network of your choice.

The first option is to take the bytecode generated by the vyper compiler and manually deploy it through mist or geth.

vyper yourFileName.vy
# returns bytecode

Second option is to take the byte code and ABI and deploy it with your current browser on myetherwallet’s contract menu.

vyper -f abi yourFileName.vy
# returns ABI

This way you can write, test and deploy your vyper contract starting from scratch.

Congratulations! 🎉🎉🎉 You have just written a vyper contract and wrote tests and deploy scripts for it in foundry.

A huge shoutout to 0xKitsune for developing this setup and helping out with this article.