Contract Testing
w3vm
can be used to test Smart Contracts in Go utilizing Go’s handy testing and fuzzing features.
w3vm
does not natively support Smart Contract compilation.
Compile Smart Contracts
The first step to testing a Smart Contract is usually to compile it to bytecode. There are a number of third party packages that provide compiler bindings in Go:
go-solc
: Go bindings for the Solidity compiler (solc
)go-huffc
: Go Bindings for the Huff Compiler (huffc
)geas
: The Good Ethereum Assembler
Setup a w3vm.VM
Before a Smart Contract can be tested with a w3vm.VM
instance, its bytecode must be deployed to the VM. This can be done in two ways, depending on whether constructor logic is present.
Without Constructor Logic
If the Smart Contract does not require constructor logic, its runtime bytecode can be directly set as the bytecode of an address:
contractRuntime = w3.B("0x...")
contractAddr := w3vm.RandA()
vm, _ := w3vm.New(
w3vm.WithState(w3types.State{
contractAddr: {Code: runtime},
}),
)
With Constructor Logic
If the Smart Contract requires constructor logic, the constructor bytecode must be sent in a standard deployment transaction (w3types.Message
) without recipient:
contractConstructor := w3.B("0x...")
deployerAddr := w3vm.RandA()
vm, _ := w3vm.New()
receipt, err := vm.Apply(&w3types.Message{
From: deployerAddr,
Input: contractConstructor,
})
if err != nil || receipt.ContractAddress == nil {
// ...
}
contractAddr := *receipt.ContractAddress
Custom State
The state of the VM can be fully customized using the w3vm.WithState
option. This allows, e.g., setting a balance for addresses that interact with the Smart Contract. State can also be modified after the VM is created using the state write methods.
State Forking
If the tested Smart Contract interacts with other existing contracts, the VM can be configured to fork the state at a specific block number (or the latest block). This enables testing contracts in a real-world environment.
client := w3.MustDial("https://rpc.ankr.com/eth")
defer client.Close()
vm, err := w3vm.New(
w3vm.WithFork(client, big.NewInt(20_000_000)),
w3vm.WithNoBaseFee(),
w3vm.WithTB(t),
)
if err != nil {
// ...
}
w3vm.WithTB(t)
can be used in tests or benchmarks to cache state. The VM persists this cached state in {package of test}/testdata/w3vm/
. This is particularly useful when working with public RPC providers, as it reduces the number of requests and significantly speeds up test execution.
Testing
Testing Smart Contracts with w3vm
follows the standard Go testing patterns using the package testing
. By integrating w3vm
into your tests, you can simulate blockchain interactions and validate Smart Contract behaviors within your test cases.
Example: Test WETH deposit
Function
Test of the WETH deposit
function.
func TestWETHDeposit(t *testing.T) {
// setup VM
vm, _ := w3vm.New(
w3vm.WithState(w3types.State{
addrWETH: {Code: codeWETH},
addrA: {Balance: w3.I("1 ether")},
}),
)
// pre check
var wethBalanceBefore *big.Int
if err := vm.CallFunc(addrWETH, funcBalanceOf, addrA).Returns(&wethBalanceBefore); err != nil {
t.Fatal(err)
}
if wethBalanceBefore.Sign() != 0 {
t.Fatal("Invalid WETH balance: want 0")
}
// deposit (via fallback)
if _, err := vm.Apply(&w3types.Message{
From: addrA,
To: &addrWETH,
Value: w3.I("1 ether"),
}); err != nil {
t.Fatalf("Deposit failed: %v", err)
}
// post check
var wethBalanceAfter *big.Int
if err := vm.CallFunc(addrWETH, funcBalanceOf, addrA).Returns(&wethBalanceAfter); err != nil {
t.Fatal(err)
}
if w3.I("1 ether").Cmp(wethBalanceAfter) != 0 {
t.Fatalf("Invalid WETH balance: want 1")
}
}
Fuzz Testing
Fuzzing Smart Contracts with w3vm
leverages Go’s fuzz testing capabilities to automatically generate a wide range of inputs for your contracts. By incorporating w3vm
into your fuzzing tests, you can effectively discover vulnerabilities and unexpected behaviors in your Smart Contracts.
Example: Fuzz Test WETH deposit
Function
Fuzz test of the WETH deposit
function.
func FuzzWETHDeposit(f *testing.F) {
f.Add([]byte{1})
f.Fuzz(func(t *testing.T, amountBytes []byte) {
if len(amountBytes) > 32 {
t.Skip()
}
amount := new(big.Int).SetBytes(amountBytes)
// setup VM
vm, _ := w3vm.New(
w3vm.WithState(w3types.State{
addrWETH: {Code: codeWETH},
addrA: {Balance: w3.BigMaxUint256},
}),
)
// Pre-check WETH balance
var wethBalanceBefore *big.Int
if err := vm.CallFunc(addrWETH, funcBalanceOf, addrA).Returns(&wethBalanceBefore); err != nil {
t.Fatal(err)
}
// Attempt deposit
vm.Apply(&w3types.Message{
From: addrA,
To: &addrWETH,
Value: amount,
})
// Post-check WETH balance
var wethBalanceAfter *big.Int
if err := vm.CallFunc(addrWETH, funcBalanceOf, addrA).Returns(&wethBalanceAfter); err != nil {
t.Fatal(err)
}
// Verify balance increment
wantBalance := new(big.Int).Add(wethBalanceBefore, amount)
if wethBalanceAfter.Cmp(wantBalance) != 0 {
t.Fatalf("Invalid WETH balance: want %s, got %s", wantBalance, wethBalanceAfter)
}
})
}