github.com/lmittmann/w3@v0.20.0/docs/pages/vm-testing.mdx (about) 1 # Contract Testing 2 3 `w3vm` can be used to test Smart Contracts in Go utilizing Go's handy testing and fuzzing features. 4 5 <Callout type="warning"> 6 `w3vm` **does not** natively support Smart Contract compilation. 7 </Callout> 8 9 ## Compile Smart Contracts 10 11 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: 12 13 * [`go-solc`](https://github.com/lmittmann/go-solc): Go bindings for the Solidity compiler (`solc`) 14 * [`go-huffc`](https://github.com/project-blanc/go-huffc): Go Bindings for the Huff Compiler (`huffc`) 15 * [`geas`](https://github.com/fjl/geas): The Good Ethereum Assembler 16 17 18 ## Setup a `w3vm.VM` 19 20 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. 21 22 ### Without Constructor Logic 23 24 If the Smart Contract does not require constructor logic, its runtime bytecode can be directly set as the bytecode of an address: 25 26 27 ```go 28 contractRuntime = w3.B("0x...") 29 contractAddr := w3vm.RandA() 30 31 vm, _ := w3vm.New( 32 w3vm.WithState(w3types.State{ 33 contractAddr: {Code: runtime}, 34 }), 35 ) 36 ``` 37 38 ### With Constructor Logic 39 40 If the Smart Contract requires constructor logic, the constructor bytecode must be sent in a standard deployment transaction (`w3types.Message`) without recipient: 41 42 ```go 43 contractConstructor := w3.B("0x...") 44 deployerAddr := w3vm.RandA() 45 46 vm, _ := w3vm.New() 47 receipt, err := vm.Apply(&w3types.Message{ 48 From: deployerAddr, 49 Input: contractConstructor, 50 }) 51 if err != nil || receipt.ContractAddress == nil { 52 // ... 53 } 54 contractAddr := *receipt.ContractAddress 55 ``` 56 57 ### Custom State 58 59 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](/vm-overview#writing-state). 60 61 ### State Forking 62 63 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. 64 65 ```go 66 client := w3.MustDial("https://eth.llamarpc.com") 67 defer client.Close() 68 69 vm, err := w3vm.New( 70 w3vm.WithFork(client, big.NewInt(20_000_000)), 71 w3vm.WithNoBaseFee(), 72 w3vm.WithTB(t), 73 ) 74 if err != nil { 75 // ... 76 } 77 ``` 78 79 <Callout> 80 `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. 81 </Callout> 82 83 84 ## Testing 85 86 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. 87 88 #### Example: Test WETH `deposit` Function 89 90 Test of the WETH `deposit` function. 91 92 ```go 93 func TestWETHDeposit(t *testing.T) { 94 // setup VM 95 vm, _ := w3vm.New( 96 w3vm.WithState(w3types.State{ 97 addrWETH: {Code: codeWETH}, 98 addrA: {Balance: w3.I("1 ether")}, 99 }), 100 ) 101 102 // pre check 103 var wethBalanceBefore *big.Int 104 if err := vm.CallFunc(addrWETH, funcBalanceOf, addrA).Returns(&wethBalanceBefore); err != nil { 105 t.Fatal(err) 106 } 107 if wethBalanceBefore.Sign() != 0 { 108 t.Fatal("Invalid WETH balance: want 0") 109 } 110 111 // deposit (via fallback) 112 if _, err := vm.Apply(&w3types.Message{ 113 From: addrA, 114 To: &addrWETH, 115 Value: w3.I("1 ether"), 116 }); err != nil { 117 t.Fatalf("Deposit failed: %v", err) 118 } 119 120 // post check 121 var wethBalanceAfter *big.Int 122 if err := vm.CallFunc(addrWETH, funcBalanceOf, addrA).Returns(&wethBalanceAfter); err != nil { 123 t.Fatal(err) 124 } 125 if w3.I("1 ether").Cmp(wethBalanceAfter) != 0 { 126 t.Fatalf("Invalid WETH balance: want 1") 127 } 128 } 129 ``` 130 131 132 ## Fuzz Testing 133 134 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. 135 136 #### Example: Fuzz Test WETH `deposit` Function 137 138 Fuzz test of the WETH `deposit` function. 139 140 ```go 141 func FuzzWETHDeposit(f *testing.F) { 142 f.Add([]byte{1}) 143 f.Fuzz(func(t *testing.T, amountBytes []byte) { 144 if len(amountBytes) > 32 { 145 t.Skip() 146 } 147 amount := new(big.Int).SetBytes(amountBytes) 148 149 // setup VM 150 vm, _ := w3vm.New( 151 w3vm.WithState(w3types.State{ 152 addrWETH: {Code: codeWETH}, 153 addrA: {Balance: w3.BigMaxUint256}, 154 }), 155 ) 156 157 // Pre-check WETH balance 158 var wethBalanceBefore *big.Int 159 if err := vm.CallFunc(addrWETH, funcBalanceOf, addrA).Returns(&wethBalanceBefore); err != nil { 160 t.Fatal(err) 161 } 162 163 // Attempt deposit 164 vm.Apply(&w3types.Message{ 165 From: addrA, 166 To: &addrWETH, 167 Value: amount, 168 }) 169 170 // Post-check WETH balance 171 var wethBalanceAfter *big.Int 172 if err := vm.CallFunc(addrWETH, funcBalanceOf, addrA).Returns(&wethBalanceAfter); err != nil { 173 t.Fatal(err) 174 } 175 176 // Verify balance increment 177 wantBalance := new(big.Int).Add(wethBalanceBefore, amount) 178 if wethBalanceAfter.Cmp(wantBalance) != 0 { 179 t.Fatalf("Invalid WETH balance: want %s, got %s", wantBalance, wethBalanceAfter) 180 } 181 }) 182 } 183 ```