Skip to main content

Thunk Functions

In software development, thunk functions are a crucial part of setting up smart contracts correctly, facilitating type-safety and easy mapping of function names to their actual implementations.

Overview

A thunk is a wrapper function employed to inject code before and/or after a function call. It helps in adapting functions to meet changing demands. Through the Schema Tool, thunks are generated to establish correct calls to smart contract functions and to ensure type-safety. They share a common function signature, fostering a straightforward creation of a function table for generic calls.

Role in Wasm

Working hand in hand with the WebAssembly (Wasm) format, thunks play a pivotal role in the lib.xx component. Below is how a chunk of a dividend smart contract would look (the detailed thunk function content is omitted):

var exportMap = wasmlib.ScExportMap{
Names: []string{
FuncDivide,
FuncInit,
FuncMember,
FuncSetOwner,
ViewGetFactor,
ViewGetOwner,
},
Funcs: []wasmlib.ScFuncContextFunction{
funcDivideThunk,
funcInitThunk,
funcMemberThunk,
funcSetOwnerThunk,
},
Views: []wasmlib.ScViewContextFunction{
viewGetFactorThunk,
viewGetOwnerThunk,
},
}

func OnDispatch(index int32) {
exportMap.Dispatch(index)
}

func funcDivideThunk(ctx wasmlib.ScFuncContext) {}
func funcInitThunk(ctx wasmlib.ScFuncContext) {}
func funcMemberThunk(ctx wasmlib.ScFuncContext) {}
func funcSetOwnerThunk(ctx wasmlib.ScFuncContext) {}
func viewGetFactorThunk(ctx wasmlib.ScViewContext) {}
func viewGetOwnerThunk(ctx wasmlib.ScViewContext) {}

The Dispatch Process

The central player here is the OnDispatch() function, called by the primary Wasm file, essentially a dynamic link library. This mechanism keeps the smart contract (SC) code self-contained and versatile, fitting for both Wasm requirements and direct client-side executions.

To meet Wasm's demands, we implement on_load() and on_call() callbacks that collaborate with OnDispatch() in the lib.xx, orchestrating a seamless dispatch process.

on_load()

The on_load() Wasm function will be called by the Wasm VM host upon loading of the Wasm code. It will inform the host of all the function ids and types (Func or View) that this smart contract provides.

on_call()

When the host needs to call a function of the smart contract it will call the on_call() callback function with the corresponding function id, and then the on_call() function will dispatch the call via the ScExportMap mapping table that was generated by the Schema Tool to the proper associated thunk function.

Generated Main Entry Point

Here is the generated main.xx that forms the main entry point for the Wasm code:

//go:build wasm
// +build wasm

package main

import "github.com/iotaledger/wasp/packages/wasmvm/wasmvmhost/go/wasmvmhost"

import "github.com/iotaledger/wasp/contracts/wasm/dividend/go/dividend"

func main() {
}

func init() {
wasmvmhost.ConnectWasmHost()
}

//export on_call
func onCall(index int32) {
dividend.OnDispatch(index)
}

//export on_load
func onLoad() {
dividend.OnDispatch(-1)
}

Finally, here is an example implementation of a thunk function for the setOwner() contract function. You can examine the other thunk functions that all follow the same pattern in the generated lib.xx:

type SetOwnerContext struct {
Params ImmutableSetOwnerParams
State MutableDividendState
}

func funcSetOwnerThunk(ctx wasmlib.ScFuncContext) {
ctx.Log("dividend.funcSetOwner")
f := &SetOwnerContext{
Params: ImmutableSetOwnerParams{
proxy: wasmlib.NewParamsProxy(),
},
State: MutableDividendState{
proxy: wasmlib.NewStateProxy(),
},
}

// only defined owner of contract can change owner
access := f.State.Owner()
ctx.Require(access.Exists(), "access not set: owner")
ctx.Require(ctx.Caller() == access.Value(), "no permission")

ctx.Require(f.Params.Owner().Exists(), "missing mandatory owner")
funcSetOwner(ctx, f)
ctx.Log("dividend.funcSetOwner ok")
}

The Thunk Function in Action

Log the Contract and Function Name

First, the thunk logs the contract and function name to show that the call has started.

Set Up Strongly Typed Context

Then it sets up a strongly typed function-specific context structure. First, we add the function-specific immutable Params interface structure, which is only present when the function can have parameters. Then we add the contract-specific State interface structure. In this case, it is mutable because setOwner is a Func. For Views, this would be an immutable state interface. Finally, we would add the function-specific mutable Results interface structure, which is only present when the function returns results. This is not the case for this setOwner() function.

Set Up Access Control

Next, it sets up access control for the function according to the schema definition file. In this case, it retrieves the owner state variable through the function context, requires that the variable exists, and then requires that the function's caller() equals that value. Any failing requirement will panic out of the thunk function with an error message. So, this code ensures that only the contract owner can call this function.

Verify Function Parameters

Now we get to the point where we can use the function-specific Params interface to check for mandatory parameters. Each mandatory parameter is required to exist, or else we will panic out of the thunk function with an error message.

Call the Smart Contract Function

With the setup and automated checks completed, we now call the actual smart contract function implementation the user maintains. After this function has been completed, we would process the returned results for those functions that have any (in this case, we don't have results). Finally, we log that the contract function has been completed successfully. Remember that any error within the user function will cause a panic, so this logging will never occur in case that happens.