Vyper support

Foundry supports compiling and testing Vyper contracts.

1. Compilation

You can install Vyper by following the instructions here. If you have vyper available in your PATH, foundry will automatically use it.

Otherwise, you can set the path to vyper in your foundry.toml by adding the following:

[vyper]
path = "/path/to/vyper"

Vyper libraries via forge install

If you want an import like the following to work in your Vyper contract:

from snekmate.utils import eip712_domain_separator

You can install Vyper the desired library via forge install e.g. forge install pcaversaccio/snekmate.

You then need to adjust your foundry.toml as follows (replacing “snekmate” with the name of your desired package):

skip = ["**/lib/snekmate/**"]
libs = ["lib", "lib/snekmate/src"]

Vyper libraries via pip

Alternatively if you want to install the package via pip into your system’s python configuration or a virtual environment you can point foundry to it by modifying your foundry.toml as follows:

# Assuming you have a virtual environment in `.venv` and are using Python 3.12
libs = ["lib", ".venv/lib/python3.12/site-packages/"]

Note that compatible alternative python package managers like uv will work too.

2. Solidity tests

Let’s write a test for this simple Counter contract:

number: public(uint256)

@deploy
@payable
def __init__(initial_number: uint256):
    self.number = initial_number

@external
def set_number(new_number: uint256):
    self.number = new_number

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

We can deploy it by using the deployCode cheatcode from forge-std and test it with the following Solidity test:

import {Test} from "forge-std/Test.sol";

interface ICounter {
    function increment() external;
    function number() external view returns (uint256);
    function set_number(uint256 newNumber) external;
}

contract CounterTest is Test {
    ICounter public counter;
    uint256 initialNumber = 5;

    function setUp() public {
        counter = ICounter(deployCode("Counter", abi.encode(initialNumber)));
        assertEq(counter.number(), initialNumber);
    }

    function test_Increment() public {
        counter.increment();
        assertEq(counter.number(), initialNumber + 1);
    }

    function testFuzz_SetNumber(uint256 x) public {
        counter.set_number(x);
        assertEq(counter.number(), x);
    }
}

3. Deploying

You can deploy Vyper contracts via forge create command:

forge create Counter --constructor-args '1' --rpc-url $RPC_URL --private-key $PRIVATE_KEY

And with deployCode you can deploy Vyper contracts in your scripts as well:

import {Script} from "forge-std/Script.sol";

contract CounterScript is Script {
    function run() public {
        vm.broadcast();
        deployCode("src/Counter.vy", abi.encode(1));
    }
}

4. Vyper scripts

You can write Vyper scripts in the same way as Solidity scripts:

interface Vm:
    def startBroadcast(): nonpayable

interface ICounter:
    def increment(): nonpayable
    def number() -> uint256: view

vm: constant(Vm) = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D)

@external
def run(counter: address):
    number_before: uint256 = staticcall ICounter(counter).number()

    extcall vm.startBroadcast()
    extcall ICounter(counter).increment()

    number_after: uint256 = staticcall ICounter(counter).number()

    assert number_after == number_before + 1

Such script can be run with the following command:

forge script script/Increment.s.vy  --sig 'run' '<counter address>' --rpc-url $RPC_URL --broadcast  --private-key $PRIVATE_KEY

5. Limitations

  • While you can write and run tests and scripts in Vyper, there is no new keyword in Vyper allowing you to deploy contracts. This will be addressed in the future with new cheatcodes.
  • Vyper does not allow overloads with the same names but different parameter types. Thus some cheatcode combinations might require workarounds to be used. (e.g. startBroadcast(address sender)) and startBroadcast(uint256 pk))
  • forge coverage currently does not support Vyper contracts.