The Witness Design Pattern
Next, we need to understand the witness pattern to peek under the hood of how a fungible token is implemented in Sui Move.
Witness is a design pattern used to prove that the resource or type in question, A
, can be initiated only once after the ephemeral witness
resource has been consumed. The witness
resource must be immediately consumed or dropped after use, ensuring that it cannot be reused to create multiple instances of A
.
Witness Pattern Example
In the below example, the witness
resource is PEACE
, while the type A
that we want to control the instantiation of is Guardian
.
The witness
resource type must have the drop
keyword so that this resource can be dropped after being passed into a function. We see that the instance of PEACE
resource is passed into the create_guardian
method and dropped (note the underscore before witness
), ensuring that only one instance of Guardian
can be created.
#![allow(unused)] fn main() { /// Module that defines a generic type `Guardian<T>` which can only be /// instantiated with a witness. module witness::peace { use sui::object::{Self, UID}; use sui::transfer; use sui::tx_context::{Self, TxContext}; /// Phantom parameter T can only be initialized in the `create_guardian` /// function. But the types passed here must have `drop`. public struct Guardian<phantom T: drop> has key, store { id: UID } /// This type is the witness resource and is intended to be used only once. public struct PEACE has drop {} /// The first argument of this function is an actual instance of the /// type T with `drop` ability. It is dropped as soon as received. public fun create_guardian<T: drop>( _witness: T, ctx: &mut TxContext ): Guardian<T> { Guardian { id: object::new(ctx) } } /// Module initializer is the best way to ensure that the /// code is called only once. With `Witness` pattern it is /// often the best practice. fun init(witness: PEACE, ctx: &mut TxContext) { transfer::transfer( create_guardian(witness, ctx), tx_context::sender(ctx) ) } } }
The example above is modified from the excellent book Sui Move by Example by Damir Shamanaev.
The phantom
Keyword
In the above example, we want the Guardian
type to have the key
and store
abilities, so that it's an asset and is transferrable and persists in global storage.
We also want to pass in the witness
resource, PEACE
, into Guardian
, but PEACE
only has the drop
ability. Recall our previous discussion on ability constraints and inner types, the rule implies that PEACE
should also have key
and storage
given that the outer type Guardian
does. But in this case, we do not want to add unnecessary abilities to our witness
type, because doing so could cause undesirable behaviors and vulnerabilities.
We can use the keyword phantom
to get around this situation. When a type parameter is either not used inside the struct definition or is only used as an argument to another phantom
type parameter, we can use the phantom
keyword to ask the Move type system to relax the ability constraint rules on inner types. We see that Guardian
doesn't use the type T
in any of its fields, so we can safely declare T
to be a phantom
type.
For a more in-depth explanation of the phantom
keyword, please check the relevant section of the Move language documentation.
One Time Witness
One Time Witness (OTW) is a sub-pattern of the Witness pattern, where we utilize the module init
function to ensure that only one instance of the witness
resource is created (so type A
is guaranteed to be a singleton).
In Sui Move a type is considered an OTW if its definition has the following properties:
- The type is named after the module but uppercased
- The type only has the
drop
ability
To get an instance of this type, you need to add it as the first argument to the module init
function as in the above example. The Sui runtime will then generate the OTW struct automatically at module publish time.
The above example uses the One Time Witness design pattern to guarantee that Guardian
is a singtleton.