Community Vote

I spent the last three weeks building Frontier Flow, a low-code programming environment for EVE Frontier. I believe this is a gamechanger for the future of Frontier. Please consider voting for me. Every vote counts.

12. Phantom Type Confusion

Overview

Phantom type parameters in Move are type parameters that don’t affect the runtime representation of a struct. Attackers can inject structurally-identical types with different phantom parameters, bypassing type-based security checks.

Risk Level

High — Can bypass type-based access control and asset isolation.

OWASP / CWE Mapping

OWASP Top 10 MITRE CWE
A04 (Insecure Design) CWE-693 (Protection Mechanism Failure), CWE-704 (Incorrect Type Conversion)

The Problem

Phantom Types Explained

/// `phantom` means T doesn't appear in any field
public struct Coin<phantom T> has key, store {
    id: UID,
    value: u64,
}

/// At runtime, Coin<SUI> and Coin<USDC> have identical layouts
/// Only the type parameter differs

The Vulnerability

If your code doesn’t verify the phantom type parameter, attackers can:

  1. Create their own type that “looks like” the expected type
  2. Pass objects with fake phantom types
  3. Bypass type-based security or asset separation

Vulnerable Example

module vulnerable::pool {
    use sui::object::{Self, UID};
    use sui::tx_context::TxContext;
    use sui::coin::{Self, Coin};

    /// Pool for any coin type
    public struct Pool<phantom T> has key {
        id: UID,
        balance: u64,
        coin_store: Coin<T>,
    }

    /// VULNERABLE: No verification that T is the expected type
    public entry fun deposit<T>(
        pool: &mut Pool<T>,
        coin: Coin<T>,
    ) {
        let amount = coin::value(&coin);
        pool.balance = pool.balance + amount;
        coin::join(&mut pool.coin_store, coin);
    }

    /// VULNERABLE: Anyone can create a Pool with a fake type
    public fun create_pool<T>(
        initial_coin: Coin<T>,
        ctx: &mut TxContext
    ): Pool<T> {
        Pool {
            id: object::new(ctx),
            balance: coin::value(&initial_coin),
            coin_store: initial_coin,
        }
    }
}

module vulnerable::lending {
    use sui::coin::Coin;

    public struct PriceOracle<phantom T> has key {
        id: UID,
        price_usd: u64,
    }

    /// VULNERABLE: Trusts any oracle with matching phantom type
    public entry fun borrow<T>(
        oracle: &PriceOracle<T>,
        collateral: Coin<T>,
        borrow_amount: u64,
    ) {
        // Attacker creates fake oracle with inflated price
        let collateral_value = coin::value(&collateral) * oracle.price_usd;

        // Borrow against inflated value
        assert!(borrow_amount <= collateral_value / 2, E_UNDERCOLLATERALIZED);
        // ...
    }
}

Attack Scenario

/// Attacker's fake token that mimics SUI
module attacker::fake_sui {
    public struct FAKE_SUI has drop {}
}

module attack::exploit {
    use vulnerable::lending::{Self, PriceOracle};
    use attacker::fake_sui::FAKE_SUI;
    use sui::coin;

    public entry fun exploit(ctx: &mut TxContext) {
        // Create a fake oracle with inflated price
        let fake_oracle = PriceOracle<FAKE_SUI> {
            id: object::new(ctx),
            price_usd: 1_000_000_000,  // Fake $1B price
        };

        // Create worthless fake coins
        let fake_coins = coin::zero<FAKE_SUI>(ctx);

        // Borrow against "valuable" fake collateral
        lending::borrow<FAKE_SUI>(
            &fake_oracle,
            fake_coins,
            999_999_999,  // Borrow almost a billion
        );
    }
}

Secure Example

module secure::pool {
    use sui::object::{Self, UID, ID};
    use sui::tx_context::TxContext;
    use sui::coin::{Self, Coin, CoinMetadata};
    use sui::transfer;
    use std::type_name::{Self, TypeName};

    /// Registry of approved coin types
    public struct CoinRegistry has key {
        id: UID,
        approved_types: vector<TypeName>,
    }

    /// Pool with verified coin type
    public struct Pool<phantom T> has key {
        id: UID,
        coin_type: TypeName,  // Store the actual type for verification
        balance: u64,
        coin_store: Coin<T>,
    }

    /// SECURE: Verify coin type is approved
    public entry fun create_pool<T>(
        registry: &CoinRegistry,
        metadata: &CoinMetadata<T>,  // Requires official metadata
        initial_coin: Coin<T>,
        ctx: &mut TxContext
    ) {
        let coin_type = type_name::get<T>();

        // Verify type is in approved registry
        assert!(
            vector::contains(&registry.approved_types, &coin_type),
            E_UNAPPROVED_COIN
        );

        transfer::share_object(Pool<T> {
            id: object::new(ctx),
            coin_type,
            balance: coin::value(&initial_coin),
            coin_store: initial_coin,
        });
    }

