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
publicstructCoin<phantomT>haskey,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:
Create their own type that “looks like” the expected type
Pass objects with fake phantom types
Bypass type-based security or asset separation
Vulnerable Example
modulevulnerable::pool{usesui::object::{Self,UID};usesui::tx_context::TxContext;usesui::coin::{Self,Coin};/// Pool for any coin type
publicstructPool<phantomT>haskey{id: UID,balance: u64,coin_store: Coin<T>,}/// VULNERABLE: No verification that T is the expected type
publicentryfundeposit<T>(pool: &mutPool<T>,coin: Coin<T>,){letamount=coin::value(&coin);pool.balance=pool.balance+amount;coin::join(&mutpool.coin_store,coin);}/// VULNERABLE: Anyone can create a Pool with a fake type
publicfuncreate_pool<T>(initial_coin: Coin<T>,ctx: &mutTxContext): Pool<T>{Pool{id: object::new(ctx),balance: coin::value(&initial_coin),coin_store: initial_coin,}}}modulevulnerable::lending{usesui::coin::Coin;publicstructPriceOracle<phantomT>haskey{id: UID,price_usd: u64,}/// VULNERABLE: Trusts any oracle with matching phantom type
publicentryfunborrow<T>(oracle: &PriceOracle<T>,collateral: Coin<T>,borrow_amount: u64,){// Attacker creates fake oracle with inflated price
letcollateral_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
moduleattacker::fake_sui{publicstructFAKE_SUIhasdrop{}}moduleattack::exploit{usevulnerable::lending::{Self,PriceOracle};useattacker::fake_sui::FAKE_SUI;usesui::coin;publicentryfunexploit(ctx: &mutTxContext){// Create a fake oracle with inflated price
letfake_oracle=PriceOracle<FAKE_SUI>{id: object::new(ctx),price_usd: 1_000_000_000,// Fake $1B price
};// Create worthless fake coins
letfake_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
modulesecure::pool{usesui::object::{Self,UID,ID};usesui::tx_context::TxContext;usesui::coin::{Self,Coin,CoinMetadata};usesui::transfer;usestd::type_name::{Self,TypeName};/// Registry of approved coin types
publicstructCoinRegistryhaskey{id: UID,approved_types: vector<TypeName>,}/// Pool with verified coin type
publicstructPool<phantomT>haskey{id: UID,coin_type: TypeName,// Store the actual type for verification
balance: u64,coin_store: Coin<T>,}/// SECURE: Verify coin type is approved
publicentryfuncreate_pool<T>(registry: &CoinRegistry,metadata: &CoinMetadata<T>,// Requires official metadata
initial_coin: Coin<T>,ctx: &mutTxContext){letcoin_type=type_name::get<T>();// Verify type is in approved registry
assert!(vector::contains(®istry.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
publicentryfundeposit<T>(pool: &mutPool<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);letamount=coin::value(&coin);pool.balance=pool.balance+amount;coin::join(&mutpool.coin_store,coin);}}modulesecure::lending{usesui::object::{Self,UID,ID};usesui::coin::{Self,Coin};usestd::type_name::{Self,TypeName};/// Oracle with verified type and trusted source
publicstructTrustedOraclehaskey{id: UID,/// Maps type name to price
prices: Table<TypeName,PriceData>,/// Only this address can update prices
oracle_admin: address,}publicstructPriceDatahasstore{price_usd: u64,last_update: u64,decimals: u8,}/// SECURE: Oracle verifies types internally
publicentryfunborrow<T>(oracle: &TrustedOracle,collateral: Coin<T>,borrow_amount: u64,ctx: &TxContext){letcoin_type=type_name::get<T>();// Get price from trusted oracle
assert!(table::contains(&oracle.prices,coin_type),E_UNKNOWN_ASSET);letprice_data=table::borrow(&oracle.prices,coin_type);// Check freshness
assert!(clock::timestamp_ms(clock)-price_data.last_update<MAX_STALENESS,E_STALE_PRICE);letcollateral_value=coin::value(&collateral)*price_data.price_usd;assert!(borrow_amount<=collateral_value/2,E_UNDERCOLLATERALIZED);// ... proceed with borrow
}}
/// Only the module defining T can create this witness
publicstructTypeWitness<phantomT>hasdrop{}/// Require witness to prove type authenticity
publicfunverified_action<T>(_witness: TypeWitness<T>,...){// Only code with access to T's module can call this
}
Pattern 3: Coin Metadata Verification
usesui::coin::CoinMetadata;/// Require CoinMetadata proves the type is a real coin
publicfundeposit_verified<T>(_metadata: &CoinMetadata<T>,coin: Coin<T>,...){// CoinMetadata only exists for properly created coins
}
Pattern 4: Store Type Information
publicstructTypedContainer<phantomT>haskey{id: UID,stored_type: TypeName,// Remember what T was
data: vector<u8>,}publicfunverify_container<T>(container: &TypedContainer<T>){assert!(container.stored_type==type_name::get<T>(),E_TYPE_MISMATCH);}
Recommended Mitigations
1. Use Type Name for Verification
usestd::type_name;publicfunoperation<T>(...){lettype_name=type_name::get<T>();// Compare against expected types
}
2. Require Official Artifacts
/// For coins, require CoinMetadata
publicfuncoin_operation<T>(metadata: &CoinMetadata<T>,// Proves T is a real coin
coin: Coin<T>,){}
/// OTW guarantees type uniqueness
publicstructMY_TOKENhasdrop{}publicfuninit(witness: MY_TOKEN,ctx: &mutTxContext){// 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