Trigger FA_STANDARD flag detected (protocol uses FungibleAsset standard) - Used by Breadth agents, depth-token-flow
Trigger: FA_STANDARD flag detected (protocol uses FungibleAsset standard) Used by: Breadth agents, depth-token-flow Covers: FungibleAsset metadata validation, zero-value exploitation, store ownership, dispatchable hooks, Ref safety, Coin-to-FA migration
Audit FungibleAsset standard usage for Aptos-specific vulnerabilities. The FA standard introduces object-based token management with capabilities (MintRef, BurnRef, TransferRef, FreezeRef) and optional dispatchable hooks. Incorrect usage creates counterfeit token acceptance, forced transfers, reentrancy, and accounting mismatches.
For EVERY function that accepts a FungibleAsset parameter or reads from a :
FungibleStore| # | Function | Accepts FA/Reads Store | Validates Metadata? | Expected Metadata | Bypass Possible? |
|---|---|---|---|---|---|
| 1 | {func} | FungibleAsset param | YES/NO | {expected_metadata_obj} | YES/NO |
How metadata validation works:
// CORRECT: validates the asset is the expected type
let metadata = fungible_asset::metadata(&fa);
assert!(metadata == expected_metadata, ERROR_WRONG_ASSET);
// VULNERABLE: no validation - accepts ANY FungibleAsset
public fun deposit(fa: FungibleAsset) {
// Attacker can pass a worthless FA created from their own metadata
fungible_asset::deposit(store, fa);
}
MANDATORY SEARCH: Grep all .move files for:
FungibleAsset in function signatures (parameters)fungible_asset::metadata(&fa) is called and comparedfungible_asset::amount(&fa) without metadata check -> FLAGSeverity: Accepting unvalidated FungibleAsset = accepting counterfeit tokens. If the function credits the user or modifies protocol state based on the FA amount -> HIGH/CRITICAL.
Analyze zero-value FungibleAsset paths:
| # | Zero-Value Source | Code Path Triggered | State Modified? | Cleanup Correct? |
|---|---|---|---|---|
| 1 | fungible_asset::zero(metadata) | {trace what happens} | YES/NO | YES/NO |
| 2 | Withdrawal of 0 amount | {trace} | YES/NO | YES/NO |
Check for each:
fungible_asset::zero(metadata) be used to trigger code paths that modify state? (e.g., register a user, set a flag, emit an event)fungible_asset::destroy_zero(fa) clean up properly, or does it leave dangling state?amount == 0 get explicitly checked and rejected at entry points?Pattern: Zero-value operations often bypass amount > 0 checks that were assumed but never written, allowing state modifications without economic cost.
Audit FungibleStore creation, ownership chains, and access control:
| Store Type | Created By | Creation Permissionless? | Owner | Can Attacker Create? |
|---|---|---|---|---|
| Primary store | primary_fungible_store::ensure_primary_store_exists() | YES - anyone can create for any address | Address owner | YES (for any address) |
| Custom store | fungible_asset::create_store() on ConstructorRef | Only during object construction | Object owner | Depends on who can construct |
CRITICAL: primary_fungible_store::ensure_primary_store_exists(addr, metadata) is permissionless. An attacker can create a primary store for ANY address for ANY metadata. If the protocol assumes a store's existence means the user has interacted with the protocol -> FINDING.
| Object A | Owns Object B | B Has FungibleStore | A Can Withdraw from B? |
|---|---|---|---|
| {object} | {child_object} | YES/NO | YES - via object ownership chain |
Check: If Object A owns Object B which owns a FungibleStore, the owner of Object A can withdraw from B's store through the ownership chain. Trace all object ownership hierarchies for unintended fund access paths.
| Function | Expects Store At | Actually Reads From | Match? |
|---|---|---|---|
| {func} | Protocol-controlled store | User-supplied address | VERIFY |
Pattern: Protocol calculates expected store address but user can supply a different store address. If the function doesn't verify the store belongs to the expected object/address -> FINDING.
If the protocol uses dispatchable FungibleAsset (custom withdraw, deposit, or derived_balance hooks):
| Hook Type | Registered? | Implementation Module | Can Reenter? | Can Revert? | Can Manipulate? |
|---|---|---|---|---|---|
| withdraw | YES/NO | {module::func} | ANALYZE | ANALYZE | ANALYZE |
| deposit | YES/NO | {module::func} | ANALYZE | ANALYZE | ANALYZE |
| derived_balance | YES/NO | {module::func} | ANALYZE | N/A | ANALYZE |
For each registered hook:
#[module_lock] applied to the registering module? (prevents indirect reentrancy but NOT direct)Reentrancy sequence:
Module::transfer() {
1. Read balance (CHECK)
2. Deduct from source store → triggers withdraw hook (INTERACTION before EFFECT completion)
3. Withdraw hook reenters Module::another_function()
4. another_function() sees partially-updated state
// ...
}
Can a deposit hook unconditionally revert to prevent deposits into a specific store?
If derived_balance hook is registered:
fungible_asset::balance(store) expecting the real balance?balance() calls derived_balance hook if registered - the returned value may differ from actual stored amountAudit the lifecycle and access control of FungibleAsset capability references:
| Ref Type | Stored Where | Who Has Access | Can Be Extracted? | Impact If Leaked |
|---|---|---|---|---|
| MintRef | {object/resource} | {module/address} | YES/NO | Infinite token minting |
| BurnRef | {object/resource} | {module/address} | YES/NO | Destroy any user's tokens |
| TransferRef | {object/resource} | {module/address} | YES/NO | Bypass freeze, forced transfers |
| FreezeRef | {object/resource} | {module/address} | YES/NO | Freeze any user's store |
MANDATORY CHECK for each Ref:
key only? (safe - not extractable)store ability? (dangerous - can be moved out)TransferRef allows transfers that bypass freeze status:
fungible_asset::transfer_with_ref(ref, from_store, to_store, amount))| Ref Type | Can Be Destroyed? | Destruction Function | Consequences of Destruction |
|---|---|---|---|
| MintRef | NO (no destroy function) | N/A | Permanent minting capability |
| BurnRef | YES (burn_ref::destroy) | {if exists} | Cannot burn tokens anymore |
| TransferRef | {check} | {if exists} | Cannot force-transfer anymore |
If the protocol handles both Coin<T> and FungibleAsset:
| # | Check | Status | Impact |
|---|---|---|---|
| 1 | Are Coin and FA treated equivalently in balance accounting? | YES/NO | {if NO: describe discrepancy} |
| 2 | Does total_supply track both representations? | YES/NO | {if NO: supply tracking broken} |
| 3 | Can user deposit as Coin, then withdraw as FA (or vice versa), exploiting accounting difference? | YES/NO | {describe path} |
| 4 | Are there functions that only accept Coin but credit FA internally (or vice versa)? | YES/NO | {conversion correct?} |
| 5 | If protocol converts Coin<T> to FA: does coin::coin_to_fungible_asset() preserve exact amount? | VERIFY | {check for fees or rounding} |
Pattern: When a protocol accepts both Coin<T> and FungibleAsset for the same underlying token, internal accounting that tracks only one representation can be exploited by depositing in one form and withdrawing in the other.
primary_fungible_store_address(owner, metadata)) - "unexpected address" may be intentionaldeposit) may already reject zero amounts internally - verify## Finding [FA-N]: Title
**Verdict**: CONFIRMED / PARTIAL / REFUTED / CONTESTED
**Step Execution**: ✓1,2,3,4,5,6 | ✗N(reason) | ?N(uncertain)
**Rules Applied**: [R1:✓/✗, R4:✓/✗, R10:✓/✗, R11:✓/✗]
**Severity**: Critical/High/Medium/Low/Info
**Location**: module_name.move:LineN
**FA Component**: {metadata/store/hook/ref/accounting}
**Attack Vector**: {counterfeit deposit / reentrancy via hook / forced transfer via TransferRef / ...}
**Description**: What's wrong
**Impact**: What can happen (fund theft, accounting mismatch, DoS)
**Evidence**: Code snippets showing the vulnerability
**Recommendation**: How to fix
### Precondition Analysis (if PARTIAL/REFUTED)
**Missing Precondition**: [What blocks exploitation]
**Precondition Type**: STATE / ACCESS / TIMING / EXTERNAL / BALANCE
### Postcondition Analysis (if CONFIRMED/PARTIAL)
**Postconditions Created**: [What conditions this creates]
**Postcondition Types**: [List applicable types]
**Who Benefits**: [Who can use these]
| Step | Required | Completed? | Notes |
|---|---|---|---|
| 1. Metadata Validation Audit | YES | ✓/✗/? | Every FA-accepting function checked |
| 2. Zero-Value Exploitation | YES | ✓/✗/? | |
| 3. Store Creation and Ownership | YES | ✓/✗/? | Primary store permissionless creation checked |
| 3b. Transitive Ownership | YES | ✓/✗/? | Object ownership chains traced |
| 4. Dispatchable Hook Analysis | IF dispatchable FA used | ✓/✗(N/A)/? | |
| 4b. Reentrancy via Hooks | IF hooks registered | ✓/✗(N/A)/? | |
| 4c. Deposit Hook Blocking | IF deposit hook registered | ✓/✗(N/A)/? | |
| 4d. Derived Balance Manipulation | IF derived_balance hook | ✓/✗(N/A)/? | |
| 5. Ref Safety Analysis | YES | ✓/✗/? | All 4 Ref types located and access traced |
| 5b. TransferRef Bypass | IF TransferRef exists | ✓/✗(N/A)/? | |
| 6. Coin-to-FA Migration Accounting | IF both Coin and FA supported | ✓/✗(N/A)/? |
If any step skipped, document valid reason (N/A, no dispatchable hooks, no Coin support, no TransferRef).