    /// SECURE: Verify pool's coin type matches
    public entry fun deposit<T>(
        pool: &mut Pool<T>,
        coin: Coin<T>,
    ) {
        // Type T is enforced by the borrow checker
        // But we can add extra verification
        assert!(pool.coin_type == type_name::get<T>(), E_TYPE_MISMATCH);

        let amount = coin::value(&coin);
        pool.balance = pool.balance + amount;
        coin::join(&mut pool.coin_store, coin);
    }
}

module secure::lending {
    use sui::object::{Self, UID, ID};
    use sui::coin::{Self, Coin};
    use std::type_name::{Self, TypeName};

    /// Oracle with verified type and trusted source
    public struct TrustedOracle has key {
        id: UID,
        /// Maps type name to price
        prices: Table<TypeName, PriceData>,
        /// Only this address can update prices
        oracle_admin: address,
    }

    public struct PriceData has store {
        price_usd: u64,
        last_update: u64,
        decimals: u8,
    }

    /// SECURE: Oracle verifies types internally
    public entry fun borrow<T>(
        oracle: &TrustedOracle,
        collateral: Coin<T>,
        borrow_amount: u64,
        ctx: &TxContext
    ) {
        let coin_type = type_name::get<T>();

        // Get price from trusted oracle
        assert!(table::contains(&oracle.prices, coin_type), E_UNKNOWN_ASSET);
        let price_data = table::borrow(&oracle.prices, coin_type);

        // Check freshness
        assert!(
            clock::timestamp_ms(clock) - price_data.last_update < MAX_STALENESS,
            E_STALE_PRICE
        );

        let collateral_value = coin::value(&collateral) * price_data.price_usd;
        assert!(borrow_amount <= collateral_value / 2, E_UNDERCOLLATERALIZED);

        // ... proceed with borrow
    }
}

Type Verification Patterns

Pattern 1: Type Name Registry

use std::type_name::{Self, TypeName};

public struct TypeRegistry has key {
    id: UID,
    allowed_types: vector<TypeName>,
}

public fun verify_type<T>(registry: &TypeRegistry) {
    let t = type_name::get<T>();
    assert!(vector::contains(&registry.allowed_types, &t), E_INVALID_TYPE);
}

Pattern 2: Witness-Based Type Verification

/// Only the module defining T can create this witness
public struct TypeWitness<phantom T> has drop {}

/// Require witness to prove type authenticity
public fun verified_action<T>(
    _witness: TypeWitness<T>,
    ...
) {
    // Only code with access to T's module can call this
}

Pattern 3: Coin Metadata Verification

use sui::coin::CoinMetadata;

/// Require CoinMetadata proves the type is a real coin
public fun deposit_verified<T>(
    _metadata: &CoinMetadata<T>,
    coin: Coin<T>,
    ...
) {
    // CoinMetadata only exists for properly created coins
}

Pattern 4: Store Type Information

public struct TypedContainer<phantom T> has key {
    id: UID,
    stored_type: TypeName,  // Remember what T was
    data: vector<u8>,
}

public fun verify_container<T>(container: &TypedContainer<T>) {
    assert!(container.stored_type == type_name::get<T>(), E_TYPE_MISMATCH);
}

1. Use Type Name for Verification

use std::type_name;

public fun operation<T>(...) {
    let type_name = type_name::get<T>();
    // Compare against expected types
}

2. Require Official Artifacts

/// For coins, require CoinMetadata
public fun coin_operation<T>(
    metadata: &CoinMetadata<T>,  // Proves T is a real coin
    coin: Coin<T>,
) { }

3. Maintain Type Whitelists

public struct Config has key {
    id: UID,
    allowed_types: vector<TypeName>,
}

public fun add_allowed_type(cap: &AdminCap, config: &mut Config, type_name: TypeName) {
    vector::push_back(&mut config.allowed_types, type_name);
}

4. Use One-Time-Witness Pattern

/// OTW guarantees type uniqueness
public struct MY_TOKEN has drop {}

public fun init(witness: MY_TOKEN, ctx: &mut TxContext) {
    // Only called once, witness proves authenticity
}

Testing Checklist

  • Test with attacker-created types that mimic expected types
  • Verify type registry correctly rejects unknown types
  • Test that phantom type verification catches mismatches
  • Confirm OTW pattern is used for critical type creation
  • Audit all generic functions for phantom type assumptions