advanced
+500 XP

Advanced Generics & Patterns

The final boss of Move on Sui. You'll learn the design patterns that power every serious Sui protocol — generics, phantom types, witness, one-time witness, hot potato, and capability. It's the most advanced lesson, but you've built up to this. Let's go.

Lesson Syllabus

Generics and Ability Constraints
🧬

Generic Types in Move

Generics are like making a container that works with anything. Instead of building a "Box for swords" and a "Box for potions" and a "Box for gold," you build a "Box for <anything>" and specify what goes in it when you actually use it. That's what `struct Box<T>` means — T is a placeholder for "whatever type you want." But here's the catch: you usually don't want your box to hold literally anything. What if someone tries to put something in that can't be stored on the blockchain? That's where **ability constraints** come in. When you write `<T: store>`, you're saying "T can be any type, as long as it has the `store` ability." It's like saying "this shelf can hold anything, as long as it fits through the door." The compiler checks this for you at build time — no surprises at runtime. You can even have multiple placeholders: `struct Pair<A: store, B: store>` holds two different storable types. The Sui framework uses generics everywhere — `Coin<T>` is the classic example, where T identifies which currency the coin represents (SUI, USDC, etc.).

👻

Phantom Type Parameters

This one's a bit mind-bending, but stay with me. A **phantom type parameter** is a label that exists only to tell types apart — like how a $1 bill and a $100 bill are both pieces of paper, but the number printed on them makes them very different. `Coin<SUI>` and `Coin<USDC>` work the same way: they have the exact same internal structure (a UID and a balance), but the type system treats them as completely different things. You can't accidentally mix them up. So why the `phantom` keyword? Here's the practical reason: normally, if your struct has the `store` ability, the compiler requires ALL type parameters to also have `store`. But currency marker types like `SUI` or `USDC` are tiny empty structs that might only have `drop`. Without `phantom`, the Coin struct couldn't have `store` because its type parameter T doesn't have `store`. The `phantom` keyword says "hey compiler, T isn't used in any field — it's just a label — so don't enforce ability requirements on it." This pattern is the backbone of the Sui token system. Every time you see `Coin<phantom T>`, that phantom T is what keeps your SUI separate from your USDC, even though both coins look identical under the hood.

🔍

Type Reflection and Type Names

Sometimes you need to ask "what type am I working with?" at runtime. Sui Move gives you the `std::type_name` module for this. Calling `type_name::get<T>()` returns the fully qualified name of type T as a string — like getting a type's ID card. Where this gets really useful is building **type-indexed maps**. Imagine you're building a DeFi protocol that supports multiple coins. You need a collection that stores one entry per coin type. You can use a `Bag` (a heterogeneous collection) with `TypeName` keys: the key for the SUI entry is the TypeName of SUI, the key for USDC is the TypeName of USDC, and so on. Each type gets exactly one slot. This is how many real Sui DeFi protocols manage multiple coin types — one shared object with a Bag inside it. Another use is **type guards**: double-checking at runtime that a generic parameter matches what you expect. While Move's type system catches most errors at compile time, type guards are handy when working with dynamic fields or bags where type information can get erased.

The Witness and One-Time Witness Patterns
👁️

The Witness Pattern

The witness pattern is like showing your employee badge to prove you work at a company. Only your module can create the badge, so showing it proves you're authorized. Here's how it works: a witness is a struct — usually with only the `drop` ability and no fields — that can only be created inside the module that defines it. This is a fundamental rule in Move: you can only write `MyStruct {}` inside the module where `MyStruct` is defined. Nobody else can forge your struct. So if a framework function says "give me a value of type T," only the module that defines T can create one and pass it in. That's the entire trick! No signatures, no registries, no permission lists — just the type system doing its thing. This pattern is used everywhere in Sui: `coin::create_currency` requires a witness to prove you're the module that defines the coin type. `transfer_policy::new` requires a witness to prove you're the creator of the NFT type. The witness itself is typically a zero-sized struct (no fields) that gets created, passed to the framework function, and immediately dropped. It exists purely as a proof of identity.

1️⃣

One-Time Witness (OTW)

A One-Time Witness is like a golden ticket — it's created exactly once, automatically, when your code is first published. It's used to prove "this is the first and only time this setup happens." The Sui runtime creates exactly one instance of the OTW and passes it as the first parameter to your module's `init` function. After `init` returns, no more instances can ever be created. Ever. This is how `coin::create_currency` ensures each coin type is set up exactly once. The rules for making a valid OTW are strict (and a common source of mistakes, so pay attention): 1. It must be named after the module in **UPPER_CASE** (module `my_coin` -> struct `MY_COIN`) 2. It must have **only the `drop` ability** (no key, store, or copy) 3. It must have **no fields** (completely empty) 4. It must not be created anywhere in your code — Sui creates it for you and passes it to `init` A common beginner mistake is adding fields or extra abilities to the OTW struct. Don't! It needs to be as minimal as possible: `public struct MY_COIN has drop {}` and nothing more. You can even verify an OTW at runtime using `sui::types::is_one_time_witness<T>(witness)` — this returns true only for the real deal.

