Skip to content

Simulating a Non-View Solidity Function With Go Ethereum

Published:
by 22Xy

When working with Solidity contracts, view or pure functions generate straightforward “call” methods in Go. But what about functions that aren’t marked as view? Can we still simulate them and get return values without submitting transactions?

The answer is yes: by using the eth_call RPC under the hood, go-ethereum provides a Call method that executes the function in an transitory environment. You can then retrieve the return value—even if the function increments some internal variable or does something that would normally cost gas in a real transaction.

How Call Works in go-ethereum

In the Go bindings generated by abigen, there’s a lower-level API on the BoundContract or <ContractName>Raw struct:

func (c *BoundContract) Call(
    opts *CallOpts,
    result *[]interface{},
    method string,
    params ...interface{},
) error

Either way, no permanent changes happen on-chain. However, the return value from the function (encoded in its ABI) is returned to you in result.

Example: Non-View Function Returning a uint256

Suppose your Solidity function is:

uint256 public scoreVar;

function incrementAndGet() public returns (uint256) {
    scoreVar++;
    return scoreVar;
}
  1. You haven’t marked it view, so abigen will generate a transactor method returning (*types.Transaction, error).

  2. But you can still do:

    // Assume you have a binding "myContractRaw" created via abigen or manually:
    myContractRaw := &MyContractRaw{Contract: myContractInstance}
    
    var outputs []interface{}
    err := myContractRaw.Call(nil, &outputs, "incrementAndGet")
    if err != nil {
        fmt.Printf("Call revert or RPC error: %v\n", err)
        return
    }
    
    // If successful, outputs[0] should be the incremented value
    newVal := outputs[0].(*big.Int)
    fmt.Printf("New value: %v\n", newVal)
    
  3. When run with an eth_call, the node will execute scoreVar++, return the incremented value, then discard the change. On-chain, scoreVar remains the same as before.

  4. If you call it again, the starting state is still the on-chain value. So if scoreVar on-chain was 0, every eth_call to incrementAndGet will return 1 (because each simulated call starts from scoreVar = 0, increments it to 1, returns 1, discards changes).

Why This Works Even If It “Writes” State

Potential Reverts

Some node configurations might revert if your code tries to do certain modifications in eth_call. However, the go-ethereum default is typically to permit temporary writes. The result from such writes is visible in your return but never persisted after the call ends.

Steps to Use Call in Go

  1. Obtain a contract binding (usually via abigen) and a “raw” struct (e.g., MyContractRaw) that exposes lower-level operations.
  2. Prepare a result []interface{} slice. That’s where Call will unpack the function’s return values.
  3. Call:
    err = myRawBinding.Call(nil, &result, "functionName", param1, param2, ...)
    
    • The first parameter is *CallOpts (can be nil if you don’t need special context).
    • The second is a pointer to a slice of interface{} that will hold the decoded return values.
    • Followed by the name of the function and its parameters.
  4. If there’s no revert, check result[i] for your data. Usually you’ll convert result[0] into the expected type (e.g., *big.Int).
  5. If there was a revert (or invalid call), err != nil.

Common Patterns

Pitfalls & Considerations

  1. No On-Chain Changes
    ”Success” from this call doesn’t mean anything was actually written. It’s purely a local node simulation.

  2. Every Call Resets State
    If your function increments a variable, every call starts from the chain’s stored value, so you might repeatedly see the same “incremented” result. It never accumulates if you’re only using eth_call.

  3. Final State
    To truly change on-chain state, you must send a transaction (e.g., myContract.Transact(opts, "incrementAndGet", ...)) instead of Call.

  4. Potential Reverts
    If a node or dev environment enforces stricter “static call” semantics, writing state might cause a revert. Typically, Geth’s default mode allows transitory changes, but it’s worth testing if that’s consistent in your environment.

Comparison With Other Libraries

ethers.js – staticCall

In ethers.js v6, you can use .staticCall on a contract method. In previous versions (ethers.js v5), this functionality was called .callStatic. Refer to the migration guide for more details.

Regardless of the name, both approaches closely mirror eth_call at the RPC level, meaning they simulate a transaction without broadcasting it to the network. Any writes made during this simulation are transitory and discarded afterward, and the method’s return data is provided to your application.

// Example in ethers.js v6
const contract = new ethers.Contract(contractAddress, abi, provider);

// The ".staticCall" simulates the function call without committing changes on-chain.
const result = await contract.incrementAndGet.staticCall();
console.log(result.toString()); // temporary increment, never actually updates on-chain

viem – simulateContract

Similarly, viem’s simulateContract function accomplishes the same goal:

Simulates & validates a contract interaction. This is useful for retrieving return data and revert reasons of contract write functions.
This function does not require gas to execute and does not change the state of the blockchain.

Internally, simulateContract uses a Public Client to issue a call action with ABI-encoded data. Any state modifications are discarded after execution, allowing you to see return data (or revert reasons) without permanently altering the blockchain.

Conclusion

Go-ethereum’s Call method provides a powerful way to simulate non-view Solidity functions without gas costs or chain updates. While it won’t persist changes, it’s invaluable for:

Remember: use view when possible, Call for simulations, and real transactions for permanent changes.


Previous Post
Implementing Solidity abi.encode Function in Go
Next Post
Resolving Solidity ABI Packing Errors in Go