In this post, we will dive into the steps involved in testing and deploying a smart-contract written using the Huff programming language.
For those who don’t know:
Huff is a low-level programming language designed for developing highly optimized smart contracts that run on the Ethereum Virtual Machine (EVM). Huff does not hide the inner workings of the EVM and instead exposes its programming stack to the developer for manual manipulation.
The Aztec Protocol team originally created Huff to write Weierstrudel, an on-chain elliptical curve arithmetic library that requires incredibly optimized code that neither Solidity nor Yul could provide.
Note that this post is neither an introduction to Huff nor a step-by-step tutorial.This post aims to help the beginners to quickly get started with testing and deployment of their Huff contracts. If you are new to Huff, this video by devtooligan is a great resource. Also feel free to explore the official documentation of Huff at your own pace.
We are going to use a simple huff contract that performs basic math operations like add, subtract, multiply, etc.,
Note that the code below is not a complete Huff contract. It just contains the logic. The function dispatcher and other parts are contained in the wrapper. You can view the wrapper here.
#define macro ADD_NUMBERS() = takes (2) returns (1) {
// Input stack: // [num2, num1]
add // [num2 + num1]
}
#define macro SUB_NUMBERS() = takes (2) returns (1) {
// Input stack: // [num2, num1]
swap1 // [num1, num2]
sub // [num1 - num2]
}
#define macro MULTIPLY_NUMBERS() = takes (2) returns (1) {
// Input stack: // [num2, num1]
mul // [num2 * num1]
}
#define macro DIVIDE_NUMBERS() = takes (2) returns (1) {
// Input stack: // [num2, num1]
swap1 // [num1, num2]
div // [num1 / num2]
}
#define macro ABS() = takes (2) returns (1) {
// Input stack: // [num2, num1]
dup1
dup3
lt
iszero swapAndSubtract jumpi
sub
complete jump
swapAndSubtract:
swap1
sub
complete:
}
The above implementation is very basic and as always there is a plenty of room for improvements. But that’s a topic for another day.
We can test the above contract in 2 ways.
To test the above logic using foundry, we need to deploy the wrapper contract since it is the one that exposes the functions. Also we need to use HuffDeployer
helper contract from the foundry-huff library.
Here’s how the tests should be setup:
Define the interface of the contract (IMath
).
Deploy the wrapper contract (in the setUp()
method) using the HuffDeployer
.
Cast the returned address to the IMath
interface.
Write tests as usual.
/// Import HuffDeployer
import {HuffDeployer} from "foundry-huff/HuffDeployer.sol";
contract MathTest is Test {
IMath public math;
function setUp() public {
address addr = HuffDeployer.deploy(
"../test/foundry/wrappers/MathWrapper"
);
math = IMath(addr);
}
function testAddNumbers() public {
uint256 result = math.addNumbers(420, 69);
assertEq(result, 489);
}
function testAddNumbers_fuzz(uint256 a, uint256 b) public {
unchecked {
uint256 c = a + b;
if (c > MAX) {
vm.expectRevert();
math.addNumbers(a, b);
return;
}
uint256 result = math.addNumbers(a, b);
assertEq(result, a + b);
}
}
function testSubNumbers() public {
uint256 result = math.subNumbers(420, 69);
assertEq(result, 351);
}
...
...
}
You can view all the foundry tests here in the repo.
Testing Huff contracts using Foundry is the most commonly used method. But we can also write simpler (and faster) unit tests using Huff itself. The huff compiler (huffc
) has a test
command, which takes in the filename as an argument which contains the tests. We can use the TestHelpers util created by Maddiaa for basic operations like ASSERT
, etc.,
A sample Huff test contract will look like:
// ./test/huff/Math.t.huff
/* Imports */
#include "./helpers/TestHelpers.huff"
#include "../../src/Math.huff"
/* Tests */
#define test TEST_ADD() = {
0x01 // [1]
0x02 // [2,1]
ADD_NUMBERS() // [sum]
0x03 // [3,sum]
ASSERT_EQ() // [3 == sum]
0x4563918244f40000 // [5e18]
0x4563918244f40000 // [5e18, 5e18]
ADD_NUMBERS() // [SUM]
0x8ac7230489e80000 // [10e18, SUM]
ASSERT_EQ() // [10e18==SUM]
}
...
...
Complete test file here.
We can run the tests using the command:
$ huffc ./test/Math.t.huff test
The output will be something similar to the below image:
We just saw how to test Huff contracts using Foundry and Huff. Now we can move on to the next step which is deploying the Huff contract to a EVM-based blockchain (Goerli
, in this case). Here’s the foundry script to deploy the `MathWrapper contract.
// scripts/DeployMath.s.sol
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.15;
import "foundry-huff/HuffDeployer.sol";
import "forge-std/Script.sol";
import {IMath} from "../src/interfaces/IMath.sol";
contract Deploy is Script {
function run() public returns (IMath math) {
math = IMath(HuffDeployer.broadcast("wrappers/MathWrapper"));
console2.log("MathWrapper contract deployed to: ", address(math));
}
}
The above script can be executed by running the following command:
$ source .env && forge script scripts/DeployMath.s.sol:DeployMath --fork-url $RPC_URL --private-key $PRIVATE_KEY --broadcast
You need to configure the RPC_URL
of the network and PRIVATE_KEY
of the deployer in the .env
file.
If everything works as intended, you’ll be seeing an output something like this:
To validate the deployment you can either add assertions in the script’s run()
method or you can also implement fork tests with the deployed contract address similar to this one. If you want to deploy contracts with arguments, you can have a look into my Huffbound deployment script.
Sweet. We have successfully tested and deployed our simple Math.huff
contract. You can follow the same process to test and deploy more complex Huff contracts as well.
Here’s the link to GitHub repo:
Until next time 👋👋.