What is a one-time witness in Sui Move?
November 5, 2026
A one-time witness (OTW) is a special struct that the Sui runtime creates exactly once, when the module is published, and passes as the first argument to that module's init function. Nobody can construct one manually, so holding an OTW proves "this code is running in the publish transaction of module X, and only once". Framework functions such as sui::package::claim (for the Publisher object) and coin_registry::new_currency_with_otw (for creating a coin) require one, which is how Sui guarantees there is only one Publisher per module and one TreasuryCap per coin type.
The rules
Sui treats a type as an OTW if it:
- Has only the
dropability. - Has no fields.
- Is not generic.
- Is named after its module in all uppercase letters.
module book::my_token;
/// The OTW for `book::my_token`.
public struct MY_TOKEN has drop {}
fun init(otw: MY_TOKEN, ctx: &mut TxContext) {
// Use `otw` here: claim a Publisher, create a currency, etc.
}The module my_token gets the OTW MY_TOKEN. Any code that tries to write MY_TOKEN {} fails to compile, and because init runs only once per module, there is only ever one instance.
Why a regular witness isn't enough
The ordinary witness pattern proves that a caller owns a type T: only the module that defines T can create a value of it. It says nothing about how many times. A coin library that accepts a plain witness can be called twice from the same init, quietly creating a second treasury. Requiring an OTW closes that hole, because the second call would need a second instance and there isn't one.
Checking for an OTW in your own library
If you write a function that should only accept a real one-time witness, check it at runtime with sui::types::is_one_time_witness:
module book::registry;
use sui::types;
const ENotOneTimeWitness: u64 = 0;
public fun register<T: drop>(otw: T) {
assert!(types::is_one_time_witness(&otw), ENotOneTimeWitness);
// ...
}This is exactly what sui::package::claim does before creating a Publisher.
Gotchas
- The name must match. A struct called
MyTokenorTOKENin modulemy_tokenis not an OTW. It's just a normal struct: aninitthat takes it as the first argument fails verification at publish, andis_one_time_witnessreturns false for it. - No other abilities. Adding
copyorstoredisqualifies it. - Only in
init. The OTW is only delivered toinit. Modules added later in a package upgrade don't have theirinitrun, so they never receive an OTW. - Use it or drop it. Since it has
drop, an OTW you don't need is discarded automatically at the end ofinit.
OTWs in tests
Tests can't get an OTW from the runtime, so the framework provides test-only helpers:
#[test]
fun test_init() {
let otw = sui::test_utils::create_one_time_witness<MY_TOKEN>();
let mut ctx = tx_context::dummy();
init(otw, &mut ctx);
}For Publisher specifically, sui::package::test_claim builds a Publisher without the OTW check. More on testing in unit tests for Sui Move.
Sources
- One Time Witness, The Move Book
- Module Initializer, The Move Book
sui::typesandsui::packagesource, MystenLabs/sui