🪙

OTW in Practice: Creating a Currency

Let's see the OTW in action with the most common real-world use: creating a fungible token using `coin::create_currency`. This function takes your OTW witness as its first argument — guaranteeing the currency can only be created once, during module publication. It returns two objects: a `TreasuryCap<T>` (the minting authority — whoever has this can create new coins) and a `CoinMetadata<T>` (the coin's name, symbol, decimals, etc.). The typical pattern is: freeze the CoinMetadata so it's publicly readable but nobody can tamper with it, and transfer the TreasuryCap to the deployer (or share it, depending on your design). Here's why this design is so elegant: the OTW proves you are the module author AND that this is the first (and only) time this setup runs. The phantom type parameter T on Coin, TreasuryCap, and CoinMetadata ties everything back to your module. Your coin type is forever linked to your module — no one can create a fake version. This same pattern extends beyond coins: `display::new<T>(witness)` for NFT display metadata, `transfer_policy::new<T>(witness)` for transfer rules, and many other Sui framework functions all use this approach.

Hot Potato, Capability, and Advanced Design Patterns
🥔

The Hot Potato Pattern

This is a fun one! A hot potato is data you MUST pass to someone else — you can't drop it, store it, or ignore it. Like in the kids' game, you HAVE to pass it along before the music stops. Technically, a hot potato is a struct with **no abilities at all** — no `key`, no `store`, no `drop`, no `copy`. Because it has no `drop`, the compiler won't let you silently discard it. Because it has no `key` or `store`, you can't stash it anywhere. The ONLY way to get rid of it is to pass it to a function that explicitly destroys it by destructuring. Why is this useful? Think about flash loans. A `borrow()` function gives you some coins AND a hot potato receipt. You can do whatever you want with the coins, but that receipt is burning a hole in your hands. The only way to get rid of it is to call `repay()`, which destructures the receipt and verifies you paid back the loan plus fees. If you don't call `repay()`? The transaction aborts — the compiler literally won't let you ignore the hot potato. This pattern is also great for multi-step workflows: Step 1 returns a receipt, Step 2 requires it, Step 3 consumes it. If you skip a step, the compiler catches it. It's like a forced checklist that the type system enforces.

🔑

The Capability Pattern

Instead of checking someone's ID card, you check if they're holding the right key. Whoever has the admin key can do admin things. Keys can be handed off, destroyed, or locked away. Let's compare this to the alternative. In some blockchains, you'd write something like "if the caller's address equals the admin address, allow the action." That works, but it's rigid — what if you want to change the admin? You'd need to upgrade the entire package. What if you want two admins? Or a time-delayed admin? Things get messy fast. With the capability pattern, you create an `AdminCap` object (a struct with `key` and optionally `store`) during `init` and transfer it to the deployer. Admin functions take `_: &AdminCap` as a parameter — just having a reference to the cap proves you own it. Want to transfer admin rights? Just transfer the object. Want to revoke access forever? Destroy the cap. Want to add a time-lock? Wrap the cap in an escrow struct. The Sui framework uses this pattern extensively. `TreasuryCap` is a capability for minting coins. `Publisher` is a capability for package-level operations. Once you recognize the pattern, you'll see it everywhere.

🏗️

Composing Patterns: Real-World Architecture

Here's where it all comes together. In the real world, production Sui protocols don't use just one of these patterns — they combine them. Each pattern solves one specific problem, and they fit together like LEGO bricks because Move's type system keeps them cleanly separated. **OTW + Capability**: Your `init` uses the OTW to create a currency and receives a `TreasuryCap` in return. The TreasuryCap IS a capability — whoever owns it can mint coins. One pattern for one-time setup, another for ongoing access control. **Witness + Phantom Types**: `transfer_policy::new<T>(witness)` uses a witness to prove you wrote the code for type T. The phantom type T links the policy to your specific NFT type. Authorization plus type safety in one function call. **Hot Potato + Capability**: Flash loans use a hot potato for forced repayment, while admin functions (guarded by a capability) control fee parameters. Two independent concerns, composed cleanly. **Capability + Wrapping**: Wrap an AdminCap inside a time-lock struct. The capability can't be used until the time-lock expires and the wrapper is unpacked. Instant delayed governance. The key insight: each pattern solves one problem (authorization, one-time setup, forced completion, type safety), and they compose cleanly because they don't step on each other's toes.