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  ```