<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Possible Vulnerabilities :: Unofficial EVE Frontier Development Notes</title>
    <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/index.html</link>
    <description>This section documents 34 vulnerability classes commonly found in Sui Move smart contracts. Each vulnerability has its own dedicated page with detailed explanations, vulnerable code examples, and recommended mitigations.&#xA;Overview Sui Move contracts face unique security challenges due to the object-centric model, capability-based access control, and programmable transaction blocks (PTBs). Understanding these vulnerabilities is essential for writing secure smart contracts.&#xA;Vulnerability Categories Access Control &amp; Authorization (1-9) Object Transfer Misuse - Unintended object transfers breaking invariants Object Freezing Misuse - Malicious freezing of critical objects Numeric / Bitwise Pitfalls - Overflow and shift operation issues Ability Misconfiguration - Improper copy, drop, store, key abilities Access-Control Mistakes - TxContext and sender verification issues Shared Object DoS - Denial of service via shared object contention Improper Object Sharing - Accidental exposure of objects as shared Dynamic Field Misuse - Child-object and dynamic field vulnerabilities Sponsored Transaction Pitfalls - Meta-transaction authority confusion Logic &amp; State Management (10-20) General Move Logic Errors - PTB reordering and mutation issues Capability Leakage - Authority leakage via indirect APIs Phantom Type Confusion - Type parameter injection attacks Unsafe Object ID Usage - Identity assumptions on child objects Dynamic Field Key Collisions - Key collision vulnerabilities Event Design Vulnerabilities - Ambiguous or missing events Unbounded Child Growth - State bloat from unlimited children PTB Ordering Issues - Non-deterministic PTB execution PTB Refund Issues - Inconsistent state from partial execution Ownership Model Confusion - Incorrect ownership transitions Weak Initializers - Reinitialization attacks External Integration &amp; Advanced (21-34) Oracle Validation Failures - Off-chain oracle trust issues Unsafe Option Authority - Authority toggles via Option Clock Time Misuse - Timestamp and time logic vulnerabilities Transfer API Misuse - Object ownership model transitions Unbounded Vector Growth - Gas exhaustion from large vectors Upgrade Boundary Errors - ABI breaks on package upgrades Event State Inconsistency - State/event synchronization Read API Leakage - Information exposure via view functions Unsafe BCS Parsing - Off-chain deserialization issues Unsafe Test Patterns - Test code leaking to production Unvalidated Struct Fields - Missing input validation Inefficient PTB Composition - Gas exhaustion patterns Overuse of Shared Objects - Unnecessary sharing risks Parent Child Authority - Implicit authority assumptions OWASP / MITRE CWE Mapping # Vulnerability Class OWASP Top 10 MITRE CWE 1 Object Transfer Misuse A01 CWE-284, CWE-275 2 Object Freezing Misuse A01 CWE-284, CWE-732 3 Numeric / Bitwise Pitfalls A06 / A03 CWE-681, CWE-190 4 Ability Misconfiguration A01 CWE-284, CWE-266 5 Access-Control Mistakes A01 CWE-285, CWE-639 6 Shared Object DoS A05 / A06 CWE-400, CWE-834 7 Improper Sharing of Objects A01 CWE-284, CWE-277 8 Dynamic Field Misuse A01 / A05 CWE-710, CWE-915 9 Sponsored TX Pitfalls A01 CWE-285, CWE-863 10 Reentrancy-like PTB Issues A01 / A04 CWE-841, CWE-362 11 Accounting / Fee Logic Bugs A04 CWE-682, CWE-840 12 Capability Leakage A01 CWE-284, CWE-668 13 Phantom Type Confusion A04 CWE-693, CWE-704 14 Unsafe object::id() A01 CWE-639, CWE-915 15 Dynamic Field Key Collisions A01 / A05 CWE-653, CWE-706 16 Event Model Vulnerabilities A04 / A09 CWE-223, CWE-778 17 Unbounded Child Growth A06 / A05 CWE-400, CWE-770 18 PTB Order Logic Flaws A04 CWE-841, CWE-662 19 Ownership-Model Confusion A01 CWE-284, CWE-266 20 Weak Initializers A01 CWE-284, CWE-665 21 Oracle Validation Failures A08 CWE-345, CWE-353 22 Unsafe Option Authority A04 CWE-696, CWE-693 23 Clock / Time Misuse A04 CWE-682, CWE-664 24 Misuse of Transfer APIs A01 CWE-284 25 Unbounded Vector Growth A05 CWE-770 26 Upgrade Boundary Errors A04 / A06 CWE-685, CWE-694 27 Event-State Inconsistency A09 CWE-778, CWE-223 28 Read API Leakage A01 CWE-200 (Info Exposure) 29 Unsafe Off-chain Parsing A08 CWE-502, CWE-116 30 Unsafe Test Signer Use A04 CWE-704, CWE-665 31 Unvalidated Struct Fields A04 CWE-20 (Input Validation) 32 Inefficient PTBs A05 / A06 CWE-400 33 Overuse of Shared Objects A01 CWE-284 34 Parent→Child Authority Assumptions A01 CWE-863, CWE-284 Tip Use the menu on the left hand side to find the article you are looking for. You can also use search at the top to search for specific terms.</description>
    <generator>Hugo</generator>
    <language>en-us</language>
    <lastBuildDate>Wed, 26 Nov 2025 21:46:35 +0000</lastBuildDate>
    <atom:link href="https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>1. Object Transfer Misuse</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/object-transfer-misuse/index.html</link>
      <pubDate>Wed, 26 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/object-transfer-misuse/index.html</guid>
      <description>Overview Any address-owned object with key (especially combined with store) can be freely transferred using sui::transfer::transfer or public_transfer. This breaks assumptions about invariants, capability possession, and ownership that your contract may depend on.&#xA;Risk Level High — Can lead to complete bypass of access control mechanisms.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A01 (Broken Access Control) CWE-284 (Improper Access Control), CWE-275 (Permission Issues) The Problem In Sui, objects with key + store abilities can be transferred by their owner to any address. If your contract issues capability objects or admin tokens and assumes they will remain with the original recipient, an attacker can transfer these objects to themselves or others, bypassing your intended access control.</description>
    </item>
    <item>
      <title>2. Object Freezing Misuse</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/object-freezing-misuse/index.html</link>
      <pubDate>Wed, 26 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/object-freezing-misuse/index.html</guid>
      <description>Overview Objects with key + store abilities can be frozen by any holder using sui::transfer::public_freeze_object. Once frozen, an object becomes immutable and globally readable. This can be exploited to permanently disable critical protocol functionality or expose sensitive data.&#xA;Risk Level High — Can permanently break protocol functionality with no recovery path.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A01 (Broken Access Control) CWE-284 (Improper Access Control), CWE-732 (Incorrect Permission Assignment) The Problem When you expose an object by value (returning it from a function), the caller gains full control including the ability to freeze it. If that object is your protocol’s treasury, configuration, or any mutable state, freezing it permanently disables all mutations.</description>
    </item>
    <item>
      <title>3. Numeric / Bitwise Pitfalls</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/numeric-bitwise-pitfalls/index.html</link>
      <pubDate>Wed, 26 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/numeric-bitwise-pitfalls/index.html</guid>
      <description>Overview Move’s numeric and bitwise operations have specific semantics that differ from other languages. Arithmetic operations abort on overflow/underflow (rather than wrapping), while bitwise shifts beyond the type width silently produce zero. These behaviors can lead to unexpected results and security vulnerabilities.&#xA;Risk Level Medium to High — Can cause denial of service or bypass access control checks.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A06 (Vulnerable Components), A03 (Injection) CWE-681 (Incorrect Conversion), CWE-190 (Integer Overflow) The Problem Overflow/Underflow Behavior Move aborts on overflow rather than wrapping:</description>
    </item>
    <item>
      <title>4. Ability Misconfiguration</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/ability-misconfiguration/index.html</link>
      <pubDate>Wed, 26 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/ability-misconfiguration/index.html</guid>
      <description>Overview Move’s four abilities (copy, drop, store, key) control what operations can be performed on types. Misconfiguring these abilities can allow duplication of assets, unintended destruction of resources, wrapping of objects, or unauthorized global storage access.&#xA;Risk Level Critical — Incorrect abilities can break fundamental economic invariants.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A01 (Broken Access Control) CWE-284 (Improper Access Control), CWE-266 (Incorrect Privilege Assignment) The Problem Ability Overview Ability Allows Danger copy Duplicating values Assets can be infinitely copied drop Implicit destruction Resources can be silently discarded store Storing inside other objects Objects can be wrapped/transferred key Top-level object status Can be stored in global storage Common Mistakes Giving copy to assets — Allows infinite minting Giving drop to valuable items — Allows destroying value without proper handling Giving store to capabilities — Allows unauthorized transfer/wrapping Over-granting abilities — Default to minimal abilities Vulnerable Example module vulnerable::token { use sui::object::{Self, UID}; use sui::tx_context::TxContext; /// CRITICAL VULNERABILITY: Token with `copy` ability /// Anyone can duplicate tokens infinitely! public struct Token has copy, drop, store { value: u64, } /// VULNERABLE: Capability with `store` allows transfer public struct MintCap has key, store { id: UID, max_supply: u64, } public fun mint(cap: &amp;MintCap, amount: u64): Token { Token { value: amount } } /// VULNERABLE: Ticket that can be dropped silently /// User might accidentally lose their ticket public struct EventTicket has key, drop, store { id: UID, event_id: u64, seat: u64, } } Attack: Infinite Token Duplication module attack::duplicate { use vulnerable::token::{Self, Token}; public fun exploit(): (Token, Token, Token) { let original = token::mint(cap, 1000); // Because Token has `copy`, we can duplicate it infinitely! let copy1 = copy original; let copy2 = copy original; let copy3 = copy original; // ... unlimited copies (original, copy1, copy2) } } Attack: Capability Transfer module attack::steal_cap { use vulnerable::token::MintCap; use sui::transfer; public entry fun steal(cap: MintCap) { // MintCap has `store`, so anyone who gets it can transfer it transfer::public_transfer(cap, @attacker); } } Secure Example module secure::token { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; use sui::event; /// SECURE: No `copy` or `drop` — true asset semantics /// Must be explicitly created and explicitly consumed public struct Token has key, store { id: UID, value: u64, } /// SECURE: No `store` — only this module controls transfers public struct MintCap has key { id: UID, max_supply: u64, minted: u64, } /// SECURE: No `drop` — must be explicitly used or refunded public struct EventTicket has key { id: UID, event_id: u64, seat: u64, owner: address, } /// Event for ticket redemption public struct TicketRedeemed has copy, drop { ticket_id: object::ID, event_id: u64, seat: u64, } public fun mint( cap: &amp;mut MintCap, amount: u64, recipient: address, ctx: &amp;mut TxContext ) { assert!(cap.minted + amount &lt;= cap.max_supply, E_EXCEEDS_SUPPLY); cap.minted = cap.minted + amount; transfer::transfer( Token { id: object::new(ctx), value: amount }, recipient ); } /// Tokens must be explicitly merged or split public fun merge(token1: Token, token2: Token, ctx: &amp;mut TxContext): Token { let Token { id: id1, value: v1 } = token1; let Token { id: id2, value: v2 } = token2; object::delete(id1); object::delete(id2); Token { id: object::new(ctx), value: v1 + v2 } } /// Tickets must be explicitly redeemed — cannot be dropped public entry fun redeem_ticket(ticket: EventTicket, ctx: &amp;TxContext) { let EventTicket { id, event_id, seat, owner } = ticket; // Verify caller is the ticket owner assert!(tx_context::sender(ctx) == owner, E_NOT_OWNER); event::emit(TicketRedeemed { ticket_id: object::uid_to_inner(&amp;id), event_id, seat, }); object::delete(id); } /// Explicit refund path for tickets public entry fun refund_ticket( ticket: EventTicket, ctx: &amp;mut TxContext ) { let EventTicket { id, event_id: _, seat: _, owner } = ticket; assert!(tx_context::sender(ctx) == owner, E_NOT_OWNER); object::delete(id); // ... refund logic } } Ability Selection Guidelines For Assets (Tokens, NFTs, Items) /// Minimal abilities for fungible-like assets public struct Asset has key, store { id: UID, value: u64, } /// For assets that should never leave the protocol public struct LockedAsset has key { id: UID, value: u64, } For Capabilities /// Capability that can only be transferred by your module public struct AdminCap has key { id: UID, } /// Witness pattern — one-time use, no abilities needed public struct WITNESS has drop {} For Receipts/Proofs /// Hot potato — must be consumed in same transaction public struct Receipt { amount: u64, } /// Cannot be stored, copied, or dropped — forces handling For Events /// Events should always have copy + drop public struct TransferEvent has copy, drop { from: address, to: address, amount: u64, } Recommended Mitigations 1. Start with Minimal Abilities /// Start with nothing, add only what&#39;s needed public struct MyType { } /// Then add based on requirements: public struct MyType has key { } // If it needs to be an object public struct MyType has key, store { } // If it needs transfer 2. Document Why Each Ability is Needed /// `key`: Required for object storage /// `store`: Required for marketplace listing (intentional risk accepted) /// NO `copy`: Asset must not be duplicable /// NO `drop`: Asset must be explicitly consumed public struct NFT has key, store { id: UID, // ... } 3. Use Wrapper Types for Different Contexts /// Internal representation — minimal abilities public struct TokenInner { value: u64, } /// Transferable version public struct TransferableToken has key, store { id: UID, inner: TokenInner, } /// Locked version — no `store` public struct LockedToken has key { id: UID, inner: TokenInner, unlock_time: u64, } Testing Checklist Verify no asset types have copy ability Verify valuable resources don’t have drop unless explicitly intended Verify capabilities lack store unless transfer is explicitly required Test that types without drop must be explicitly consumed Audit all has declarations against security requirements Related Vulnerabilities Object Transfer Misuse Object Freezing Misuse Capability Leakage</description>
    </item>
    <item>
      <title>5. Access-Control Mistakes</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/access-control-mistakes/index.html</link>
      <pubDate>Wed, 26 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/access-control-mistakes/index.html</guid>
      <description>Overview Access control mistakes occur when authorization checks are missing, incorrectly implemented, or rely on wrong assumptions about TxContext::sender(). These vulnerabilities allow unauthorized users to perform privileged operations.&#xA;Risk Level Critical — Direct path to unauthorized access and fund theft.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A01 (Broken Access Control) CWE-285 (Improper Authorization), CWE-639 (Authorization Bypass) The Problem Common Access Control Mistakes Missing authorization checks — Functions accessible to anyone Checking wrong sender — Confusing gas sponsor with transaction sender Hardcoded addresses — Addresses that cannot be updated or rotated Race conditions — Authorization state changes between check and use Inconsistent models — Mixing capability-based and address-based checks Vulnerable Example module vulnerable::vault { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::coin::{Self, Coin}; use sui::sui::SUI; use sui::transfer; const ADMIN: address = @0xDEADBEEF; public struct Vault has key { id: UID, funds: Coin&lt;SUI&gt;, admin: address, } public struct AdminCap has key, store { id: UID, } /// VULNERABLE: No access control at all! public entry fun withdraw_all( vault: &amp;mut Vault, ctx: &amp;mut TxContext ) { let amount = coin::value(&amp;vault.funds); let withdrawn = coin::split(&amp;mut vault.funds, amount, ctx); transfer::public_transfer(withdrawn, tx_context::sender(ctx)); } /// VULNERABLE: Hardcoded address cannot be updated public entry fun emergency_withdraw( vault: &amp;mut Vault, ctx: &amp;mut TxContext ) { // What if ADMIN key is compromised? No way to rotate! assert!(tx_context::sender(ctx) == ADMIN, E_NOT_ADMIN); // ... withdraw logic } /// VULNERABLE: Checks sender but ignores capability public entry fun update_admin( vault: &amp;mut Vault, _cap: &amp;AdminCap, // Cap is ignored! new_admin: address, ctx: &amp;mut TxContext ) { // This checks sender even though cap is passed // If cap was transferred, wrong person might have access assert!(tx_context::sender(ctx) == vault.admin, E_NOT_ADMIN); vault.admin = new_admin; } /// VULNERABLE: Time-of-check to time-of-use issue public entry fun conditional_withdraw( vault: &amp;mut Vault, amount: u64, ctx: &amp;mut TxContext ) { let sender = tx_context::sender(ctx); // Check is performed... assert!(is_authorized(sender), E_NOT_AUTHORIZED); // ...but in a PTB, authorization might change before this executes let withdrawn = coin::split(&amp;mut vault.funds, amount, ctx); transfer::public_transfer(withdrawn, sender); } } Secure Example module secure::vault { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::coin::{Self, Coin}; use sui::sui::SUI; use sui::transfer; use sui::event; const E_NOT_ADMIN: u64 = 0; const E_ZERO_AMOUNT: u64 = 1; const E_INSUFFICIENT_FUNDS: u64 = 2; /// SECURE: No `store` — only this module controls the cap public struct AdminCap has key { id: UID, vault_id: ID, // Tied to specific vault } public struct Vault has key { id: UID, funds: Coin&lt;SUI&gt;, } public struct WithdrawEvent has copy, drop { vault_id: ID, amount: u64, recipient: address, } fun init(ctx: &amp;mut TxContext) { let vault = Vault { id: object::new(ctx), funds: coin::zero(ctx), }; let vault_id = object::id(&amp;vault); // Create admin cap tied to this vault let admin_cap = AdminCap { id: object::new(ctx), vault_id, }; transfer::share_object(vault); transfer::transfer(admin_cap, tx_context::sender(ctx)); } /// SECURE: Capability-based access control public entry fun withdraw( cap: &amp;AdminCap, vault: &amp;mut Vault, amount: u64, recipient: address, ctx: &amp;mut TxContext ) { // Verify cap is for this vault assert!(cap.vault_id == object::id(vault), E_NOT_ADMIN); assert!(amount &gt; 0, E_ZERO_AMOUNT); assert!(coin::value(&amp;vault.funds) &gt;= amount, E_INSUFFICIENT_FUNDS); let withdrawn = coin::split(&amp;mut vault.funds, amount, ctx); event::emit(WithdrawEvent { vault_id: object::id(vault), amount, recipient, }); transfer::public_transfer(withdrawn, recipient); } /// SECURE: Explicit admin transfer with cap consumption public entry fun transfer_admin( cap: AdminCap, new_admin: address, ctx: &amp;mut TxContext ) { // Old cap is consumed, new one is created let AdminCap { id, vault_id } = cap; object::delete(id); transfer::transfer( AdminCap { id: object::new(ctx), vault_id, }, new_admin ); } /// SECURE: Multi-sig pattern for critical operations public struct MultiSigProposal has key { id: UID, action: vector&lt;u8&gt;, approvals: vector&lt;address&gt;, threshold: u64, vault_id: ID, } public entry fun approve_and_execute( proposal: &amp;mut MultiSigProposal, cap: &amp;AdminCap, ctx: &amp;TxContext ) { let sender = tx_context::sender(ctx); // Add approval if not already present if (!vector::contains(&amp;proposal.approvals, &amp;sender)) { vector::push_back(&amp;mut proposal.approvals, sender); }; // Execute if threshold reached if (vector::length(&amp;proposal.approvals) &gt;= proposal.threshold) { // ... execute action } } } Access Control Patterns Pattern 1: Pure Capability-Based /// Best for most cases — clear, composable public entry fun admin_action(cap: &amp;AdminCap, ...) { // Whoever holds the cap can perform the action // No sender checks needed } Pattern 2: Capability + Sender Verification /// For soul-bound capabilities public struct SoulBoundCap has key { id: UID, owner: address, } public entry fun action(cap: &amp;SoulBoundCap, ctx: &amp;TxContext) { assert!(tx_context::sender(ctx) == cap.owner, E_NOT_OWNER); // Both cap possession AND sender match required } Pattern 3: Role-Based Access public struct RoleRegistry has key { id: UID, admins: vector&lt;address&gt;, operators: vector&lt;address&gt;, } public fun is_admin(registry: &amp;RoleRegistry, addr: address): bool { vector::contains(&amp;registry.admins, &amp;addr) } public entry fun admin_action( registry: &amp;RoleRegistry, ctx: &amp;TxContext ) { assert!(is_admin(registry, tx_context::sender(ctx)), E_NOT_ADMIN); } Pattern 4: Time-Locked Operations public struct TimeLock has key { id: UID, operation: vector&lt;u8&gt;, execute_after: u64, } public entry fun execute_timelock( lock: TimeLock, clock: &amp;Clock, ) { assert!(clock::timestamp_ms(clock) &gt;= lock.execute_after, E_TOO_EARLY); let TimeLock { id, operation, execute_after: _ } = lock; object::delete(id); // ... execute operation } Recommended Mitigations 1. Choose One Authorization Model // GOOD: Consistent capability-based public entry fun action1(cap: &amp;AdminCap, ...) { } public entry fun action2(cap: &amp;AdminCap, ...) { } public entry fun action3(cap: &amp;AdminCap, ...) { } // BAD: Mixed models public entry fun action1(cap: &amp;AdminCap, ...) { } public entry fun action2(ctx: &amp;TxContext) { // sender check assert!(sender(ctx) == ADMIN, 0); } 2. Tie Capabilities to Resources public struct VaultCap has key { id: UID, vault_id: ID, // Can only control this specific vault } 3. Implement Emergency Procedures public struct EmergencyConfig has key { id: UID, guardians: vector&lt;address&gt;, pause_threshold: u64, } public entry fun emergency_pause( config: &amp;EmergencyConfig, signatures: vector&lt;vector&lt;u8&gt;&gt;, // ... verify multi-sig ) { } Testing Checklist Every state-modifying function has explicit access control No hardcoded addresses without upgrade path Capabilities are tied to specific resources where appropriate No mixing of authorization models Emergency procedures exist for key rotation All access control paths are tested with unauthorized callers Related Vulnerabilities Object Transfer Misuse Sponsored Transaction Pitfalls Capability Leakage</description>
    </item>
    <item>
      <title>6. Shared Object DoS</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/shared-object-dos/index.html</link>
      <pubDate>Wed, 26 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/shared-object-dos/index.html</guid>
      <description>Overview Shared objects in Sui can be mutated by any transaction, leading to contention when many actors try to modify the same object simultaneously. This can cause performance degradation or complete denial of service (DoS) for protocols that rely heavily on shared state.&#xA;Risk Level High — Can make protocols unusable during high-demand periods.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A05 (Security Misconfiguration), A06 (Vulnerable Components) CWE-400 (Uncontrolled Resource Consumption), CWE-834 (Excessive Iteration) The Problem Sui processes transactions that touch the same shared object sequentially to maintain consistency. When many transactions contend for the same shared object:</description>
    </item>
    <item>
      <title>7. Improper Object Sharing</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/improper-object-sharing/index.html</link>
      <pubDate>Wed, 26 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/improper-object-sharing/index.html</guid>
      <description>Overview Accidentally exposing objects as shared via transfer::share_object enables global mutation by anyone. Once an object is shared, it cannot be unshared — this is a permanent, irreversible change to the object’s ownership model.&#xA;Risk Level High — Shared objects are accessible to all, potentially exposing sensitive operations.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A01 (Broken Access Control) CWE-284 (Improper Access Control), CWE-277 (Insecure Inherited Permissions) The Problem Ownership Models in Sui Type Created By Who Can Use Can Be Changed Address-owned transfer::transfer Only owner Yes (transfer) Shared transfer::share_object Anyone No (permanent) Immutable transfer::freeze_object Anyone (read) No (permanent) Why Sharing is Dangerous Global access — Any transaction can reference the shared object No revocation — Cannot convert back to address-owned Mutation exposure — All &amp;mut entry functions become callable by anyone Contention — Performance issues from concurrent access Vulnerable Example module vulnerable::wallet { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; use sui::coin::{Self, Coin}; use sui::sui::SUI; public struct Wallet has key { id: UID, funds: Coin&lt;SUI&gt;, owner: address, } /// VULNERABLE: Wallet is shared instead of transferred! fun init(ctx: &amp;mut TxContext) { let wallet = Wallet { id: object::new(ctx), funds: coin::zero(ctx), owner: tx_context::sender(ctx), }; // WRONG: This should be transfer(), not share_object()! transfer::share_object(wallet); } /// Because wallet is shared, ANYONE can call this! public entry fun withdraw( wallet: &amp;mut Wallet, amount: u64, ctx: &amp;mut TxContext ) { // This check is useless — attacker just passes their address let recipient = tx_context::sender(ctx); // Wait, the owner check is missing entirely! let withdrawn = coin::split(&amp;mut wallet.funds, amount, ctx); transfer::public_transfer(withdrawn, recipient); } /// VULNERABLE: Even with owner check, sharing was wrong public entry fun withdraw_checked( wallet: &amp;mut Wallet, amount: u64, ctx: &amp;mut TxContext ) { // Owner check exists but... assert!(tx_context::sender(ctx) == wallet.owner, E_NOT_OWNER); // If owner&#39;s key is compromised, wallet is drained // With address-owned, owner could at least try to transfer first let withdrawn = coin::split(&amp;mut wallet.funds, amount, ctx); transfer::public_transfer(withdrawn, wallet.owner); } } Attack Scenario // Attacker finds the shared wallet object module attack::drain_wallet { use vulnerable::wallet; use sui::tx_context::TxContext; public entry fun steal( wallet: &amp;mut wallet::Wallet, ctx: &amp;mut TxContext ) { // Because wallet is shared, attacker can reference it directly // If withdraw() lacks owner check, funds are gone wallet::withdraw(wallet, 1000000, ctx); } } Secure Example module secure::wallet { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; use sui::coin::{Self, Coin}; use sui::sui::SUI; /// Wallet is address-owned — only owner can use it public struct Wallet has key { id: UID, funds: Coin&lt;SUI&gt;, } /// SECURE: Transfer to user, not share fun init(ctx: &amp;mut TxContext) { transfer::transfer( Wallet { id: object::new(ctx), funds: coin::zero(ctx), }, tx_context::sender(ctx) ); } /// Owner must possess the wallet to call this public entry fun withdraw( wallet: &amp;mut Wallet, amount: u64, recipient: address, ctx: &amp;mut TxContext ) { let withdrawn = coin::split(&amp;mut wallet.funds, amount, ctx); transfer::public_transfer(withdrawn, recipient); } /// Only owner can transfer their wallet public entry fun transfer_wallet( wallet: Wallet, new_owner: address, ) { transfer::transfer(wallet, new_owner); } } When Sharing IS Appropriate module appropriate_sharing::examples { use sui::object::{Self, UID}; use sui::tx_context::TxContext; use sui::transfer; /// APPROPRIATE: Global configuration that needs to be readable by all public struct GlobalConfig has key { id: UID, fee_bps: u64, paused: bool, } /// APPROPRIATE: Order book that multiple parties interact with public struct OrderBook has key { id: UID, bids: vector&lt;Order&gt;, asks: vector&lt;Order&gt;, } /// APPROPRIATE: Liquidity pool for AMM public struct LiquidityPool has key { id: UID, reserve_a: Coin&lt;A&gt;, reserve_b: Coin&lt;B&gt;, } /// For shared objects, use capability-based access control public struct AdminCap has key { id: UID, config_id: ID, } public entry fun update_config( cap: &amp;AdminCap, config: &amp;mut GlobalConfig, new_fee: u64, ) { assert!(cap.config_id == object::id(config), E_WRONG_CONFIG); config.fee_bps = new_fee; } } Sharing Decision Checklist Ask these questions before using share_object:</description>
    </item>
    <item>
      <title>8. Dynamic Field Misuse</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/dynamic-field-misuse/index.html</link>
      <pubDate>Wed, 26 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/dynamic-field-misuse/index.html</guid>
      <description>Overview Dynamic fields and child objects in Sui allow flexible, runtime-determined storage attached to objects. Incorrect usage leads to unbounded growth, key collisions, invariant violations, and data corruption.&#xA;Risk Level High — Can cause state corruption, gas exhaustion, or security bypasses.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A01 (Broken Access Control), A05 (Security Misconfiguration) CWE-710 (Improper Adherence to Coding Standards), CWE-915 (Improperly Controlled Modification of Dynamically-Determined Object Attributes) The Problem Dynamic Fields vs Regular Fields Aspect Regular Fields Dynamic Fields Defined at Compile time Runtime Type safety Full Partial (key type determines value type) Enumeration Yes No (must know keys) Growth Fixed Unbounded Common Mistakes Unbounded growth — No limit on dynamic field additions Key collisions — User-controlled keys overwrite existing data Type confusion — Same key used for different value types Orphaned data — Parent deleted without removing dynamic fields Access control bypass — Dynamic fields circumvent intended restrictions Vulnerable Example module vulnerable::inventory { use sui::object::{Self, UID}; use sui::tx_context::TxContext; use sui::dynamic_field as df; public struct Inventory has key { id: UID, owner: address, } public struct Item has store { name: vector&lt;u8&gt;, value: u64, } /// VULNERABLE: User-controlled key allows overwriting public entry fun add_item( inventory: &amp;mut Inventory, item_name: vector&lt;u8&gt;, // User-controlled key! value: u64, ctx: &amp;mut TxContext ) { // Attacker can overwrite existing items! df::add(&amp;mut inventory.id, item_name, Item { name: item_name, value }); } /// VULNERABLE: No ownership check for item modification public entry fun update_item_value( inventory: &amp;mut Inventory, item_name: vector&lt;u8&gt;, new_value: u64, ) { // Anyone can modify any item if they know the key let item: &amp;mut Item = df::borrow_mut(&amp;mut inventory.id, item_name); item.value = new_value; } /// VULNERABLE: No limit on items — gas exhaustion attack public entry fun bulk_add( inventory: &amp;mut Inventory, count: u64, ctx: &amp;mut TxContext ) { let i = 0; while (i &lt; count) { let key = i; // Sequential keys df::add(&amp;mut inventory.id, key, Item { name: b&#34;spam&#34;, value: 0 }); i = i + 1; } // Inventory now has unbounded number of items } } module vulnerable::storage { use sui::dynamic_field as df; use sui::dynamic_object_field as dof; public struct Container has key { id: UID, } /// VULNERABLE: Same key used for different types public entry fun store_string( container: &amp;mut Container, key: vector&lt;u8&gt;, value: vector&lt;u8&gt;, ) { df::add(&amp;mut container.id, key, value); } public entry fun store_number( container: &amp;mut Container, key: vector&lt;u8&gt;, value: u64, ) { // If key already exists with string value, this will fail // OR worse — type confusion if not properly checked df::add(&amp;mut container.id, key, value); } } Attack Scenarios Key Collision Attack:</description>
    </item>
    <item>
      <title>9. Sponsored Transaction Pitfalls</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/sponsored-transaction-pitfalls/index.html</link>
      <pubDate>Wed, 26 Nov 2025 20:07:13 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/sponsored-transaction-pitfalls/index.html</guid>
      <description>Overview Sui supports sponsored transactions where one account pays gas fees for another account’s transaction. Confusing the gas sponsor with the transaction sender can lead to impersonation attacks, unauthorized actions, and broken access control.&#xA;Risk Level High — Can lead to complete access control bypass and impersonation.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A01 (Broken Access Control) CWE-285 (Improper Authorization), CWE-863 (Incorrect Authorization) The Problem Transaction Participants Role Function Example Sender Signs transaction, authorizes actions User performing action Sponsor Pays gas fees Relayer, dApp backend Validator Executes transaction Network node The Confusion Developers sometimes assume:</description>
    </item>
    <item>
      <title>10. General Move Logic Errors</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/general-move-logic-errors/index.html</link>
      <pubDate>Wed, 26 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/general-move-logic-errors/index.html</guid>
      <description>Overview General logic errors in Move contracts include PTB (Programmable Transaction Block) reordering effects, incorrect mutation order, fee miscalculations, and state inconsistencies. These bugs are often subtle and can lead to fund loss or protocol manipulation.&#xA;Risk Level Medium to Critical — Varies based on the specific logic error.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A01 (Broken Access Control), A04 (Insecure Design) CWE-841 (Improper Enforcement of Behavioral Workflow), CWE-362 (Race Condition) The Problem Categories of Logic Errors State mutation order — Operations performed in wrong sequence PTB assumptions — Expecting specific call ordering in transactions Arithmetic errors — Rounding, precision loss, fee calculations Invariant violations — Protocol rules not enforced consistently Edge cases — Zero values, empty collections, boundary conditions Vulnerable Examples Example 1: Incorrect Mutation Order module vulnerable::lending { use sui::object::UID; use sui::coin::{Self, Coin}; use sui::sui::SUI; public struct LendingPool has key { id: UID, total_deposits: u64, total_borrows: u64, interest_rate: u64, } /// VULNERABLE: Interest calculated on old balance public entry fun deposit( pool: &amp;mut LendingPool, payment: Coin&lt;SUI&gt;, ) { let amount = coin::value(&amp;payment); // WRONG: Accruing interest AFTER updating deposits // New deposit earns interest it shouldn&#39;t pool.total_deposits = pool.total_deposits + amount; // Interest calculated on inflated total accrue_interest(pool); // ... store payment } /// VULNERABLE: Withdraw before interest accrual public entry fun withdraw( pool: &amp;mut LendingPool, amount: u64, ctx: &amp;mut TxContext ) { // WRONG: Withdrawing before accruing interest // User avoids paying accumulated interest pool.total_deposits = pool.total_deposits - amount; accrue_interest(pool); // Too late! } } Example 2: Fee Calculation Errors module vulnerable::exchange { const FEE_BPS: u64 = 30; // 0.3% const BPS_DENOMINATOR: u64 = 10000; public struct Exchange has key { id: UID, accumulated_fees: u64, } /// VULNERABLE: Precision loss in fee calculation public fun calculate_fee(amount: u64): u64 { // For small amounts, this can round to 0 // amount = 100, fee = 100 * 30 / 10000 = 0 amount * FEE_BPS / BPS_DENOMINATOR } /// VULNERABLE: Fee-on-fee calculation public fun swap_with_fee( exchange: &amp;mut Exchange, input_amount: u64, ): u64 { let fee = calculate_fee(input_amount); let net_input = input_amount - fee; // Calculate output let output = calculate_output(net_input); // WRONG: Taking fee again on output! let output_fee = calculate_fee(output); let net_output = output - output_fee; // User pays fee twice exchange.accumulated_fees = exchange.accumulated_fees + fee + output_fee; net_output } /// VULNERABLE: Integer division ordering public fun calculate_share(amount: u64, user_balance: u64, total_balance: u64): u64 { // WRONG: Division before multiplication loses precision // If user_balance &lt; total_balance, this might return 0 amount * (user_balance / total_balance) // Should be: (amount * user_balance) / total_balance } } Example 3: State Invariant Violations module vulnerable::amm { public struct Pool has key { id: UID, reserve_a: u64, reserve_b: u64, k: u64, // Constant product: reserve_a * reserve_b = k } /// VULNERABLE: k not updated after liquidity change public entry fun add_liquidity( pool: &amp;mut Pool, amount_a: u64, amount_b: u64, ) { pool.reserve_a = pool.reserve_a + amount_a; pool.reserve_b = pool.reserve_b + amount_b; // FORGOT to update k! // pool.k = pool.reserve_a * pool.reserve_b; // Now k invariant is broken // Swaps will use stale k value } /// VULNERABLE: No check that k is maintained after swap public entry fun swap_a_for_b( pool: &amp;mut Pool, amount_a_in: u64, ): u64 { let new_reserve_a = pool.reserve_a + amount_a_in; // Calculate output to maintain k // But rounding might break the invariant let amount_b_out = pool.reserve_b - (pool.k / new_reserve_a); pool.reserve_a = new_reserve_a; pool.reserve_b = pool.reserve_b - amount_b_out; // No assertion that k is still valid! // assert!(pool.reserve_a * pool.reserve_b &gt;= pool.k, E_K_VIOLATED); amount_b_out } } Secure Examples Secure Lending Pool module secure::lending { use sui::object::UID; use sui::coin::{Self, Coin}; use sui::sui::SUI; use sui::clock::{Self, Clock}; public struct LendingPool has key { id: UID, total_deposits: u64, total_borrows: u64, interest_rate_per_ms: u64, last_update_ms: u64, accumulated_interest: u64, } /// SECURE: Always accrue interest first fun accrue_interest_internal(pool: &amp;mut LendingPool, clock: &amp;Clock) { let now = clock::timestamp_ms(clock); let elapsed = now - pool.last_update_ms; if (elapsed &gt; 0) { let interest = (pool.total_borrows as u128) * (pool.interest_rate_per_ms as u128) * (elapsed as u128) / 1_000_000_000_000; pool.accumulated_interest = pool.accumulated_interest + (interest as u64); pool.last_update_ms = now; } } /// SECURE: Interest accrued before state change public entry fun deposit( pool: &amp;mut LendingPool, payment: Coin&lt;SUI&gt;, clock: &amp;Clock, ) { // FIRST: Accrue interest on existing state accrue_interest_internal(pool, clock); // THEN: Update deposits let amount = coin::value(&amp;payment); pool.total_deposits = pool.total_deposits + amount; // ... store payment } /// SECURE: Consistent ordering public entry fun withdraw( pool: &amp;mut LendingPool, amount: u64, clock: &amp;Clock, ctx: &amp;mut TxContext ) { // FIRST: Accrue interest accrue_interest_internal(pool, clock); // THEN: Process withdrawal assert!(pool.total_deposits &gt;= amount, E_INSUFFICIENT_LIQUIDITY); pool.total_deposits = pool.total_deposits - amount; } } Secure Fee Calculations module secure::exchange { const FEE_BPS: u64 = 30; const BPS_DENOMINATOR: u64 = 10000; const MIN_FEE: u64 = 1; // Minimum fee to prevent zero-fee exploits /// SECURE: Handle precision loss public fun calculate_fee(amount: u64): u64 { let fee = (amount * FEE_BPS) / BPS_DENOMINATOR; // Ensure minimum fee on non-zero amounts if (amount &gt; 0 &amp;&amp; fee == 0) { MIN_FEE } else { fee } } /// SECURE: Use u128 for intermediate calculations public fun calculate_share( amount: u64, user_balance: u64, total_balance: u64 ): u64 { if (total_balance == 0) { return 0 }; // Use u128 to prevent overflow and maintain precision let result = ((amount as u128) * (user_balance as u128)) / (total_balance as u128); (result as u64) } /// SECURE: Single fee, clear calculation public fun swap_with_fee( exchange: &amp;mut Exchange, input_amount: u64, ): u64 { assert!(input_amount &gt; 0, E_ZERO_INPUT); let fee = calculate_fee(input_amount); let net_input = input_amount - fee; // Calculate output — no additional fee let output = calculate_output(net_input); exchange.accumulated_fees = exchange.accumulated_fees + fee; output } } Secure AMM with Invariant Checks module secure::amm { const E_K_VIOLATED: u64 = 1; const E_ZERO_LIQUIDITY: u64 = 2; const E_SLIPPAGE: u64 = 3; public struct Pool has key { id: UID, reserve_a: u64, reserve_b: u64, } /// Calculate k from current reserves fun get_k(pool: &amp;Pool): u128 { (pool.reserve_a as u128) * (pool.reserve_b as u128) } /// SECURE: Update reserves and verify invariant public entry fun add_liquidity( pool: &amp;mut Pool, amount_a: u64, amount_b: u64, ) { assert!(amount_a &gt; 0 &amp;&amp; amount_b &gt; 0, E_ZERO_LIQUIDITY); // For existing pool, require proportional deposit if (pool.reserve_a &gt; 0) { let expected_b = ((amount_a as u128) * (pool.reserve_b as u128)) / (pool.reserve_a as u128); // Allow small deviation for rounding assert!( amount_b &gt;= (expected_b as u64) - 1 &amp;&amp; amount_b &lt;= (expected_b as u64) + 1, E_SLIPPAGE ); }; pool.reserve_a = pool.reserve_a + amount_a; pool.reserve_b = pool.reserve_b + amount_b; } /// SECURE: Verify k maintained after swap public entry fun swap_a_for_b( pool: &amp;mut Pool, amount_a_in: u64, min_b_out: u64, ): u64 { let k_before = get_k(pool); let new_reserve_a = pool.reserve_a + amount_a_in; // Calculate output using u128 for precision let new_reserve_b = (k_before / (new_reserve_a as u128)) as u64; let amount_b_out = pool.reserve_b - new_reserve_b; // Slippage check assert!(amount_b_out &gt;= min_b_out, E_SLIPPAGE); // Update reserves pool.reserve_a = new_reserve_a; pool.reserve_b = new_reserve_b; // CRITICAL: Verify k invariant (with tolerance for rounding) let k_after = get_k(pool); assert!(k_after &gt;= k_before, E_K_VIOLATED); amount_b_out } } Logic Error Prevention Patterns Pattern 1: Check-Effects-Interactions public entry fun secure_operation(state: &amp;mut State, input: u64) { // 1. CHECKS - Validate all preconditions assert!(input &gt; 0, E_ZERO_INPUT); assert!(state.balance &gt;= input, E_INSUFFICIENT); // 2. EFFECTS - Update state state.balance = state.balance - input; state.processed = state.processed + 1; // 3. INTERACTIONS - External calls last emit_event(...); } Pattern 2: Invariant Assertions /// Always assert invariants at function end public entry fun modify_state(state: &amp;mut State, ...) { // ... make changes // Assert invariants before returning assert_invariants(state); } fun assert_invariants(state: &amp;State) { assert!(state.total == state.a + state.b + state.c, E_TOTAL_MISMATCH); assert!(state.balance &gt;= state.minimum_required, E_UNDERCOLLATERALIZED); } Pattern 3: Use Receipts for Multi-Step Operations /// Hot potato pattern for operations that must complete public struct OperationReceipt { expected_outcome: u64, } public fun start_operation(state: &amp;mut State, amount: u64): OperationReceipt { state.locked = state.locked + amount; OperationReceipt { expected_outcome: calculate_expected(amount) } } public fun finish_operation(state: &amp;mut State, receipt: OperationReceipt, actual: u64) { let OperationReceipt { expected_outcome } = receipt; assert!(actual &gt;= expected_outcome, E_UNEXPECTED_OUTCOME); // Receipt consumed — operation must complete } Testing Checklist Test all functions with zero values Test with maximum (u64::MAX) values Verify interest/fee calculations across time boundaries Test invariants hold after every state-changing operation Test PTB with reordered calls Verify precision in all arithmetic with small and large values Related Vulnerabilities PTB Ordering Issues Numeric / Bitwise Pitfalls Event State Inconsistency</description>
    </item>
    <item>
      <title>11. Capability Leakage</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/capability-leakage/index.html</link>
      <pubDate>Wed, 26 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/capability-leakage/index.html</guid>
      <description>Overview Capability leakage occurs when authority-granting objects (capabilities) are unintentionally exposed through return values, public functions, or parent struct access. Once a capability leaks, unauthorized parties can perform privileged operations.&#xA;Risk Level Critical — Direct path to unauthorized privileged access.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A01 (Broken Access Control) CWE-284 (Improper Access Control), CWE-668 (Exposure of Resource to Wrong Sphere) The Problem How Capabilities Leak Returning capabilities by value — Functions that return capability objects Parent struct exposure — Returning structs containing capability children Public field access — Capabilities stored in accessible fields Dynamic field exposure — Capabilities stored as retrievable dynamic fields Vulnerable Example module vulnerable::protocol { use sui::object::{Self, UID}; use sui::tx_context::TxContext; use sui::dynamic_field as df; public struct AdminCap has key, store { id: UID, } public struct ProtocolState has key { id: UID, admin_cap: AdminCap, // Capability embedded in state! } public struct CapWrapper has key, store { id: UID, cap: AdminCap, } /// VULNERABLE: Returns parent containing capability public fun get_state(state: &amp;mut ProtocolState): ProtocolState { // Caller now has access to admin_cap! *state } /// VULNERABLE: Exposes capability through wrapper public fun get_wrapper(state: &amp;ProtocolState): &amp;CapWrapper { // If wrapper is extractable, cap is leaked &amp;state.wrapper } /// VULNERABLE: Creates accessor that leaks authority public fun borrow_admin_cap(state: &amp;ProtocolState): &amp;AdminCap { // Even a reference can be used to call admin functions! &amp;state.admin_cap } /// VULNERABLE: Dynamic field stores capability public fun store_cap_in_field( parent: &amp;mut UID, cap: AdminCap, ) { df::add(parent, b&#34;admin&#34;, cap); } /// Anyone who knows the key can retrieve it public fun get_cap_from_field(parent: &amp;mut UID): &amp;AdminCap { df::borrow(parent, b&#34;admin&#34;) } } module vulnerable::treasury { use vulnerable::protocol::AdminCap; /// VULNERABLE: Accepts capability reference /// Anyone who leaked the reference can call this public entry fun drain_treasury( _cap: &amp;AdminCap, treasury: &amp;mut Treasury, ctx: &amp;mut TxContext ) { // No additional checks — trusting the capability let all_funds = treasury.balance; treasury.balance = 0; // ... transfer funds } } Attack Scenario module attack::exploit { use vulnerable::protocol; public entry fun steal_admin( state: &amp;vulnerable::protocol::ProtocolState, treasury: &amp;mut Treasury, ctx: &amp;mut TxContext ) { // Leak the capability reference let cap_ref = protocol::borrow_admin_cap(state); // Use leaked capability to drain treasury vulnerable::treasury::drain_treasury(cap_ref, treasury, ctx); } } Secure Example module secure::protocol { use sui::object::{Self, UID, ID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; /// Capability with no `store` — cannot be wrapped or transferred public struct AdminCap has key { id: UID, protocol_id: ID, authorized_address: address, } /// State does NOT contain capability public struct ProtocolState has key { id: UID, admin_cap_id: ID, // Only stores the ID, not the cap itself treasury_balance: u64, } fun init(ctx: &amp;mut TxContext) { let state = ProtocolState { id: object::new(ctx), admin_cap_id: object::id_from_address(@0x0), // Placeholder treasury_balance: 0, }; let state_id = object::id(&amp;state); let cap = AdminCap { id: object::new(ctx), protocol_id: state_id, authorized_address: tx_context::sender(ctx), }; // Update state with cap ID state.admin_cap_id = object::id(&amp;cap); transfer::share_object(state); transfer::transfer(cap, tx_context::sender(ctx)); } /// SECURE: No capability return — action performed internally public entry fun admin_withdraw( cap: &amp;AdminCap, state: &amp;mut ProtocolState, amount: u64, ctx: &amp;mut TxContext ) { // Verify cap matches this protocol assert!(cap.protocol_id == object::id(state), E_WRONG_PROTOCOL); // Verify caller is authorized holder assert!(tx_context::sender(ctx) == cap.authorized_address, E_NOT_AUTHORIZED); // Perform action directly — no capability exposure assert!(state.treasury_balance &gt;= amount, E_INSUFFICIENT); state.treasury_balance = state.treasury_balance - amount; // ... transfer funds } /// SECURE: View function returns data, not capability public fun get_admin_cap_id(state: &amp;ProtocolState): ID { state.admin_cap_id } /// SECURE: Check authorization without exposing capability public fun is_admin(cap: &amp;AdminCap, state: &amp;ProtocolState): bool { cap.protocol_id == object::id(state) } } Capability Protection Patterns Pattern 1: Action Functions Instead of Capability Exposure /// BAD: Exposes capability public fun get_admin_cap(state: &amp;State): &amp;AdminCap { &amp;state.admin_cap } /// GOOD: Performs action with internal capability public entry fun perform_admin_action( cap: &amp;AdminCap, state: &amp;mut State, action_params: ActionParams, ) { verify_cap(cap, state); // Perform action internally } Pattern 2: Separate Capability Storage /// Capabilities stored separately, not in protocol state public struct CapabilityRegistry has key { id: UID, // Only IDs, not actual capabilities admin_cap_ids: vector&lt;ID&gt;, } /// Capabilities owned by users, not stored centrally public struct AdminCap has key { id: UID, registry_id: ID, } Pattern 3: Witness Pattern for One-Time Auth /// Witness can only be created once (in init) public struct PROTOCOL has drop {} /// Auth checked via witness possession public fun authorized_action&lt;T: drop&gt;( _witness: T, state: &amp;mut State, ) { // Only code with the witness type can call } Pattern 4: Hot Potato for Scoped Authority /// Hot potato — must be consumed in same transaction public struct AdminSession { state_id: ID, action_count: u64, max_actions: u64, } public fun start_admin_session( cap: &amp;AdminCap, state: &amp;State, ): AdminSession { verify_cap(cap, state); AdminSession { state_id: object::id(state), action_count: 0, max_actions: 10, } } public fun admin_action( session: &amp;mut AdminSession, state: &amp;mut State, ) { assert!(session.state_id == object::id(state), E_WRONG_STATE); assert!(session.action_count &lt; session.max_actions, E_MAX_ACTIONS); session.action_count = session.action_count + 1; // Perform action } public fun end_admin_session(session: AdminSession) { let AdminSession { state_id: _, action_count: _, max_actions: _ } = session; // Session consumed } Recommended Mitigations 1. Never Return Capabilities // BAD public fun get_cap(): AdminCap { ... } public fun borrow_cap(): &amp;AdminCap { ... } // GOOD public entry fun use_cap_for_action(cap: &amp;AdminCap, ...) { ... } 2. Remove store from Capabilities /// Without `store`, cap cannot be wrapped or dynamically stored public struct AdminCap has key { id: UID, } 3. Tie Capabilities to Specific Resources public struct VaultAdminCap has key { id: UID, vault_id: ID, // Only valid for this specific vault } 4. Use Capability References, Not Values /// Functions should borrow capabilities, not consume them public entry fun action(cap: &amp;AdminCap, ...) { } // Borrow /// Only transfer functions should consume public entry fun transfer_admin(cap: AdminCap, new_admin: address) { } Testing Checklist Verify no functions return capability objects Confirm no functions return structs containing capabilities Check that capabilities lack store ability Verify capabilities are not stored in dynamic fields accessibly Test that leaked references cannot bypass access control Audit all places where capability references are passed Related Vulnerabilities Object Transfer Misuse Ability Misconfiguration Access-Control Mistakes</description>
    </item>
    <item>
      <title>12. Phantom Type Confusion</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/phantom-type-confusion/index.html</link>
      <pubDate>Wed, 26 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/phantom-type-confusion/index.html</guid>
      <description>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.&#xA;Risk Level High — Can bypass type-based access control and asset isolation.&#xA;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&#39;t appear in any field public struct Coin&lt;phantom T&gt; has key, store { id: UID, value: u64, } /// At runtime, Coin&lt;SUI&gt; and Coin&lt;USDC&gt; have identical layouts /// Only the type parameter differs The Vulnerability If your code doesn’t verify the phantom type parameter, attackers can:</description>
    </item>
    <item>
      <title>13. Unsafe Object ID Usage</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/unsafe-object-id-usage/index.html</link>
      <pubDate>Wed, 26 Nov 2025 20:11:50 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/unsafe-object-id-usage/index.html</guid>
      <description>Overview Object IDs (object::ID) in Sui are unique identifiers, but using them as stable identity anchors for child objects or in access control can lead to vulnerabilities. Child object IDs can change when objects are unwrapped, rewrapped, or transferred between parents.&#xA;Risk Level Medium — Can lead to authorization bypasses and state inconsistencies.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A01 (Broken Access Control) CWE-639 (Authorization Bypass), CWE-915 (Improperly Controlled Modification) The Problem Object ID Characteristics Object Type ID Stability Notes Address-owned Stable ID persists across transfers Shared Stable ID fixed after sharing Dynamic field child Unstable ID can change on wrap/unwrap Wrapped object Lost Inner object’s ID changes when unwrapped Common Mistakes Using child object IDs as permanent identifiers — IDs change when structure changes Storing IDs for authorization — Referenced object may no longer exist Cross-referencing by ID without verification — IDs may point to wrong objects Assuming ID uniqueness across time — Same ID could be reused after deletion Vulnerable Example module vulnerable::membership { use sui::object::{Self, UID, ID}; use sui::tx_context::TxContext; use sui::transfer; use sui::dynamic_object_field as dof; public struct Organization has key { id: UID, /// VULNERABLE: Storing child object IDs as member identifiers member_ids: vector&lt;ID&gt;, admin_member_id: ID, } public struct MemberBadge has key, store { id: UID, org_id: ID, role: u8, } /// VULNERABLE: Uses ID for permanent reference public entry fun add_member( org: &amp;mut Organization, ctx: &amp;mut TxContext ) { let badge = MemberBadge { id: object::new(ctx), org_id: object::id(org), role: 0, }; let badge_id = object::id(&amp;badge); vector::push_back(&amp;mut org.member_ids, badge_id); // Badge stored as dynamic field dof::add(&amp;mut org.id, badge_id, badge); } /// VULNERABLE: ID-based lookup can fail or return wrong object public entry fun promote_member( org: &amp;mut Organization, member_id: ID, ) { // What if badge was removed and re-added with same ID? // What if member_id doesn&#39;t exist? assert!(vector::contains(&amp;org.member_ids, &amp;member_id), E_NOT_MEMBER); let badge: &amp;mut MemberBadge = dof::borrow_mut(&amp;mut org.id, member_id); badge.role = 1; } /// VULNERABLE: Admin check using potentially invalid ID public entry fun admin_action( org: &amp;mut Organization, actor_badge_id: ID, ) { // If admin badge was rewrapped, this ID is stale assert!(actor_badge_id == org.admin_member_id, E_NOT_ADMIN); // ... perform admin action } /// VULNERABLE: ID reuse after deletion public entry fun remove_member( org: &amp;mut Organization, member_id: ID, ) { let badge: MemberBadge = dof::remove(&amp;mut org.id, member_id); // Remove from member list let (found, idx) = vector::index_of(&amp;org.member_ids, &amp;member_id); if (found) { vector::remove(&amp;mut org.member_ids, idx); }; // Delete badge let MemberBadge { id, org_id: _, role: _ } = badge; object::delete(id); // Problem: member_id is now &#34;free&#34; and could theoretically be reused // (not in practice for UID, but the logic is still flawed) } } Secure Example module secure::membership { use sui::object::{Self, UID, ID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; use sui::table::{Self, Table}; /// Use a stable identifier separate from object ID public struct MemberId has copy, drop, store { value: u64, } public struct Organization has key { id: UID, /// SECURE: Use stable member ID, not object ID next_member_id: u64, /// Map stable ID to member data members: Table&lt;MemberId, MemberRecord&gt;, admin_id: MemberId, } public struct MemberRecord has store { address: address, role: u8, joined_at: u64, } /// SECURE: Member badge references stable ID, owned by member public struct MemberBadge has key { id: UID, org_id: ID, member_id: MemberId, // Stable identifier } public entry fun add_member( org: &amp;mut Organization, member_address: address, ctx: &amp;mut TxContext ) { // Generate stable member ID let member_id = MemberId { value: org.next_member_id }; org.next_member_id = org.next_member_id + 1; // Store member record table::add(&amp;mut org.members, member_id, MemberRecord { address: member_address, role: 0, joined_at: tx_context::epoch(ctx), }); // Create badge with stable ID transfer::transfer( MemberBadge { id: object::new(ctx), org_id: object::id(org), member_id, }, member_address ); } /// SECURE: Verify both badge ownership and org membership public entry fun promote_member( org: &amp;mut Organization, badge: &amp;MemberBadge, ctx: &amp;TxContext ) { // Verify badge is for this org assert!(badge.org_id == object::id(org), E_WRONG_ORG); // Verify member exists in org assert!(table::contains(&amp;org.members, badge.member_id), E_NOT_MEMBER); let record = table::borrow_mut(&amp;mut org.members, badge.member_id); record.role = 1; } /// SECURE: Admin check using badge possession public entry fun admin_action( org: &amp;mut Organization, admin_badge: &amp;MemberBadge, ctx: &amp;TxContext ) { // Verify badge is for this org assert!(admin_badge.org_id == object::id(org), E_WRONG_ORG); // Verify caller holds the admin badge assert!(admin_badge.member_id == org.admin_id, E_NOT_ADMIN); // Additional: verify sender owns the badge // (implicit through object ownership) // ... perform admin action } /// SECURE: Clean removal with stable ID public entry fun remove_member( org: &amp;mut Organization, badge: MemberBadge, ctx: &amp;TxContext ) { let MemberBadge { id, org_id, member_id } = badge; // Verify badge is for this org assert!(org_id == object::id(org), E_WRONG_ORG); // Remove from membership table assert!(table::contains(&amp;org.members, member_id), E_NOT_MEMBER); let _record = table::remove(&amp;mut org.members, member_id); // Delete badge object::delete(id); } } Safe ID Usage Patterns Pattern 1: Stable Application-Level IDs /// Use incrementing counter for stable IDs public struct StableId has copy, drop, store { value: u64, } public struct IdGenerator has key { id: UID, next_id: u64, } public fun generate_id(gen: &amp;mut IdGenerator): StableId { let id = StableId { value: gen.next_id }; gen.next_id = gen.next_id + 1; id } Pattern 2: Object Ownership for Authorization /// Don&#39;t store IDs for auth — use object possession public entry fun authorized_action( cap: &amp;AuthCap, // Possession proves authorization target: &amp;mut Target, ) { // No ID comparison needed // Caller must own cap to include it in transaction } Pattern 3: Verify ID References /// When IDs must be used, verify they point to valid objects public fun use_reference( registry: &amp;Registry, obj_id: ID, ) { // Verify object still exists in registry assert!(table::contains(&amp;registry.objects, obj_id), E_OBJECT_NOT_FOUND); // Get the actual object and verify properties let obj = table::borrow(&amp;registry.objects, obj_id); assert!(obj.valid, E_OBJECT_INVALID); } Pattern 4: Immutable Reference Objects /// Create immutable reference objects for stable identity public struct IdentityAnchor has key { id: UID, // Never modified after creation owner: address, created_at: u64, } public fun create_anchor(ctx: &amp;mut TxContext): IdentityAnchor { let anchor = IdentityAnchor { id: object::new(ctx), owner: tx_context::sender(ctx), created_at: tx_context::epoch(ctx), }; // Immediately freeze — ID now permanently stable transfer::freeze_object(anchor); anchor } Recommended Mitigations 1. Use Application-Level Identifiers // Instead of object::id(obj) // Use a stable counter-based ID let stable_id = StableId { value: counter.next() }; 2. Prefer Object Possession Over ID Checks // BAD: ID comparison assert!(user_id == stored_admin_id, E_NOT_ADMIN); // GOOD: Object possession public entry fun admin_action(admin_cap: &amp;AdminCap, ...) { } 3. Verify Referenced Objects Still Exist public fun safe_lookup(registry: &amp;Registry, id: ID): &amp;Object { assert!(registry.contains(id), E_NOT_FOUND); registry.borrow(id) } 4. Document ID Stability Requirements /// NOTE: This ID is stable because: /// - Object is address-owned (not wrapped) /// - Object is never transferred to dynamic field /// - Object is immutable after creation Testing Checklist Test that removing and re-adding objects doesn’t reuse stale IDs Verify authorization works after objects are transferred Test behavior when referenced objects are deleted Confirm child object ID changes are handled correctly Test with wrapped and unwrapped object scenarios Related Vulnerabilities Dynamic Field Misuse Ownership Model Confusion Access-Control Mistakes</description>
    </item>
    <item>
      <title>14. Dynamic Field Key Collisions</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/dynamic-field-key-collisions/index.html</link>
      <pubDate>Wed, 26 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/dynamic-field-key-collisions/index.html</guid>
      <description>Overview Dynamic fields in Sui use keys to store and retrieve values. When user-controlled or predictable keys are used, attackers can cause collisions that overwrite existing data, inject malicious values, or break protocol invariants.&#xA;Risk Level High — Can lead to data corruption, asset theft, or protocol takeover.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A01 (Broken Access Control), A05 (Security Misconfiguration) CWE-653 (Improper Isolation), CWE-706 (Use of Incorrectly-Resolved Name) The Problem How Dynamic Field Keys Work // Keys can be any type with `copy + drop + store` df::add(&amp;mut uid, key, value); // Same key retrieves the same slot let val = df::borrow(&amp;uid, key); // Different key types create different namespaces df::add(&amp;mut uid, string_key, value1); df::add(&amp;mut uid, u64_key, value2); // Different namespace Collision Scenarios User-controlled string keys — Attacker chooses key to collide with system data Predictable numeric keys — Sequential IDs can be predicted and front-run Type confusion — Same key value in different types might be expected to differ Namespace pollution — Attacker fills namespace with garbage data Vulnerable Example module vulnerable::storage { use sui::object::UID; use sui::dynamic_field as df; use sui::tx_context::{Self, TxContext}; public struct Storage has key { id: UID, } public struct UserData has store { balance: u64, is_admin: bool, } /// VULNERABLE: User-controlled key allows collision attacks public entry fun store_user_data( storage: &amp;mut Storage, username: vector&lt;u8&gt;, // User-controlled key! balance: u64, ctx: &amp;mut TxContext ) { // Attacker can choose username = &#34;admin&#34; and overwrite admin data df::add(&amp;mut storage.id, username, UserData { balance, is_admin: false, }); } /// VULNERABLE: System uses same key namespace public entry fun set_admin( storage: &amp;mut Storage, admin_name: vector&lt;u8&gt;, ) { df::add(&amp;mut storage.id, admin_name, UserData { balance: 0, is_admin: true, }); } /// VULNERABLE: No existence check before add public entry fun update_balance( storage: &amp;mut Storage, username: vector&lt;u8&gt;, amount: u64, ) { // Will abort if key doesn&#39;t exist // But attacker might have already added their own entry let data: &amp;mut UserData = df::borrow_mut(&amp;mut storage.id, username); data.balance = data.balance + amount; } } module vulnerable::vault { use sui::dynamic_field as df; public struct Vault has key { id: UID, next_slot_id: u64, // Sequential, predictable } /// VULNERABLE: Predictable slot IDs can be front-run public entry fun create_deposit_slot( vault: &amp;mut Vault, ctx: &amp;mut TxContext ): u64 { let slot_id = vault.next_slot_id; vault.next_slot_id = slot_id + 1; // Attacker predicts next slot_id and front-runs df::add(&amp;mut vault.id, slot_id, DepositSlot { owner: tx_context::sender(ctx), amount: 0, }); slot_id } } Attack: Key Collision // Attacker observes admin_name = &#34;superadmin&#34; was used module attack::collision { use vulnerable::storage; public entry fun become_admin( storage: &amp;mut storage::Storage, ctx: &amp;mut TxContext ) { // Use same key as admin to inject data // If store_user_data doesn&#39;t check existence, this might work storage::store_user_data( storage, b&#34;superadmin&#34;, // Collide with admin key 999999, ctx ); } } Secure Example module secure::storage { use sui::object::{Self, UID, ID}; use sui::dynamic_field as df; use sui::tx_context::{Self, TxContext}; use std::type_name::{Self, TypeName}; const E_KEY_EXISTS: u64 = 0; const E_KEY_NOT_FOUND: u64 = 1; const E_NOT_OWNER: u64 = 2; public struct Storage has key { id: UID, } /// SECURE: Type-safe key for user data public struct UserDataKey has copy, drop, store { user_address: address, } /// SECURE: Separate type for admin keys public struct AdminKey has copy, drop, store { admin_address: address, } public struct UserData has store { balance: u64, } public struct AdminData has store { permissions: u64, } /// SECURE: Key is derived from sender address (unique) public entry fun store_user_data( storage: &amp;mut Storage, balance: u64, ctx: &amp;mut TxContext ) { let sender = tx_context::sender(ctx); let key = UserDataKey { user_address: sender }; // Check if already exists assert!(!df::exists_(&amp;storage.id, key), E_KEY_EXISTS); df::add(&amp;mut storage.id, key, UserData { balance }); } /// SECURE: Admin uses different key type public entry fun set_admin( storage: &amp;mut Storage, admin_cap: &amp;AdminCap, admin_address: address, permissions: u64, ) { let key = AdminKey { admin_address }; // Remove existing if present if (df::exists_(&amp;storage.id, key)) { let _: AdminData = df::remove(&amp;mut storage.id, key); }; df::add(&amp;mut storage.id, key, AdminData { permissions }); } /// SECURE: Verify ownership before update public entry fun update_balance( storage: &amp;mut Storage, amount: u64, ctx: &amp;mut TxContext ) { let sender = tx_context::sender(ctx); let key = UserDataKey { user_address: sender }; assert!(df::exists_(&amp;storage.id, key), E_KEY_NOT_FOUND); let data: &amp;mut UserData = df::borrow_mut(&amp;mut storage.id, key); data.balance = data.balance + amount; } } module secure::vault { use sui::object::{Self, UID, ID}; use sui::dynamic_field as df; use sui::tx_context::{Self, TxContext}; use sui::hash; public struct Vault has key { id: UID, } /// SECURE: Unpredictable slot key using object ID public struct SlotKey has copy, drop, store { slot_id: ID, } public struct DepositSlot has store { owner: address, amount: u64, } /// SECURE: Slot ID is unpredictable object ID public entry fun create_deposit_slot( vault: &amp;mut Vault, ctx: &amp;mut TxContext ): ID { // Create a temporary object just for its unique ID let temp_uid = object::new(ctx); let slot_id = object::uid_to_inner(&amp;temp_uid); let key = SlotKey { slot_id }; df::add(&amp;mut vault.id, key, DepositSlot { owner: tx_context::sender(ctx), amount: 0, }); object::delete(temp_uid); slot_id } } Key Design Patterns Pattern 1: Type-Safe Key Namespaces /// Each data type has its own key type public struct UserBalanceKey has copy, drop, store { user: address } public struct UserProfileKey has copy, drop, store { user: address } public struct ConfigKey has copy, drop, store { name: vector&lt;u8&gt; } /// Compiler ensures different key types = different namespaces df::add(&amp;mut uid, UserBalanceKey { user }, balance); df::add(&amp;mut uid, UserProfileKey { user }, profile); // These cannot collide even with same `user` value Pattern 2: Sender-Derived Keys /// Only sender can create their own key public fun user_key(ctx: &amp;TxContext): UserKey { UserKey { address: tx_context::sender(ctx) } } public entry fun store(container: &amp;mut Container, data: Data, ctx: &amp;mut TxContext) { let key = user_key(ctx); // Key derived from sender df::add(&amp;mut container.id, key, data); } Pattern 3: Composite Keys /// Combine multiple factors for unique keys public struct CompositeKey has copy, drop, store { owner: address, category: u8, index: u64, } /// Uniqueness across multiple dimensions public fun make_key(owner: address, category: u8, index: u64): CompositeKey { CompositeKey { owner, category, index } } Pattern 4: Existence Checks /// Always check before add/remove public fun safe_add&lt;K: copy + drop + store, V: store&gt;( uid: &amp;mut UID, key: K, value: V, ) { assert!(!df::exists_(uid, key), E_ALREADY_EXISTS); df::add(uid, key, value); } public fun safe_get&lt;K: copy + drop + store, V: store&gt;( uid: &amp;UID, key: K, ): &amp;V { assert!(df::exists_(uid, key), E_NOT_FOUND); df::borrow(uid, key) } Recommended Mitigations 1. Never Use User-Controlled Strings as Keys // BAD public fun store(uid: &amp;mut UID, user_key: vector&lt;u8&gt;, value: Data) { df::add(uid, user_key, value); } // GOOD public fun store(uid: &amp;mut UID, value: Data, ctx: &amp;TxContext) { let key = TypeSafeKey { owner: tx_context::sender(ctx) }; df::add(uid, key, value); } 2. Use Object IDs for Unpredictable Keys let uid = object::new(ctx); let unique_key = object::uid_to_inner(&amp;uid); object::delete(uid); // unique_key is cryptographically random 3. Separate Key Types for Different Data // Different structs = different namespaces public struct UserKey has copy, drop, store { ... } public struct AdminKey has copy, drop, store { ... } public struct ConfigKey has copy, drop, store { ... } 4. Always Check Existence // Before add assert!(!df::exists_(uid, key), E_EXISTS); // Before borrow/remove assert!(df::exists_(uid, key), E_NOT_FOUND); Testing Checklist Test that different users cannot access each other’s data Verify key collisions are properly rejected Test admin and user key namespaces are isolated Confirm unpredictable keys cannot be front-run Test existence checks prevent overwrites Related Vulnerabilities Dynamic Field Misuse Access-Control Mistakes Unbounded Child Growth</description>
    </item>
    <item>
      <title>15. Event Design Vulnerabilities</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/event-design-vulnerabilities/index.html</link>
      <pubDate>Wed, 26 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/event-design-vulnerabilities/index.html</guid>
      <description>Overview Events in Sui are the primary mechanism for off-chain systems to observe on-chain state changes. Poor event design leads to missed state changes, ambiguous interpretations, replay vulnerabilities, and off-chain system failures.&#xA;Risk Level Medium — Can cause off-chain desync, incorrect UI state, or indexer failures.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A04 (Insecure Design), A09 (Security Logging and Monitoring Failures) CWE-223 (Omission of Security-relevant Information), CWE-778 (Insufficient Logging) The Problem Event Design Issues Missing events — State changes without corresponding events Ambiguous events — Events that don’t clearly indicate what happened Incomplete events — Missing critical context (who, what, when) Duplicate events — Same event for different operations No event versioning — Breaking changes affect off-chain systems Vulnerable Example module vulnerable::trading { use sui::object::{Self, UID}; use sui::event; use sui::tx_context::TxContext; /// VULNERABLE: Ambiguous event — which operation? public struct TradeEvent has copy, drop { amount: u64, } /// VULNERABLE: Missing critical context public struct TransferEvent has copy, drop { amount: u64, // Missing: from, to, timestamp, tx_digest, token_type } public struct Order has key { id: UID, amount: u64, status: u8, } /// VULNERABLE: State change without event public entry fun cancel_order(order: &amp;mut Order) { order.status = 2; // Cancelled // NO EVENT! Off-chain systems don&#39;t know about this } /// VULNERABLE: Same event for different operations public entry fun buy(amount: u64) { // ... execute buy event::emit(TradeEvent { amount }); // Was this a buy? } public entry fun sell(amount: u64) { // ... execute sell event::emit(TradeEvent { amount }); // Or a sell? } /// VULNERABLE: Event emitted before operation might fail public entry fun risky_transfer( amount: u64, ctx: &amp;mut TxContext ) { event::emit(TransferEvent { amount }); // This might abort! But event already emitted assert!(amount &gt; 0, E_ZERO_AMOUNT); // ... transfer logic that might also fail } } Impact on Off-Chain Systems // Off-chain indexer can&#39;t determine: // 1. Was TradeEvent a buy or sell? // 2. Who initiated the trade? // 3. What was the token involved? // 4. What was the order status before change? async function processEvent(event) { if (event.type === &#39;TradeEvent&#39;) { // Ambiguous! Can&#39;t update state correctly } } Secure Example module secure::trading { use sui::object::{Self, UID, ID}; use sui::event; use sui::tx_context::{Self, TxContext}; use sui::clock::{Self, Clock}; use std::type_name::{Self, TypeName}; // Event version for future compatibility const EVENT_VERSION: u8 = 1; /// SECURE: Specific, complete event for each operation public struct OrderCreatedEvent has copy, drop { version: u8, order_id: ID, creator: address, token_type: TypeName, amount: u64, price: u64, side: u8, // 0 = buy, 1 = sell timestamp_ms: u64, } public struct OrderFilledEvent has copy, drop { version: u8, order_id: ID, filler: address, fill_amount: u64, fill_price: u64, remaining_amount: u64, timestamp_ms: u64, } public struct OrderCancelledEvent has copy, drop { version: u8, order_id: ID, canceller: address, unfilled_amount: u64, reason: u8, // 0 = user, 1 = expired, 2 = admin timestamp_ms: u64, } public struct TransferEvent has copy, drop { version: u8, token_type: TypeName, from: address, to: address, amount: u64, memo: vector&lt;u8&gt;, timestamp_ms: u64, } public struct Order has key { id: UID, creator: address, amount: u64, filled: u64, price: u64, side: u8, status: u8, } /// SECURE: Event emitted only after successful state change public entry fun create_order&lt;T&gt;( amount: u64, price: u64, side: u8, clock: &amp;Clock, ctx: &amp;mut TxContext ) { // Validate first assert!(amount &gt; 0, E_ZERO_AMOUNT); assert!(price &gt; 0, E_ZERO_PRICE); assert!(side &lt;= 1, E_INVALID_SIDE); // Create order let order = Order { id: object::new(ctx), creator: tx_context::sender(ctx), amount, filled: 0, price, side, status: 0, // Active }; let order_id = object::id(&amp;order); // Transfer order to creator transfer::transfer(order, tx_context::sender(ctx)); // Emit event AFTER successful creation event::emit(OrderCreatedEvent { version: EVENT_VERSION, order_id, creator: tx_context::sender(ctx), token_type: type_name::get&lt;T&gt;(), amount, price, side, timestamp_ms: clock::timestamp_ms(clock), }); } /// SECURE: Event includes before/after state public entry fun cancel_order( order: Order, clock: &amp;Clock, ctx: &amp;mut TxContext ) { assert!(tx_context::sender(ctx) == order.creator, E_NOT_CREATOR); let unfilled = order.amount - order.filled; let order_id = object::id(&amp;order); // Clean up order let Order { id, creator: _, amount: _, filled: _, price: _, side: _, status: _ } = order; object::delete(id); // Emit cancellation event event::emit(OrderCancelledEvent { version: EVENT_VERSION, order_id, canceller: tx_context::sender(ctx), unfilled_amount: unfilled, reason: 0, // User-initiated timestamp_ms: clock::timestamp_ms(clock), }); } /// SECURE: Fill event includes all relevant details public entry fun fill_order( order: &amp;mut Order, fill_amount: u64, clock: &amp;Clock, ctx: &amp;mut TxContext ) { assert!(order.status == 0, E_ORDER_NOT_ACTIVE); assert!(fill_amount &lt;= order.amount - order.filled, E_OVERFILL); order.filled = order.filled + fill_amount; if (order.filled == order.amount) { order.status = 1; // Filled }; event::emit(OrderFilledEvent { version: EVENT_VERSION, order_id: object::id(order), filler: tx_context::sender(ctx), fill_amount, fill_price: order.price, remaining_amount: order.amount - order.filled, timestamp_ms: clock::timestamp_ms(clock), }); } } Event Design Guidelines 1. Event Naming Convention /// Use past tense — events describe completed actions public struct OrderCreated has copy, drop { } // Good public struct CreateOrder has copy, drop { } // Bad /// Specific names for specific operations public struct TokensMinted has copy, drop { } public struct TokensBurned has copy, drop { } public struct TokensTransferred has copy, drop { } 2. Essential Event Fields public struct CompleteEvent has copy, drop { // Version for forward compatibility version: u8, // WHO actor: address, // WHAT object_id: ID, operation_type: u8, // WHEN timestamp_ms: u64, // CONTEXT old_value: u64, new_value: u64, // OPTIONAL: Additional context metadata: vector&lt;u8&gt;, } 3. Event Ordering public entry fun complex_operation(...) { // 1. Validate inputs assert!(valid_input, E_INVALID); // 2. Perform state changes state.value = new_value; // 3. Emit events AFTER success event::emit(StateChangedEvent { ... }); } 4. Event Versioning const EVENT_VERSION_V1: u8 = 1; const EVENT_VERSION_V2: u8 = 2; /// V1: Original event public struct TransferEventV1 has copy, drop { version: u8, from: address, to: address, amount: u64, } /// V2: Added fields (backward compatible) public struct TransferEventV2 has copy, drop { version: u8, from: address, to: address, amount: u64, // New fields token_type: TypeName, memo: vector&lt;u8&gt;, } Recommended Mitigations 1. Every State Change Gets an Event public entry fun update_config(config: &amp;mut Config, new_value: u64) { let old_value = config.value; config.value = new_value; event::emit(ConfigUpdatedEvent { old_value, new_value, updater: tx_context::sender(ctx), }); } 2. Use Specific Events for Each Operation // Instead of generic &#34;TradeEvent&#34; public struct BuyOrderExecuted has copy, drop { ... } public struct SellOrderExecuted has copy, drop { ... } public struct OrderMatched has copy, drop { ... } 3. Include Sufficient Context // Bad: Missing context event::emit(Transfer { amount: 100 }); // Good: Complete context event::emit(TransferEvent { version: 1, from: sender, to: recipient, amount: 100, token_type: type_name::get&lt;T&gt;(), timestamp_ms: clock::timestamp_ms(clock), }); 4. Emit After Successful Completion // Bad: Emit before potential failure event::emit(ActionEvent { ... }); assert!(condition, E_FAILED); // If this fails, event was false // Good: Emit after success assert!(condition, E_FAILED); // ... all operations succeed event::emit(ActionEvent { ... }); Testing Checklist Every state-changing function emits an event Events include who, what, when, and relevant context No events are emitted before potential abort points Different operations emit distinguishable events Events are versioned for future compatibility Off-chain systems can reconstruct state from events Related Vulnerabilities Event State Inconsistency General Move Logic Errors Clock Time Misuse</description>
    </item>
    <item>
      <title>16. Unbounded Child Growth</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/unbounded-child-growth/index.html</link>
      <pubDate>Wed, 26 Nov 2025 20:16:13 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/unbounded-child-growth/index.html</guid>
      <description>Overview Parent objects can accumulate unlimited child objects through dynamic fields or dynamic object fields. This unbounded growth causes gas exhaustion, state bloat, and can make critical operations prohibitively expensive or impossible.&#xA;Risk Level High — Can cause denial of service and gas exhaustion.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A06 (Vulnerable Components), A05 (Security Misconfiguration) CWE-400 (Uncontrolled Resource Consumption), CWE-770 (Allocation of Resources Without Limits) The Problem Gas Implications While Sui charges gas per byte, objects with many children:</description>
    </item>
    <item>
      <title>17. PTB Ordering Issues</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/ptb-ordering-issues/index.html</link>
      <pubDate>Wed, 26 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/ptb-ordering-issues/index.html</guid>
      <description>Overview Programmable Transaction Blocks (PTBs) in Sui allow multiple operations in a single transaction. Attackers can reorder or interleave calls in unexpected ways, bypassing invariants that assume specific execution order.&#xA;Risk Level High — Can bypass access control and break protocol invariants.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A04 (Insecure Design) CWE-841 (Improper Enforcement of Behavioral Workflow), CWE-662 (Improper Synchronization) The Problem PTB Characteristics Multiple Move calls in one atomic transaction Caller controls the order of calls Intermediate results can be passed between calls All calls see the same object state (within the PTB) Vulnerability Pattern Expected: auth_check() → perform_action() Attacker: perform_action() → some_cleanup() // Skip auth entirely Vulnerable Example module vulnerable::multistep { use sui::object::{Self, UID}; use sui::tx_context::TxContext; public struct AuthSession has key { id: UID, user: address, is_authenticated: bool, } public struct Vault has key { id: UID, balance: u64, } /// Step 1: User calls this to authenticate public entry fun authenticate( session: &amp;mut AuthSession, password_hash: vector&lt;u8&gt;, ) { // Verify password assert!(verify_password(password_hash), E_WRONG_PASSWORD); session.is_authenticated = true; } /// Step 2: User calls this to withdraw (should be after auth) /// VULNERABLE: Assumes authenticate was called first public entry fun withdraw( session: &amp;AuthSession, vault: &amp;mut Vault, amount: u64, ctx: &amp;mut TxContext ) { // This check can be bypassed in a PTB! assert!(session.is_authenticated, E_NOT_AUTHENTICATED); vault.balance = vault.balance - amount; // ... transfer to user } /// VULNERABLE: Cleanup resets auth state public entry fun logout(session: &amp;mut AuthSession) { session.is_authenticated = false; } } module vulnerable::flashloan { public struct Pool has key { id: UID, balance: u64, borrowed: u64, } /// VULNERABLE: No hot potato to enforce repayment public entry fun borrow( pool: &amp;mut Pool, amount: u64, ) { assert!(pool.balance &gt;= amount, E_INSUFFICIENT); pool.borrowed = pool.borrowed + amount; pool.balance = pool.balance - amount; // ... give coins to borrower } /// Repayment can be skipped in PTB public entry fun repay( pool: &amp;mut Pool, amount: u64, ) { pool.borrowed = pool.borrowed - amount; pool.balance = pool.balance + amount; // ... receive coins } } Attack: PTB Reordering // Attacker&#39;s PTB: Transaction { // Skip authenticate entirely! // Or: authenticate with wrong password, then proceed anyway commands: [ // Borrow from flash loan Call(flashloan::borrow, [pool, 1000000]), // Use borrowed funds for exploit Call(some_protocol::exploit, [borrowed_coins]), // Never call flashloan::repay // Transaction completes successfully! ] } Secure Example module secure::multistep { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; /// Hot potato pattern — must be consumed in same transaction public struct AuthToken { user: address, expires_at: u64, vault_id: ID, } public struct Vault has key { id: UID, balance: u64, } /// SECURE: Returns hot potato that must be consumed public fun authenticate( password_hash: vector&lt;u8&gt;, vault: &amp;Vault, clock: &amp;Clock, ctx: &amp;TxContext ): AuthToken { assert!(verify_password(password_hash, ctx), E_WRONG_PASSWORD); AuthToken { user: tx_context::sender(ctx), expires_at: clock::timestamp_ms(clock) + 60000, // 1 minute vault_id: object::id(vault), } } /// SECURE: Consumes hot potato, ensuring auth happened public fun withdraw( token: AuthToken, vault: &amp;mut Vault, amount: u64, clock: &amp;Clock, ctx: &amp;mut TxContext ) { let AuthToken { user, expires_at, vault_id } = token; // Verify token is for this vault assert!(vault_id == object::id(vault), E_WRONG_VAULT); // Verify token hasn&#39;t expired assert!(clock::timestamp_ms(clock) &lt; expires_at, E_EXPIRED); // Verify caller is the authenticated user assert!(tx_context::sender(ctx) == user, E_WRONG_USER); vault.balance = vault.balance - amount; // Token is consumed — cannot be reused } } module secure::flashloan { use sui::object::{Self, UID, ID}; use sui::coin::{Self, Coin}; use sui::sui::SUI; public struct Pool has key { id: UID, coins: Coin&lt;SUI&gt;, } /// Hot potato — MUST be repaid before transaction ends public struct FlashLoanReceipt { pool_id: ID, amount: u64, fee: u64, } /// SECURE: Returns receipt that must be consumed public fun borrow( pool: &amp;mut Pool, amount: u64, ctx: &amp;mut TxContext ): (Coin&lt;SUI&gt;, FlashLoanReceipt) { assert!(coin::value(&amp;pool.coins) &gt;= amount, E_INSUFFICIENT); let borrowed = coin::split(&amp;mut pool.coins, amount, ctx); let fee = amount / 1000; // 0.1% fee let receipt = FlashLoanReceipt { pool_id: object::id(pool), amount, fee, }; (borrowed, receipt) } /// SECURE: Must be called to destroy receipt public fun repay( pool: &amp;mut Pool, receipt: FlashLoanReceipt, repayment: Coin&lt;SUI&gt;, ) { let FlashLoanReceipt { pool_id, amount, fee } = receipt; // Verify correct pool assert!(pool_id == object::id(pool), E_WRONG_POOL); // Verify full repayment with fee assert!(coin::value(&amp;repayment) &gt;= amount + fee, E_INSUFFICIENT_REPAYMENT); coin::join(&amp;mut pool.coins, repayment); // Receipt consumed — loan is repaid } } PTB-Safe Patterns Pattern 1: Hot Potato (Must Consume) /// No abilities — cannot be stored, dropped, or copied /// MUST be consumed in the same transaction public struct MustConsume { value: u64, } public fun start_operation(): MustConsume { MustConsume { value: 100 } } public fun finish_operation(potato: MustConsume) { let MustConsume { value: _ } = potato; // Potato consumed — operation complete } Pattern 2: Atomic Operations /// Combine check and action in single function public entry fun authenticated_withdraw( password_hash: vector&lt;u8&gt;, vault: &amp;mut Vault, amount: u64, ctx: &amp;mut TxContext ) { // Auth and action in same call — cannot be separated assert!(verify_password(password_hash, ctx), E_WRONG_PASSWORD); vault.balance = vault.balance - amount; } Pattern 3: Sequence Numbers public struct StateMachine has key { id: UID, current_step: u64, expected_next: u64, } public entry fun step_one(state: &amp;mut StateMachine) { assert!(state.current_step == 0, E_WRONG_STEP); state.current_step = 1; state.expected_next = 2; } public entry fun step_two(state: &amp;mut StateMachine) { assert!(state.current_step == 1, E_WRONG_STEP); state.current_step = 2; // ... } Pattern 4: Invariant Assertions /// Check invariants at function boundaries public entry fun operation(state: &amp;mut State, ...) { // Pre-conditions assert_invariants(state); // Perform operation // ... // Post-conditions assert_invariants(state); } fun assert_invariants(state: &amp;State) { assert!(state.total == state.a + state.b, E_INVARIANT_BROKEN); assert!(state.balance &gt;= state.minimum, E_UNDERCOLLATERALIZED); } Recommended Mitigations 1. Use Hot Potatoes for Multi-Step Operations // Force the caller to complete the operation public fun start(): Receipt { } public fun finish(receipt: Receipt) { } // Receipt has no abilities — must be consumed 2. Validate Invariants in Every Function public entry fun any_function(state: &amp;mut State, ...) { // Don&#39;t assume previous function was called // Validate everything this function needs } 3. Make Operations Atomic When Possible // Instead of: check() → action() // Use: check_and_action() 4. Use Object Ownership for Authorization // Object ownership is enforced by Sui itself public entry fun action(cap: &amp;AdminCap, ...) { // Caller must own cap — cannot be bypassed } Testing Checklist Test each function in isolation (not just expected sequence) Test with functions called in reversed order Test skipping intermediate steps Verify hot potatoes cannot be stored or dropped Test that invariants hold regardless of call order Related Vulnerabilities PTB Refund Issues General Move Logic Errors Access-Control Mistakes</description>
    </item>
    <item>
      <title>18. PTB Refund Issues</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/ptb-refund-issues/index.html</link>
      <pubDate>Wed, 26 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/ptb-refund-issues/index.html</guid>
      <description>Overview Improper refund or undo patterns in Programmable Transaction Blocks (PTBs) can leave state inconsistent when partial execution occurs. Write-then-undo patterns are particularly dangerous as they can be exploited.&#xA;Risk Level Medium — Can lead to inconsistent state and protocol manipulation.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A04 (Insecure Design) CWE-841 (Improper Enforcement of Behavioral Workflow), CWE-662 (Improper Synchronization) The Problem In PTBs, if a later operation fails, earlier operations are NOT rolled back within custom logic. Sui’s atomicity ensures the transaction fails entirely, but if your code has a “refund” or “undo” function, partial execution becomes possible.</description>
    </item>
    <item>
      <title>19. Ownership Model Confusion</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/ownership-model-confusion/index.html</link>
      <pubDate>Wed, 26 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/ownership-model-confusion/index.html</guid>
      <description>Overview Sui has multiple ownership models: address-owned, shared, immutable, and object-owned (wrapped/child). Incorrect transitions between these models or confusion about which model applies can break invariants and security assumptions.&#xA;Risk Level High — Can lead to complete access control bypass.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A01 (Broken Access Control) CWE-284 (Improper Access Control), CWE-266 (Incorrect Privilege Assignment) The Problem Ownership Models Model Created By Access Mutability Reversible Address-owned transfer() Owner only Yes Yes (transfer) Shared share_object() Anyone Yes No Immutable freeze_object() Anyone (read) No No Object-owned transfer_to_object() Parent object Yes Yes Common Confusion Shared → Address-owned — Not possible after sharing Immutable → Mutable — Not possible after freezing Object-owned access — Parent owner doesn’t automatically control child Wrapped objects — UID changes behavior Vulnerable Example module vulnerable::ownership { use sui::object::{Self, UID}; use sui::tx_context::TxContext; use sui::transfer; public struct Vault has key, store { id: UID, balance: u64, owner: address, } public struct VaultController has key { id: UID, } /// VULNERABLE: Attempts to &#34;unshare&#34; an object public entry fun make_private( vault: Vault, // Taking by value from shared object new_owner: address, ) { // This doesn&#39;t work as expected! // Once shared, always shared // This just moves the shared object, it&#39;s still shared transfer::transfer(vault, new_owner); } /// VULNERABLE: Assumes object ownership = child access public entry fun access_child_vault( controller: &amp;VaultController, // Can&#39;t actually access object-owned objects this way // The vault would need to be passed separately ) { // This function signature is fundamentally broken // Object ownership doesn&#39;t give direct access to children } /// VULNERABLE: Wrong ownership transition public entry fun setup_vault( ctx: &amp;mut TxContext ) { let vault = Vault { id: object::new(ctx), balance: 1000, owner: tx_context::sender(ctx), }; // Bug: sharing when should be transferring to owner // Now anyone can access the vault! transfer::share_object(vault); } /// VULNERABLE: Freezing breaks protocol public entry fun publish_vault( vault: Vault, ) { // Freezing makes it immutable forever // Can never update balance again! transfer::freeze_object(vault); } } Secure Example module secure::ownership { use sui::object::{Self, UID, ID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; use sui::dynamic_object_field as dof; /// Private vault — only owner can access public struct PrivateVault has key { id: UID, balance: u64, } /// Shared pool — anyone can interact (with proper checks) public struct SharedPool has key { id: UID, balance: u64, admin_cap_id: ID, } /// Admin capability — controls the shared pool public struct PoolAdminCap has key { id: UID, pool_id: ID, } /// Published config — immutable by design public struct PublishedConfig has key { id: UID, version: u64, // Only immutable data here fee_bps: u64, name: vector&lt;u8&gt;, } /// SECURE: Clear ownership from the start public entry fun create_private_vault( initial_balance: u64, ctx: &amp;mut TxContext ) { let vault = PrivateVault { id: object::new(ctx), balance: initial_balance, }; // Private — only creator can access transfer::transfer(vault, tx_context::sender(ctx)); } /// SECURE: Shared with proper access control public entry fun create_shared_pool( ctx: &amp;mut TxContext ) { let pool = SharedPool { id: object::new(ctx), balance: 0, admin_cap_id: object::id_from_address(@0x0), // Placeholder }; let pool_id = object::id(&amp;pool); let cap = PoolAdminCap { id: object::new(ctx), pool_id, }; pool.admin_cap_id = object::id(&amp;cap); // Pool is shared, but admin actions require cap transfer::share_object(pool); transfer::transfer(cap, tx_context::sender(ctx)); } /// SECURE: Admin action requires capability public entry fun admin_withdraw( cap: &amp;PoolAdminCap, pool: &amp;mut SharedPool, amount: u64, ctx: &amp;mut TxContext ) { // Verify cap is for this pool assert!(cap.pool_id == object::id(pool), E_WRONG_POOL); pool.balance = pool.balance - amount; // ... transfer } /// SECURE: Published config is intentionally immutable public entry fun publish_config( version: u64, fee_bps: u64, name: vector&lt;u8&gt;, ctx: &amp;mut TxContext ) { let config = PublishedConfig { id: object::new(ctx), version, fee_bps, name, }; // Intentionally immutable — this is the design transfer::freeze_object(config); } /// SECURE: Use dynamic fields for parent-child relationships public struct Parent has key { id: UID, } public struct Child has key, store { id: UID, value: u64, } public entry fun add_child_to_parent( parent: &amp;mut Parent, child: Child, ) { dof::add(&amp;mut parent.id, b&#34;child&#34;, child); } public fun access_child(parent: &amp;Parent): &amp;Child { dof::borrow(&amp;parent.id, b&#34;child&#34;) } public fun access_child_mut(parent: &amp;mut Parent): &amp;mut Child { dof::borrow_mut(&amp;mut parent.id, b&#34;child&#34;) } } Ownership Decision Guide When to Use Address-Owned // User-specific assets public struct UserWallet has key { } // Individual NFTs public struct NFT has key { } // Capabilities (usually) public struct AdminCap has key { } transfer::transfer(obj, owner); When to Use Shared // Global registries public struct Registry has key { } // Liquidity pools public struct Pool has key { } // Order books public struct OrderBook has key { } transfer::share_object(obj); When to Use Immutable // Published configurations public struct Config has key { } // Static data public struct Metadata has key { } // Verified credentials public struct Credential has key { } transfer::freeze_object(obj); When to Use Object-Owned/Dynamic Fields // Parent-child relationships public struct Parent has key { id: UID, // Children via dynamic fields } // Encapsulated components dof::add(&amp;mut parent.id, key, child); Recommended Mitigations 1. Document Ownership Intent /// This object is SHARED because: /// - Multiple users need to interact /// - Admin actions protected by AdminCap /// NEVER attempt to unshare public struct SharedProtocol has key { } 2. Use Type System to Enforce Ownership /// No `store` = cannot be wrapped or transferred publicly public struct MustBeOwned has key { id: UID, } /// Has `store` = can be wrapped in other objects public struct CanBeWrapped has key, store { id: UID, } 3. Validate Ownership Before Operations public entry fun owner_only_action( obj: &amp;mut MyObject, ctx: &amp;TxContext ) { // For address-owned: ownership enforced by Sui // For shared: explicit check required assert!(tx_context::sender(ctx) == obj.owner, E_NOT_OWNER); } 4. Create Clear Ownership Transitions /// Explicit transition from private to shared public entry fun make_shared( obj: PrivateObject, ctx: &amp;TxContext ) { assert!(tx_context::sender(ctx) == obj.owner, E_NOT_OWNER); // Convert to shared form let shared = SharedObject { id: obj.id, data: obj.data, original_owner: obj.owner, }; transfer::share_object(shared); } Testing Checklist Verify ownership model matches intended access pattern Test that shared objects cannot be “unshared” Confirm immutable objects are truly immutable Test parent-child access patterns with dynamic fields Verify ownership transitions are intentional and documented Related Vulnerabilities Object Transfer Misuse Improper Object Sharing Object Freezing Misuse</description>
    </item>
    <item>
      <title>20. Weak Initializers</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/weak-initializers/index.html</link>
      <pubDate>Wed, 26 Nov 2025 20:16:25 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/weak-initializers/index.html</guid>
      <description>Overview Weak or improperly protected initialization functions can allow attackers to reinitialize protocol state, overwrite critical settings, or take control of the protocol. The init function in Sui Move has special protections, but custom initialization patterns often lack similar safeguards.&#xA;Risk Level Critical — Can lead to complete protocol takeover.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A01 (Broken Access Control) CWE-284 (Improper Access Control), CWE-665 (Improper Initialization) The Problem Safe vs Unsafe Initialization Pattern Safety Notes fun init(ctx) Safe Called once at publish, not callable after fun init(witness: WITNESS, ctx) Safe One-Time Witness pattern public fun initialize(...) Unsafe Can be called by anyone, anytime public entry fun setup(...) Unsafe Unless properly guarded Vulnerable Example module vulnerable::protocol { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; public struct ProtocolState has key { id: UID, admin: address, fee_bps: u64, treasury: address, initialized: bool, } /// VULNERABLE: Public init can be called by anyone public entry fun initialize( fee_bps: u64, treasury: address, ctx: &amp;mut TxContext ) { let state = ProtocolState { id: object::new(ctx), admin: tx_context::sender(ctx), // Caller becomes admin! fee_bps, treasury, initialized: true, }; transfer::share_object(state); } /// VULNERABLE: Boolean check can be bypassed public entry fun reinitialize( state: &amp;mut ProtocolState, new_admin: address, ctx: &amp;mut TxContext ) { // Attacker sets initialized = false first // Then calls reinitialize assert!(!state.initialized, E_ALREADY_INITIALIZED); state.admin = new_admin; state.initialized = true; } /// VULNERABLE: Allows resetting initialized flag public entry fun reset(state: &amp;mut ProtocolState) { // Anyone can reset, then reinitialize state.initialized = false; } } module vulnerable::token { use sui::coin::{Self, TreasuryCap}; /// VULNERABLE: TreasuryCap created in callable function public fun create_currency&lt;T: drop&gt;( witness: T, ctx: &amp;mut TxContext ): TreasuryCap&lt;T&gt; { // If witness type has `drop`, attacker can call this let (treasury_cap, metadata) = coin::create_currency( witness, 9, b&#34;VULN&#34;, b&#34;Vulnerable Token&#34;, b&#34;&#34;, option::none(), ctx ); transfer::public_freeze_object(metadata); treasury_cap // Attacker gets minting rights! } } Attack Scenario // Attacker sees protocol deployed without calling init module attack::takeover { use vulnerable::protocol; public entry fun exploit(ctx: &amp;mut TxContext) { // Attacker becomes admin protocol::initialize( 9999, // Max fees @attacker, // Treasury to attacker ctx ); } } Secure Example module secure::protocol { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; use sui::package; /// One-Time Witness — can only be used in init public struct PROTOCOL has drop {} public struct ProtocolState has key { id: UID, admin: address, fee_bps: u64, treasury: address, } /// SECURE: Admin capability created at init public struct AdminCap has key { id: UID, protocol_id: ID, } /// SECURE: Called exactly once at package publish fun init(witness: PROTOCOL, ctx: &amp;mut TxContext) { // Create publisher for package let publisher = package::claim(witness, ctx); let state = ProtocolState { id: object::new(ctx), admin: tx_context::sender(ctx), fee_bps: 100, // Default fee treasury: tx_context::sender(ctx), }; let state_id = object::id(&amp;state); let admin_cap = AdminCap { id: object::new(ctx), protocol_id: state_id, }; transfer::share_object(state); transfer::transfer(admin_cap, tx_context::sender(ctx)); transfer::public_transfer(publisher, tx_context::sender(ctx)); } /// SECURE: Admin updates require capability public entry fun update_config( cap: &amp;AdminCap, state: &amp;mut ProtocolState, new_fee_bps: u64, new_treasury: address, ) { assert!(cap.protocol_id == object::id(state), E_WRONG_PROTOCOL); assert!(new_fee_bps &lt;= 1000, E_FEE_TOO_HIGH); // Max 10% state.fee_bps = new_fee_bps; state.treasury = new_treasury; } /// SECURE: No reinitialize function exists // Initialization happens exactly once in init() } module secure::token { use sui::coin::{Self, TreasuryCap, CoinMetadata}; use sui::tx_context::TxContext; use sui::transfer; /// SECURE: One-Time Witness public struct MY_TOKEN has drop {} /// SECURE: Called once at publish fun init(witness: MY_TOKEN, ctx: &amp;mut TxContext) { let (treasury_cap, metadata) = coin::create_currency( witness, 9, b&#34;MYT&#34;, b&#34;My Token&#34;, b&#34;A secure token&#34;, option::none(), ctx ); // Freeze metadata transfer::public_freeze_object(metadata); // Transfer cap to deployer only transfer::transfer(treasury_cap, tx_context::sender(ctx)); } // No public function to create additional currencies } Initialization Patterns Pattern 1: One-Time Witness (OTW) /// The module name, in CAPS, with `drop` ability only public struct MY_MODULE has drop {} /// init receives the OTW and can only be called once fun init(witness: MY_MODULE, ctx: &amp;mut TxContext) { // Guaranteed to run exactly once at publish // witness cannot be created elsewhere } Pattern 2: Package Publisher use sui::package::{Self, Publisher}; fun init(otw: MY_MODULE, ctx: &amp;mut TxContext) { let publisher = package::claim(otw, ctx); // Publisher proves package ownership transfer::transfer(publisher, tx_context::sender(ctx)); } Pattern 3: Capability Created at Init fun init(ctx: &amp;mut TxContext) { let admin_cap = AdminCap { id: object::new(ctx) }; // Cap only exists because init was called // Cannot be recreated transfer::transfer(admin_cap, tx_context::sender(ctx)); } Pattern 4: Post-Deploy Configuration (Safe) /// State created in init, configured later public struct PendingSetup has key { id: UID, deployer: address, setup_deadline: u64, } fun init(ctx: &amp;mut TxContext) { transfer::share_object(PendingSetup { id: object::new(ctx), deployer: tx_context::sender(ctx), setup_deadline: 0, // Set during first setup }); } /// SECURE: Only deployer, only once, with deadline public entry fun complete_setup( pending: PendingSetup, config: SetupConfig, clock: &amp;Clock, ctx: &amp;TxContext ) { let PendingSetup { id, deployer, setup_deadline } = pending; assert!(tx_context::sender(ctx) == deployer, E_NOT_DEPLOYER); if (setup_deadline &gt; 0) { assert!(clock::timestamp_ms(clock) &lt; setup_deadline, E_SETUP_EXPIRED); }; object::delete(id); // Create actual protocol state let state = ProtocolState { /* ... */ }; transfer::share_object(state); } Recommended Mitigations 1. Always Use the init Function /// This is the ONLY safe way to initialize fun init(ctx: &amp;mut TxContext) { // Called exactly once at publish } 2. Use One-Time Witness for Important Setup public struct MY_PROTOCOL has drop {} fun init(witness: MY_PROTOCOL, ctx: &amp;mut TxContext) { // witness guarantees single execution } 3. Never Provide Public Initialize Functions // BAD: Anyone can call public fun initialize(...) { } // BAD: Entry doesn&#39;t help public entry fun initialize(...) { } // GOOD: Use init only fun init(ctx: &amp;mut TxContext) { } 4. Create Capabilities at Init Time fun init(ctx: &amp;mut TxContext) { // Admin cap created here cannot be recreated let cap = AdminCap { id: object::new(ctx) }; transfer::transfer(cap, tx_context::sender(ctx)); } Testing Checklist Verify init() is the only initialization function Confirm no public initialize/setup functions exist Test that OTW cannot be created outside init Verify state cannot be reinitialized after init Confirm capabilities created in init cannot be recreated Related Vulnerabilities Access-Control Mistakes Capability Leakage Object Transfer Misuse</description>
    </item>
    <item>
      <title>21. Oracle Validation Failures</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/oracle-validation-failures/index.html</link>
      <pubDate>Wed, 26 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/oracle-validation-failures/index.html</guid>
      <description>Overview Oracle validation failures occur when smart contracts blindly trust off-chain data sources without proper verification. In Sui Move, oracles provide critical data like prices, random numbers, or external state, but improper validation can lead to manipulation, stale data exploitation, or complete protocol compromise.&#xA;Risk Level Critical — Can lead to significant financial losses, especially in DeFi protocols.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A08 (Software and Data Integrity Failures) CWE-345 (Insufficient Verification of Data Authenticity), CWE-353 (Missing Support for Integrity Check) The Problem Common Oracle Trust Issues Issue Risk Description No staleness check High Using outdated prices that no longer reflect market No source verification Critical Accepting data from untrusted oracles No price bounds High Accepting unrealistic price values Single oracle dependency Medium No fallback if oracle fails No signature verification Critical Accepting unsigned or improperly signed data Vulnerable Example module vulnerable::lending { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; use sui::coin::{Self, Coin}; use sui::balance::{Self, Balance}; const E_INSUFFICIENT_COLLATERAL: u64 = 1; public struct LendingPool&lt;phantom T&gt; has key { id: UID, total_deposits: Balance&lt;T&gt;, oracle: address, // Just stores an address! } public struct PriceData has copy, drop { asset: vector&lt;u8&gt;, price: u64, timestamp: u64, } /// VULNERABLE: No validation of oracle data public entry fun borrow&lt;T, C&gt;( pool: &amp;mut LendingPool&lt;T&gt;, collateral: Coin&lt;C&gt;, borrow_amount: u64, price_data: PriceData, // Anyone can pass any price! ctx: &amp;mut TxContext ) { let collateral_value = coin::value(&amp;collateral); // VULNERABLE: No check on who provided price_data // VULNERABLE: No check if price_data is stale // VULNERABLE: No signature verification let required_collateral = (borrow_amount * 150) / price_data.price; assert!(collateral_value &gt;= required_collateral, E_INSUFFICIENT_COLLATERAL); // Process borrow... } /// VULNERABLE: Oracle address can be set by anyone public entry fun set_oracle&lt;T&gt;( pool: &amp;mut LendingPool&lt;T&gt;, new_oracle: address, _ctx: &amp;mut TxContext ) { // No access control! pool.oracle = new_oracle; } } module vulnerable::price_oracle { use sui::object::{Self, UID}; use sui::tx_context::TxContext; public struct PriceFeed has key { id: UID, price: u64, last_update: u64, } /// VULNERABLE: Anyone can update the price public entry fun update_price( feed: &amp;mut PriceFeed, new_price: u64, _ctx: &amp;mut TxContext ) { // No access control! // No validation of price bounds! feed.price = new_price; feed.last_update = 0; // Timestamp not even set properly } /// VULNERABLE: Returns stale data without warning public fun get_price(feed: &amp;PriceFeed): u64 { // No staleness check! feed.price } } Attack Scenario module attack::oracle_manipulation { use vulnerable::lending; use vulnerable::price_oracle; /// Attacker manipulates price to undercollateralize public entry fun exploit(ctx: &amp;mut TxContext) { // Step 1: Create fake price data with inflated collateral price let fake_price = lending::PriceData { asset: b&#34;ETH&#34;, price: 1_000_000_000, // Massively inflated price timestamp: 0, }; // Step 2: Use tiny collateral to borrow huge amounts // With price of 1B, $1 of collateral = $1B value // lending::borrow(..., fake_price, ...); } } Secure Example module secure::oracle { use sui::object::{Self, UID, ID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; use sui::clock::{Self, Clock}; use sui::ed25519; use sui::bcs; const E_INVALID_SIGNATURE: u64 = 1; const E_STALE_PRICE: u64 = 2; const E_PRICE_OUT_OF_BOUNDS: u64 = 3; const E_WRONG_ASSET: u64 = 4; const E_INVALID_ORACLE: u64 = 5; const MAX_STALENESS_MS: u64 = 60_000; // 1 minute const MIN_PRICE: u64 = 1; const MAX_PRICE: u64 = 1_000_000_000_000; // $1T max public struct OracleRegistry has key { id: UID, trusted_oracles: vector&lt;vector&lt;u8&gt;&gt;, // Public keys min_confirmations: u64, } public struct AdminCap has key { id: UID, registry_id: ID, } public struct PriceFeed has key { id: UID, asset: vector&lt;u8&gt;, price: u64, last_update_ms: u64, oracle_pubkey: vector&lt;u8&gt;, } public struct SignedPriceData has copy, drop { asset: vector&lt;u8&gt;, price: u64, timestamp_ms: u64, signature: vector&lt;u8&gt;, oracle_pubkey: vector&lt;u8&gt;, } /// SECURE: Only admin can manage oracle registry fun init(ctx: &amp;mut TxContext) { let registry = OracleRegistry { id: object::new(ctx), trusted_oracles: vector::empty(), min_confirmations: 1, }; let registry_id = object::id(&amp;registry); let admin_cap = AdminCap { id: object::new(ctx), registry_id, }; transfer::share_object(registry); transfer::transfer(admin_cap, tx_context::sender(ctx)); } /// SECURE: Admin-controlled oracle registration public entry fun register_oracle( cap: &amp;AdminCap, registry: &amp;mut OracleRegistry, oracle_pubkey: vector&lt;u8&gt;, ) { assert!(cap.registry_id == object::id(registry), E_INVALID_ORACLE); vector::push_back(&amp;mut registry.trusted_oracles, oracle_pubkey); } /// SECURE: Validates signature, staleness, and bounds public fun validate_and_get_price( registry: &amp;OracleRegistry, signed_data: &amp;SignedPriceData, expected_asset: vector&lt;u8&gt;, clock: &amp;Clock, ): u64 { // 1. Verify the oracle is trusted let is_trusted = is_oracle_trusted(registry, &amp;signed_data.oracle_pubkey); assert!(is_trusted, E_INVALID_ORACLE); // 2. Verify signature let message = create_price_message( &amp;signed_data.asset, signed_data.price, signed_data.timestamp_ms ); let valid_sig = ed25519::ed25519_verify( &amp;signed_data.signature, &amp;signed_data.oracle_pubkey, &amp;message ); assert!(valid_sig, E_INVALID_SIGNATURE); // 3. Check staleness let current_time = clock::timestamp_ms(clock); let age = current_time - signed_data.timestamp_ms; assert!(age &lt;= MAX_STALENESS_MS, E_STALE_PRICE); // 4. Verify asset matches assert!(signed_data.asset == expected_asset, E_WRONG_ASSET); // 5. Check price bounds assert!(signed_data.price &gt;= MIN_PRICE, E_PRICE_OUT_OF_BOUNDS); assert!(signed_data.price &lt;= MAX_PRICE, E_PRICE_OUT_OF_BOUNDS); signed_data.price } fun is_oracle_trusted( registry: &amp;OracleRegistry, pubkey: &amp;vector&lt;u8&gt; ): bool { let len = vector::length(&amp;registry.trusted_oracles); let mut i = 0; while (i &lt; len) { if (*vector::borrow(&amp;registry.trusted_oracles, i) == *pubkey) { return true }; i = i + 1; }; false } fun create_price_message( asset: &amp;vector&lt;u8&gt;, price: u64, timestamp: u64 ): vector&lt;u8&gt; { let mut msg = vector::empty&lt;u8&gt;(); vector::append(&amp;mut msg, *asset); vector::append(&amp;mut msg, bcs::to_bytes(&amp;price)); vector::append(&amp;mut msg, bcs::to_bytes(&amp;timestamp)); msg } } module secure::lending { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::coin::{Self, Coin}; use sui::balance::{Self, Balance}; use sui::clock::Clock; use secure::oracle::{Self, OracleRegistry, SignedPriceData}; const E_INSUFFICIENT_COLLATERAL: u64 = 1; const COLLATERAL_RATIO_BPS: u64 = 15000; // 150% public struct LendingPool&lt;phantom T&gt; has key { id: UID, total_deposits: Balance&lt;T&gt;, collateral_asset: vector&lt;u8&gt;, borrow_asset: vector&lt;u8&gt;, } /// SECURE: Uses validated oracle data public entry fun borrow&lt;T, C&gt;( pool: &amp;mut LendingPool&lt;T&gt;, registry: &amp;OracleRegistry, collateral: Coin&lt;C&gt;, borrow_amount: u64, collateral_price_data: SignedPriceData, borrow_price_data: SignedPriceData, clock: &amp;Clock, ctx: &amp;mut TxContext ) { // Get validated prices let collateral_price = oracle::validate_and_get_price( registry, &amp;collateral_price_data, pool.collateral_asset, clock ); let borrow_price = oracle::validate_and_get_price( registry, &amp;borrow_price_data, pool.borrow_asset, clock ); // Calculate collateral requirement with validated prices let collateral_value = coin::value(&amp;collateral) * collateral_price; let borrow_value = borrow_amount * borrow_price; let required_collateral_value = (borrow_value * COLLATERAL_RATIO_BPS) / 10000; assert!(collateral_value &gt;= required_collateral_value, E_INSUFFICIENT_COLLATERAL); // Process borrow safely... } } Oracle Integration Patterns Pattern 1: Pyth Network Integration module example::pyth_consumer { use pyth::price::{Self, Price}; use pyth::price_feed::{Self, PriceFeed}; use pyth::pyth; use sui::clock::Clock; const E_STALE_PRICE: u64 = 1; const E_NEGATIVE_PRICE: u64 = 2; const MAX_AGE_SECONDS: u64 = 60; public fun get_validated_price( price_feed: &amp;PriceFeed, clock: &amp;Clock, ): u64 { let price = price_feed::get_price(price_feed); // Check price age let price_timestamp = price::get_timestamp(&amp;price); let current_time = clock::timestamp_ms(clock) / 1000; assert!(current_time - price_timestamp &lt;= MAX_AGE_SECONDS, E_STALE_PRICE); // Get price value (handle negative prices) let price_i64 = price::get_price(&amp;price); assert!(price_i64 &gt; 0, E_NEGATIVE_PRICE); (price_i64 as u64) } } Pattern 2: Multi-Oracle Aggregation module secure::aggregated_oracle { use sui::clock::Clock; const E_INSUFFICIENT_ORACLES: u64 = 1; const E_PRICE_DEVIATION_TOO_HIGH: u64 = 2; const MAX_DEVIATION_BPS: u64 = 500; // 5% public struct AggregatedPrice has copy, drop { median_price: u64, num_sources: u64, timestamp_ms: u64, } /// Aggregate multiple oracle prices with deviation check public fun aggregate_prices( prices: vector&lt;u64&gt;, clock: &amp;Clock, min_sources: u64, ): AggregatedPrice { let num_prices = vector::length(&amp;prices); assert!(num_prices &gt;= min_sources, E_INSUFFICIENT_ORACLES); // Sort prices to find median let sorted = sort_prices(prices); let median_price = *vector::borrow(&amp;sorted, num_prices / 2); // Check all prices are within acceptable deviation let mut i = 0; while (i &lt; num_prices) { let price = *vector::borrow(&amp;sorted, i); let deviation = calculate_deviation(price, median_price); assert!(deviation &lt;= MAX_DEVIATION_BPS, E_PRICE_DEVIATION_TOO_HIGH); i = i + 1; }; AggregatedPrice { median_price, num_sources: num_prices, timestamp_ms: clock::timestamp_ms(clock), } } fun calculate_deviation(price: u64, median: u64): u64 { if (price &gt; median) { ((price - median) * 10000) / median } else { ((median - price) * 10000) / median } } fun sort_prices(prices: vector&lt;u64&gt;): vector&lt;u64&gt; { // Implementation of sorting algorithm prices // Simplified } } Pattern 3: Heartbeat Monitoring module secure::heartbeat_oracle { use sui::object::{Self, UID}; use sui::clock::{Self, Clock}; const E_ORACLE_DEAD: u64 = 1; const HEARTBEAT_INTERVAL_MS: u64 = 30_000; // 30 seconds public struct HeartbeatOracle has key { id: UID, price: u64, last_heartbeat_ms: u64, is_active: bool, } /// Check oracle health before using public fun is_oracle_healthy( oracle: &amp;HeartbeatOracle, clock: &amp;Clock, ): bool { if (!oracle.is_active) { return false }; let current_time = clock::timestamp_ms(clock); let time_since_heartbeat = current_time - oracle.last_heartbeat_ms; time_since_heartbeat &lt;= HEARTBEAT_INTERVAL_MS } public fun get_price_if_healthy( oracle: &amp;HeartbeatOracle, clock: &amp;Clock, ): u64 { assert!(is_oracle_healthy(oracle, clock), E_ORACLE_DEAD); oracle.price } } Recommended Mitigations 1. Always Verify Oracle Signatures // Verify the data comes from a trusted oracle let valid = ed25519::ed25519_verify( &amp;signature, &amp;trusted_pubkey, &amp;message ); assert!(valid, E_INVALID_SIGNATURE); 2. Check Data Freshness // Never use stale data let age = current_time - data_timestamp; assert!(age &lt;= MAX_STALENESS, E_STALE_DATA); 3. Validate Price Bounds // Sanity check price values assert!(price &gt;= MIN_REASONABLE_PRICE, E_PRICE_TOO_LOW); assert!(price &lt;= MAX_REASONABLE_PRICE, E_PRICE_TOO_HIGH); 4. Use Multiple Oracle Sources // Aggregate from multiple sources let prices = get_prices_from_multiple_oracles(); let median = calculate_median(prices); 5. Implement Circuit Breakers // Pause on extreme price movements let price_change = calculate_change(old_price, new_price); if (price_change &gt; MAX_CHANGE_THRESHOLD) { pause_protocol(); emit_alert(); } Testing Checklist Verify signature validation cannot be bypassed Test with stale price data — should be rejected Test with prices outside bounds — should be rejected Test with untrusted oracle addresses — should be rejected Verify fallback behavior when primary oracle fails Test multi-oracle aggregation with malicious minority Verify circuit breakers trigger on extreme price movements Related Vulnerabilities Access-Control Mistakes Clock Time Misuse Unsafe BCS Parsing Weak Initializers</description>
    </item>
    <item>
      <title>22. Unsafe Option Authority</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/unsafe-option-authority/index.html</link>
      <pubDate>Wed, 26 Nov 2025 20:48:47 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/unsafe-option-authority/index.html</guid>
      <description>Overview Unsafe Option authority occurs when developers use Option&lt;T&gt; types to toggle permissions or authority states, creating vulnerabilities where attackers can manipulate authorization by extracting, replacing, or exploiting the optional nature of authority objects. This pattern is particularly dangerous when capabilities or access tokens are wrapped in Option types.&#xA;Risk Level High — Can lead to privilege escalation or unauthorized access.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A04 (Insecure Design) CWE-696 (Incorrect Behavior Order), CWE-693 (Protection Mechanism Failure) The Problem Common Option Authority Issues Issue Risk Description Mutable Option capability Critical Authority can be extracted and replaced None as “no permission” High Missing != denied, logic can be bypassed Option in shared objects High Race conditions on authority state fill/extract patterns Medium Authority can be temporarily removed Vulnerable Example module vulnerable::vault { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; use sui::coin::{Self, Coin}; use sui::balance::{Self, Balance}; use std::option::{Self, Option}; const E_NOT_AUTHORIZED: u64 = 1; const E_NO_ADMIN: u64 = 2; public struct AdminCap has key, store { id: UID, } public struct Vault&lt;phantom T&gt; has key { id: UID, balance: Balance&lt;T&gt;, /// VULNERABLE: Admin stored as Option in shared object admin_cap: Option&lt;AdminCap&gt;, withdraw_enabled: bool, } /// VULNERABLE: Admin can be extracted public fun extract_admin(vault: &amp;mut Vault&lt;SUI&gt;): AdminCap { assert!(option::is_some(&amp;vault.admin_cap), E_NO_ADMIN); option::extract(&amp;mut vault.admin_cap) } /// VULNERABLE: Anyone can fill when empty public fun set_admin(vault: &amp;mut Vault&lt;SUI&gt;, cap: AdminCap) { // If admin was extracted, anyone can become admin! assert!(option::is_none(&amp;vault.admin_cap), E_NOT_AUTHORIZED); option::fill(&amp;mut vault.admin_cap, cap); } /// VULNERABLE: Check passes when Option is None public entry fun emergency_withdraw&lt;T&gt;( vault: &amp;mut Vault&lt;T&gt;, ctx: &amp;mut TxContext ) { // Attacker extracts admin, making this None // Then this check becomes meaningless if (option::is_some(&amp;vault.admin_cap)) { // Only check if admin exists - but it was extracted! let admin = option::borrow(&amp;vault.admin_cap); // No actual verification of caller }; // Withdraw proceeds even without proper auth let amount = balance::value(&amp;vault.balance); let coins = coin::take(&amp;mut vault.balance, amount, ctx); transfer::public_transfer(coins, tx_context::sender(ctx)); } } module vulnerable::toggle_auth { use sui::object::{Self, UID}; use std::option::{Self, Option}; public struct Permission has store, drop {} public struct Resource has key { id: UID, /// VULNERABLE: Permission as toggle permission: Option&lt;Permission&gt;, data: vector&lt;u8&gt;, } /// VULNERABLE: Toggle-based auth can be manipulated public fun modify_if_permitted( resource: &amp;mut Resource, new_data: vector&lt;u8&gt; ) { // Attacker can swap in their own Permission if (option::is_some(&amp;resource.permission)) { resource.data = new_data; } // Also: if Permission has `drop`, it can be destroyed // leaving permanent &#34;no permission&#34; state } /// VULNERABLE: Permission can be stolen via swap public fun swap_permission( resource: &amp;mut Resource, new_perm: Permission ): Option&lt;Permission&gt; { // Returns the old permission to caller! let old = option::swap(&amp;mut resource.permission, new_perm); option::some(old) } } Attack Scenario module attack::option_exploit { use vulnerable::vault::{Self, Vault, AdminCap}; use sui::tx_context::TxContext; /// Step 1: Extract admin during legitimate operation public fun steal_admin(vault: &amp;mut Vault&lt;SUI&gt;): AdminCap { // If we can call extract, we become the admin holder vault::extract_admin(vault) } /// Step 2: Vault now has no admin, emergency_withdraw check fails open public entry fun drain_vault( vault: &amp;mut Vault&lt;SUI&gt;, ctx: &amp;mut TxContext ) { // Admin check sees None, doesn&#39;t properly deny access vault::emergency_withdraw(vault, ctx); } /// Alternative: Front-run admin reinsertion public entry fun become_admin( vault: &amp;mut Vault&lt;SUI&gt;, ctx: &amp;mut TxContext ) { // Create our own admin cap let fake_admin = AdminCap { id: object::new(ctx) }; // Race to fill the empty slot vault::set_admin(vault, fake_admin); } } Secure Example module secure::vault { use sui::object::{Self, UID, ID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; use sui::coin::{Self, Coin}; use sui::balance::{Self, Balance}; const E_NOT_ADMIN: u64 = 1; const E_WRONG_VAULT: u64 = 2; /// SECURE: Capability is a separate object, not embedded public struct AdminCap has key { id: UID, vault_id: ID, // Bound to specific vault } public struct Vault&lt;phantom T&gt; has key { id: UID, balance: Balance&lt;T&gt;, admin: address, // Store admin address, not capability withdraw_enabled: bool, } fun init(ctx: &amp;mut TxContext) { let vault = Vault { id: object::new(ctx), balance: balance::zero(), admin: tx_context::sender(ctx), withdraw_enabled: true, }; let vault_id = object::id(&amp;vault); let admin_cap = AdminCap { id: object::new(ctx), vault_id, }; transfer::share_object(vault); transfer::transfer(admin_cap, tx_context::sender(ctx)); } /// SECURE: Requires capability proof, not Option check public entry fun emergency_withdraw&lt;T&gt;( cap: &amp;AdminCap, vault: &amp;mut Vault&lt;T&gt;, ctx: &amp;mut TxContext ) { // Verify cap matches vault assert!(cap.vault_id == object::id(vault), E_WRONG_VAULT); let amount = balance::value(&amp;vault.balance); let coins = coin::take(&amp;mut vault.balance, amount, ctx); transfer::public_transfer(coins, tx_context::sender(ctx)); } /// SECURE: Transfer admin to new address public entry fun transfer_admin( cap: AdminCap, vault: &amp;mut Vault&lt;SUI&gt;, new_admin: address, ctx: &amp;mut TxContext ) { assert!(cap.vault_id == object::id(vault), E_WRONG_VAULT); vault.admin = new_admin; transfer::transfer(cap, new_admin); } } module secure::optional_feature { use sui::object::{Self, UID, ID}; use sui::tx_context::{Self, TxContext}; use std::option::{Self, Option}; const E_FEATURE_DISABLED: u64 = 1; const E_NOT_OWNER: u64 = 2; /// SECURE: Feature flag is a simple bool, not authority public struct FeatureConfig has key { id: UID, owner: address, premium_enabled: bool, max_operations: Option&lt;u64&gt;, // Optional limit, not authority } /// SECURE: Authority check is separate from optional config public entry fun use_premium_feature( config: &amp;FeatureConfig, ctx: &amp;TxContext ) { // First: verify ownership (authority) assert!(tx_context::sender(ctx) == config.owner, E_NOT_OWNER); // Then: check feature flag (configuration) assert!(config.premium_enabled, E_FEATURE_DISABLED); // Optional config affects behavior, not authorization let limit = if (option::is_some(&amp;config.max_operations)) { *option::borrow(&amp;config.max_operations) } else { 1000 // Default limit }; // Proceed with operation... } } Safe Option Patterns Pattern 1: Option for Optional Data, Not Authority module safe::optional_data { use std::option::{Self, Option}; public struct UserProfile has key { id: UID, owner: address, // Authority: non-optional name: vector&lt;u8&gt;, // Required field bio: Option&lt;vector&lt;u8&gt;&gt;, // Optional data - OK avatar_url: Option&lt;vector&lt;u8&gt;&gt;, // Optional data - OK } /// Safe: Option used for optional data, not permissions public fun get_bio(profile: &amp;UserProfile): Option&lt;vector&lt;u8&gt;&gt; { profile.bio } /// Safe: Authority check uses address, not Option public fun update_bio( profile: &amp;mut UserProfile, new_bio: Option&lt;vector&lt;u8&gt;&gt;, ctx: &amp;TxContext ) { assert!(tx_context::sender(ctx) == profile.owner, E_NOT_OWNER); profile.bio = new_bio; } } Pattern 2: Capability References, Never Embedded Options module safe::capability_pattern { use sui::object::{Self, UID, ID}; public struct AdminCap has key { id: UID, resource_id: ID, } public struct ManagedResource has key { id: UID, // NO Option&lt;AdminCap&gt; here! // Admin holds cap separately data: vector&lt;u8&gt;, } /// Capability passed as reference, not extracted from Option public fun admin_modify( cap: &amp;AdminCap, resource: &amp;mut ManagedResource, new_data: vector&lt;u8&gt; ) { assert!(cap.resource_id == object::id(resource), E_WRONG_RESOURCE); resource.data = new_data; } } Pattern 3: Immutable Authority with Optional Delegation module safe::delegation { use sui::object::{Self, UID, ID}; use std::option::{Self, Option}; public struct PrimaryAdmin has key { id: UID, resource_id: ID, } public struct DelegatedAdmin has key { id: UID, resource_id: ID, delegated_by: ID, expires_at: u64, } public struct Resource has key { id: UID, primary_admin: address, // Immutable primary authority // Delegation is separate objects, not Options data: vector&lt;u8&gt;, } /// Primary admin always works public fun primary_modify( cap: &amp;PrimaryAdmin, resource: &amp;mut Resource, new_data: vector&lt;u8&gt; ) { assert!(cap.resource_id == object::id(resource), E_WRONG_RESOURCE); resource.data = new_data; } /// Delegated admin requires valid, non-expired delegation public fun delegated_modify( cap: &amp;DelegatedAdmin, resource: &amp;mut Resource, new_data: vector&lt;u8&gt;, clock: &amp;Clock ) { assert!(cap.resource_id == object::id(resource), E_WRONG_RESOURCE); assert!(clock::timestamp_ms(clock) &lt; cap.expires_at, E_DELEGATION_EXPIRED); resource.data = new_data; } } Pattern 4: Option for Grace Periods, Not Access module safe::grace_period { use std::option::{Self, Option}; use sui::clock::{Self, Clock}; public struct Subscription has key { id: UID, owner: address, active: bool, expires_at: u64, grace_period_end: Option&lt;u64&gt;, // Optional extension, not authority } public fun can_access( sub: &amp;Subscription, clock: &amp;Clock, ctx: &amp;TxContext ): bool { // Authority: must be owner if (tx_context::sender(ctx) != sub.owner) { return false }; if (!sub.active) { return false }; let now = clock::timestamp_ms(clock); // Active subscription if (now &lt; sub.expires_at) { return true }; // Check grace period (optional feature, not authority) if (option::is_some(&amp;sub.grace_period_end)) { let grace_end = *option::borrow(&amp;sub.grace_period_end); return now &lt; grace_end }; false } } Recommended Mitigations 1. Never Store Capabilities in Option // BAD: Capability in Option can be extracted public struct Bad has key { admin: Option&lt;AdminCap&gt;, } // GOOD: Capability is separate object public struct Good has key { admin_address: address, } 2. Use Address or ID for Authority Reference // Store who has authority, not the authority itself public struct Resource has key { id: UID, owner: address, admin_cap_id: ID, // Reference, not embedded } 3. Require Capability Proof, Not Option Check // BAD: Check if Option contains value if (option::is_some(&amp;resource.admin)) { ... } // GOOD: Require capability as parameter public fun admin_action(cap: &amp;AdminCap, resource: &amp;mut Resource) { assert!(cap.resource_id == object::id(resource), E_WRONG_CAP); } 4. Use Option Only for Optional Data // GOOD: Optional configuration data public struct Config has key { max_limit: Option&lt;u64&gt;, // Optional setting description: Option&lt;String&gt;, // Optional metadata } Testing Checklist Verify no capabilities are stored in Option types Test that extracting optional values doesn’t bypass auth Confirm authority checks don’t rely on Option::is_some Test race conditions on shared objects with Option fields Verify None state doesn’t grant unexpected access Check that swap/extract patterns can’t steal authority Related Vulnerabilities Capability Leakage Access-Control Mistakes Ability Misconfiguration Shared Object DoS</description>
    </item>
    <item>
      <title>23. Clock Time Misuse</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/clock-time-misuse/index.html</link>
      <pubDate>Wed, 26 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/clock-time-misuse/index.html</guid>
      <description>Overview Clock time misuse occurs when smart contracts improperly use Sui’s Clock object for time-sensitive operations. Unlike traditional blockchains where block timestamps can be manipulated by validators, Sui provides a system Clock object with millisecond precision. However, misusing this clock—through incorrect comparisons, time zone assumptions, or precision errors—can lead to serious vulnerabilities.&#xA;Risk Level High — Can lead to premature unlocks, expired deadlines being bypassed, or time-locked funds becoming inaccessible.</description>
    </item>
    <item>
      <title>24. Transfer API Misuse</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/transfer-api-misuse/index.html</link>
      <pubDate>Wed, 26 Nov 2025 21:02:54 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/transfer-api-misuse/index.html</guid>
      <description>Overview Transfer API misuse occurs when developers incorrectly use Sui’s object transfer functions, leading to objects being sent to wrong addresses, locked permanently, or transferred with incorrect ownership semantics. Sui provides multiple transfer functions (transfer::transfer, transfer::public_transfer, transfer::share_object, transfer::freeze_object) each with specific requirements and behaviors that must be understood.&#xA;Risk Level Critical — Can result in permanent loss of assets or complete protocol failure.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A01 (Broken Access Control) CWE-284 (Improper Access Control) The Problem Common Transfer API Issues Issue Risk Description Using transfer without store Critical Compile error or runtime panic Sharing owned objects incorrectly High Can break ownership invariants Freezing mutable state Critical Permanently locks needed functionality Transfer to wrong address Critical Assets sent to unrecoverable address public_transfer vs transfer confusion High Security implications differ Transfer API Quick Reference Function Requires store Use Case transfer::transfer No Internal module transfers of objects without store transfer::public_transfer Yes External transfers of objects with store transfer::share_object No Making objects shared (accessible by anyone) transfer::public_share_object Yes External sharing of objects with store transfer::freeze_object No Making objects immutable transfer::public_freeze_object Yes External freezing of objects with store Vulnerable Example module vulnerable::token { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; use sui::coin::{Self, Coin}; const E_NOT_OWNER: u64 = 1; /// Has `store` ability - can use public_transfer public struct Token has key, store { id: UID, value: u64, } /// VULNERABLE: Transfers to sender without verification public entry fun claim_airdrop( amount: u64, ctx: &amp;mut TxContext ) { let token = Token { id: object::new(ctx), value: amount, }; // Who is sender? Could be anyone, including a contract // that can&#39;t receive objects transfer::public_transfer(token, tx_context::sender(ctx)); } /// VULNERABLE: Typo in address loses funds forever public entry fun send_to_treasury( token: Token, _ctx: &amp;mut TxContext ) { // Hardcoded address - typo = permanent loss transfer::public_transfer(token, @0x1234567890abcdef); } /// VULNERABLE: Shares object that should stay owned public entry fun make_accessible( token: Token, _ctx: &amp;mut TxContext ) { // Now ANYONE can access this token! transfer::share_object(token); } } module vulnerable::vault { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; use sui::balance::{Self, Balance}; /// Missing `store` ability public struct Vault&lt;phantom T&gt; has key { id: UID, balance: Balance&lt;T&gt;, owner: address, } /// VULNERABLE: Can&#39;t use public_transfer without `store` public entry fun transfer_vault&lt;T&gt;( vault: Vault&lt;T&gt;, new_owner: address, ctx: &amp;mut TxContext ) { // This will fail! Vault doesn&#39;t have `store` // transfer::public_transfer(vault, new_owner); // And using transfer from outside the module won&#39;t work either // Only this module can call transfer::transfer on Vault } /// VULNERABLE: Freezes vault, locking funds forever public entry fun secure_vault&lt;T&gt;( vault: Vault&lt;T&gt;, _ctx: &amp;mut TxContext ) { // Frozen = immutable forever = funds locked! transfer::freeze_object(vault); } } module vulnerable::nft { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; public struct NFT has key, store { id: UID, name: vector&lt;u8&gt;, } public struct NFTCollection has key { id: UID, nfts: vector&lt;NFT&gt;, // Owned NFTs inside collection } /// VULNERABLE: Shares collection, exposing all NFTs public entry fun publish_collection( collection: NFTCollection, _ctx: &amp;mut TxContext ) { // All NFTs in the collection are now accessible! transfer::share_object(collection); } /// VULNERABLE: No validation of recipient public entry fun gift_nft( nft: NFT, recipient: address, _ctx: &amp;mut TxContext ) { // What if recipient is @0x0? // What if recipient is a module address that can&#39;t hold objects? transfer::public_transfer(nft, recipient); } } Attack Scenario module attack::steal_shared { use vulnerable::token::{Self, Token}; use sui::tx_context::TxContext; /// After token is incorrectly shared, anyone can take it public entry fun steal_shared_token( token: &amp;mut Token, // Shared object = mutable by anyone ctx: &amp;mut TxContext ) { // Attacker can now manipulate the token // that was accidentally shared } } Secure Example module secure::token { use sui::object::{Self, UID, ID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; use sui::event; const E_ZERO_ADDRESS: u64 = 1; const E_SELF_TRANSFER: u64 = 2; const E_NOT_OWNER: u64 = 3; public struct Token has key, store { id: UID, value: u64, original_owner: address, } public struct TokenTransferred has copy, drop { token_id: ID, from: address, to: address, } /// SECURE: Validates recipient before transfer public entry fun safe_transfer( token: Token, recipient: address, ctx: &amp;mut TxContext ) { let sender = tx_context::sender(ctx); // Validate recipient assert!(recipient != @0x0, E_ZERO_ADDRESS); assert!(recipient != sender, E_SELF_TRANSFER); let token_id = object::id(&amp;token); // Emit event for tracking event::emit(TokenTransferred { token_id, from: sender, to: recipient, }); transfer::public_transfer(token, recipient); } /// SECURE: Treasury address as a verified constant const TREASURY: address = @0xTREASURY_ADDRESS_HERE; public entry fun send_to_treasury( token: Token, ctx: &amp;mut TxContext ) { let token_id = object::id(&amp;token); let sender = tx_context::sender(ctx); event::emit(TokenTransferred { token_id, from: sender, to: TREASURY, }); transfer::public_transfer(token, TREASURY); } } module secure::vault { use sui::object::{Self, UID, ID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; use sui::balance::{Self, Balance}; use sui::coin::{Self, Coin}; const E_NOT_OWNER: u64 = 1; const E_ZERO_ADDRESS: u64 = 2; const E_HAS_BALANCE: u64 = 3; /// Note: No `store` - transfers controlled by this module only public struct Vault&lt;phantom T&gt; has key { id: UID, balance: Balance&lt;T&gt;, owner: address, } /// SECURE: Module-controlled transfer with validation public entry fun transfer_vault&lt;T&gt;( vault: Vault&lt;T&gt;, new_owner: address, ctx: &amp;mut TxContext ) { // Verify current ownership assert!(vault.owner == tx_context::sender(ctx), E_NOT_OWNER); // Validate new owner assert!(new_owner != @0x0, E_ZERO_ADDRESS); // Update internal owner tracking let Vault { id, balance, owner: _ } = vault; let new_vault = Vault { id, balance, owner: new_owner, }; // Use transfer (not public_transfer) since no `store` transfer::transfer(new_vault, new_owner); } /// SECURE: Withdraw before destroying, never freeze with balance public entry fun close_vault&lt;T&gt;( vault: Vault&lt;T&gt;, ctx: &amp;mut TxContext ) { assert!(vault.owner == tx_context::sender(ctx), E_NOT_OWNER); let Vault { id, balance, owner } = vault; // Return any remaining balance to owner if (balance::value(&amp;balance) &gt; 0) { let coins = coin::from_balance(balance, ctx); transfer::public_transfer(coins, owner); } else { balance::destroy_zero(balance); }; object::delete(id); // Vault is properly destroyed, not frozen with funds } } module secure::nft { use sui::object::{Self, UID, ID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; use sui::dynamic_object_field as dof; const E_NOT_OWNER: u64 = 1; const E_ZERO_ADDRESS: u64 = 2; public struct NFT has key, store { id: UID, name: vector&lt;u8&gt;, creator: address, } /// Collection owns NFTs via dynamic fields, not vector public struct NFTCollection has key { id: UID, owner: address, nft_count: u64, } /// SECURE: Share collection but NFTs remain owned separately public entry fun create_collection( ctx: &amp;mut TxContext ) { let collection = NFTCollection { id: object::new(ctx), owner: tx_context::sender(ctx), nft_count: 0, }; // Sharing collection is safe - it only contains metadata // Individual NFTs are transferred separately transfer::share_object(collection); } /// SECURE: Add NFT to collection as dynamic field public entry fun add_to_collection( collection: &amp;mut NFTCollection, nft: NFT, ctx: &amp;mut TxContext ) { assert!(collection.owner == tx_context::sender(ctx), E_NOT_OWNER); let nft_id = object::id(&amp;nft); dof::add(&amp;mut collection.id, nft_id, nft); collection.nft_count = collection.nft_count + 1; } /// SECURE: Validated gift with proper checks public entry fun gift_nft( nft: NFT, recipient: address, ctx: &amp;mut TxContext ) { // Validate recipient assert!(recipient != @0x0, E_ZERO_ADDRESS); // Additional validation could include: // - Checking recipient is not a known module address // - Requiring recipient acknowledgment via separate flow transfer::public_transfer(nft, recipient); } } Transfer Pattern Guidelines Pattern 1: Choosing the Right Transfer Function /// Object WITHOUT store - use transfer (module-only) public struct InternalAsset has key { id: UID, } /// Object WITH store - can use public_transfer public struct TransferableAsset has key, store { id: UID, } /// Correct usage: fun transfer_internal(asset: InternalAsset, recipient: address) { // Only callable from within this module transfer::transfer(asset, recipient); } public entry fun transfer_external(asset: TransferableAsset, recipient: address) { // Callable from PTBs or other modules transfer::public_transfer(asset, recipient); } Pattern 2: Safe Sharing Decisions /// SAFE to share: Configuration/registry with no sensitive data public struct PublicRegistry has key { id: UID, entries: Table&lt;ID, RegistryEntry&gt;, } /// UNSAFE to share: Objects containing value or authority public struct Vault has key { id: UID, balance: Balance&lt;SUI&gt;, // Don&#39;t share! } /// When in doubt, keep objects owned public fun should_share(obj_type: &amp;str): bool { // Share: registries, oracles, public state // Don&#39;t share: vaults, tokens, capabilities, user assets false // Default to not sharing } Pattern 3: Freeze vs Delete /// Use freeze for truly immutable data public struct ImmutableMetadata has key { id: UID, name: vector&lt;u8&gt;, image_url: vector&lt;u8&gt;, // No balance, no mutable state } public entry fun publish_metadata( metadata: ImmutableMetadata, _ctx: &amp;mut TxContext ) { // Safe to freeze - no funds, metadata is permanent transfer::freeze_object(metadata); } /// Use delete for objects with value public entry fun close_account( vault: Vault, ctx: &amp;mut TxContext ) { // Withdraw value first, then delete let Vault { id, balance } = vault; let coins = coin::from_balance(balance, ctx); transfer::public_transfer(coins, tx_context::sender(ctx)); object::delete(id); // Never freeze a vault! } Pattern 4: Transfer with Receipts public struct TransferReceipt has key { id: UID, asset_id: ID, from: address, to: address, timestamp: u64, } public entry fun tracked_transfer( asset: TransferableAsset, recipient: address, clock: &amp;Clock, ctx: &amp;mut TxContext ) { let asset_id = object::id(&amp;asset); let sender = tx_context::sender(ctx); // Create receipt before transfer let receipt = TransferReceipt { id: object::new(ctx), asset_id, from: sender, to: recipient, timestamp: clock::timestamp_ms(clock), }; // Keep receipt with sender for records transfer::transfer(receipt, sender); // Transfer asset transfer::public_transfer(asset, recipient); } Recommended Mitigations 1. Always Validate Recipients assert!(recipient != @0x0, E_ZERO_ADDRESS); assert!(recipient != tx_context::sender(ctx), E_SELF_TRANSFER); 2. Use the Correct Transfer Function // Check if object has `store` ability // - With store: use public_transfer // - Without store: use transfer (module-internal only) 3. Never Freeze Objects with Value // BAD: Funds locked forever transfer::freeze_object(vault_with_balance); // GOOD: Withdraw first, then delete let coins = withdraw_all(vault); delete_vault(vault); 4. Be Intentional About Sharing // Ask: &#34;Should anyone be able to access this?&#34; // If no, use transfer to specific address // If yes, carefully consider the implications 5. Emit Events for Transfers event::emit(TransferEvent { object_id: object::id(&amp;obj), from: sender, to: recipient, }); Testing Checklist Verify store ability matches transfer function used Test transfer to zero address is rejected Confirm self-transfers are handled appropriately Test that frozen objects don’t contain value Verify shared objects don’t expose sensitive capabilities Check hardcoded addresses are correct Test transfer events are emitted correctly Verify ownership updates when using internal owner fields Related Vulnerabilities Object Transfer Misuse Object Freezing Misuse Improper Object Sharing Ownership Model Confusion Ability Misconfiguration</description>
    </item>
    <item>
      <title>25. Unbounded Vector Growth</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/unbounded-vector-growth/index.html</link>
      <pubDate>Wed, 26 Nov 2025 21:06:01 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/unbounded-vector-growth/index.html</guid>
      <description>Overview Unbounded vector growth occurs when smart contracts allow vectors to grow without limits, leading to gas exhaustion attacks, denial of service, or excessive storage costs. In Sui Move, vectors stored in objects consume gas for both storage and iteration. Attackers can exploit unbounded vectors to make operations prohibitively expensive or cause transactions to fail.&#xA;Risk Level High — Can lead to denial of service or protocol unavailability.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A05 (Security Misconfiguration) CWE-770 (Allocation of Resources Without Limits or Throttling) The Problem Common Unbounded Vector Issues Issue Risk Description No size limit on push Critical Vector grows until gas exhaustion Iteration over large vectors High O(n) operations become too expensive Vector as primary storage High Should use Table or dynamic fields No cleanup mechanism Medium Data accumulates forever Copying large vectors High Unnecessary gas consumption Vulnerable Example module vulnerable::registry { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; use std::vector; const E_ALREADY_REGISTERED: u64 = 1; public struct Registry has key { id: UID, /// VULNERABLE: No limit on vector size members: vector&lt;address&gt;, /// VULNERABLE: Grows with every action action_log: vector&lt;ActionEntry&gt;, } public struct ActionEntry has store, copy, drop { actor: address, action_type: u8, timestamp: u64, } /// VULNERABLE: Vector grows without bound public entry fun register( registry: &amp;mut Registry, ctx: &amp;mut TxContext ) { let sender = tx_context::sender(ctx); // O(n) check - gets slower as registry grows let mut i = 0; let len = vector::length(&amp;registry.members); while (i &lt; len) { assert!(*vector::borrow(&amp;registry.members, i) != sender, E_ALREADY_REGISTERED); i = i + 1; }; // Unbounded growth vector::push_back(&amp;mut registry.members, sender); } /// VULNERABLE: Logs accumulate forever public entry fun log_action( registry: &amp;mut Registry, action_type: u8, clock: &amp;Clock, ctx: &amp;mut TxContext ) { let entry = ActionEntry { actor: tx_context::sender(ctx), action_type, timestamp: clock::timestamp_ms(clock), }; // Never cleaned up - grows forever vector::push_back(&amp;mut registry.action_log, entry); } /// VULNERABLE: O(n) iteration becomes impossible public fun get_total_members(registry: &amp;Registry): u64 { vector::length(&amp;registry.members) } /// VULNERABLE: Will fail for large registries public fun is_member(registry: &amp;Registry, addr: address): bool { let mut i = 0; let len = vector::length(&amp;registry.members); while (i &lt; len) { if (*vector::borrow(&amp;registry.members, i) == addr) { return true }; i = i + 1; }; false } } module vulnerable::marketplace { use sui::object::{Self, UID}; use std::vector; public struct Marketplace has key { id: UID, /// VULNERABLE: All listings in one vector listings: vector&lt;Listing&gt;, } public struct Listing has store, drop { seller: address, price: u64, item_id: ID, active: bool, } /// VULNERABLE: Searching listings is O(n) public fun find_listing( market: &amp;Marketplace, item_id: ID ): Option&lt;&amp;Listing&gt; { let mut i = 0; let len = vector::length(&amp;market.listings); while (i &lt; len) { let listing = vector::borrow(&amp;market.listings, i); if (listing.item_id == item_id) { return option::some(listing) }; i = i + 1; }; option::none() } /// VULNERABLE: Removal leaves gaps or requires O(n) shift public fun cancel_listing( market: &amp;mut Marketplace, item_id: ID, ) { let mut i = 0; let len = vector::length(&amp;market.listings); while (i &lt; len) { let listing = vector::borrow(&amp;market.listings, i); if (listing.item_id == item_id) { // swap_remove is O(1) but changes indices // remove is O(n) due to shifting vector::remove(&amp;mut market.listings, i); return }; i = i + 1; }; } } module vulnerable::voting { use std::vector; public struct Proposal has key { id: UID, /// VULNERABLE: All votes stored in vector votes: vector&lt;Vote&gt;, } public struct Vote has store, drop { voter: address, choice: u8, weight: u64, } /// VULNERABLE: Counting votes is O(n) public fun count_votes(proposal: &amp;Proposal): (u64, u64) { let mut yes_votes = 0u64; let mut no_votes = 0u64; let mut i = 0; let len = vector::length(&amp;proposal.votes); while (i &lt; len) { let vote = vector::borrow(&amp;proposal.votes, i); if (vote.choice == 1) { yes_votes = yes_votes + vote.weight; } else { no_votes = no_votes + vote.weight; }; i = i + 1; }; (yes_votes, no_votes) } } Attack Scenario module attack::dos_registry { use vulnerable::registry::{Self, Registry}; use sui::tx_context::TxContext; /// Attacker registers many addresses to bloat the registry public entry fun bloat_registry( registry: &amp;mut Registry, count: u64, ctx: &amp;mut TxContext ) { // In practice, attacker would use multiple transactions // and addresses to avoid duplicate checks // After thousands of entries: // - is_member() becomes too expensive // - register() duplicate check times out // - Legitimate users can&#39;t interact } } Secure Example module secure::registry { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; use sui::table::{Self, Table}; use sui::vec_set::{Self, VecSet}; const E_ALREADY_REGISTERED: u64 = 1; const E_MAX_MEMBERS_REACHED: u64 = 2; const E_NOT_MEMBER: u64 = 3; const MAX_MEMBERS: u64 = 10_000; public struct Registry has key { id: UID, /// SECURE: O(1) lookups with Table members: Table&lt;address, MemberInfo&gt;, member_count: u64, } public struct MemberInfo has store { joined_at: u64, active: bool, } /// SECURE: Bounded membership with O(1) operations public entry fun register( registry: &amp;mut Registry, clock: &amp;Clock, ctx: &amp;mut TxContext ) { let sender = tx_context::sender(ctx); // O(1) existence check assert!(!table::contains(&amp;registry.members, sender), E_ALREADY_REGISTERED); // Enforce maximum assert!(registry.member_count &lt; MAX_MEMBERS, E_MAX_MEMBERS_REACHED); let info = MemberInfo { joined_at: clock::timestamp_ms(clock), active: true, }; table::add(&amp;mut registry.members, sender, info); registry.member_count = registry.member_count + 1; } /// SECURE: O(1) membership check public fun is_member(registry: &amp;Registry, addr: address): bool { if (table::contains(&amp;registry.members, addr)) { let info = table::borrow(&amp;registry.members, addr); info.active } else { false } } /// SECURE: O(1) removal public entry fun unregister( registry: &amp;mut Registry, ctx: &amp;mut TxContext ) { let sender = tx_context::sender(ctx); assert!(table::contains(&amp;registry.members, sender), E_NOT_MEMBER); let _info = table::remove(&amp;mut registry.members, sender); registry.member_count = registry.member_count - 1; } } module secure::action_log { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::table::{Self, Table}; use sui::clock::{Self, Clock}; const MAX_LOG_ENTRIES: u64 = 1000; public struct ActionLog has key { id: UID, /// SECURE: Circular buffer pattern entries: Table&lt;u64, ActionEntry&gt;, next_index: u64, total_entries: u64, } public struct ActionEntry has store, drop { actor: address, action_type: u8, timestamp: u64, } /// SECURE: Bounded log with circular overwrite public entry fun log_action( log: &amp;mut ActionLog, action_type: u8, clock: &amp;Clock, ctx: &amp;mut TxContext ) { let entry = ActionEntry { actor: tx_context::sender(ctx), action_type, timestamp: clock::timestamp_ms(clock), }; // Calculate index in circular buffer let index = log.next_index % MAX_LOG_ENTRIES; // Remove old entry if exists if (table::contains(&amp;log.entries, index)) { table::remove(&amp;mut log.entries, index); }; // Add new entry table::add(&amp;mut log.entries, index, entry); log.next_index = log.next_index + 1; if (log.total_entries &lt; MAX_LOG_ENTRIES) { log.total_entries = log.total_entries + 1; }; } } module secure::marketplace { use sui::object::{Self, UID, ID}; use sui::tx_context::{Self, TxContext}; use sui::table::{Self, Table}; use sui::dynamic_object_field as dof; const E_LISTING_EXISTS: u64 = 1; const E_LISTING_NOT_FOUND: u64 = 2; const E_NOT_SELLER: u64 = 3; public struct Marketplace has key { id: UID, /// SECURE: O(1) lookups by item ID listings: Table&lt;ID, ListingInfo&gt;, listing_count: u64, } public struct ListingInfo has store { seller: address, price: u64, listed_at: u64, } /// Items stored as dynamic fields, not in vector public entry fun list_item&lt;T: key + store&gt;( market: &amp;mut Marketplace, item: T, price: u64, clock: &amp;Clock, ctx: &amp;mut TxContext ) { let item_id = object::id(&amp;item); assert!(!table::contains(&amp;market.listings, item_id), E_LISTING_EXISTS); let info = ListingInfo { seller: tx_context::sender(ctx), price, listed_at: clock::timestamp_ms(clock), }; // Store listing info in table (O(1)) table::add(&amp;mut market.listings, item_id, info); // Store item as dynamic field dof::add(&amp;mut market.id, item_id, item); market.listing_count = market.listing_count + 1; } /// SECURE: O(1) lookup public fun get_listing( market: &amp;Marketplace, item_id: ID ): &amp;ListingInfo { assert!(table::contains(&amp;market.listings, item_id), E_LISTING_NOT_FOUND); table::borrow(&amp;market.listings, item_id) } /// SECURE: O(1) removal public entry fun cancel_listing&lt;T: key + store&gt;( market: &amp;mut Marketplace, item_id: ID, ctx: &amp;mut TxContext ) { assert!(table::contains(&amp;market.listings, item_id), E_LISTING_NOT_FOUND); let info = table::borrow(&amp;market.listings, item_id); assert!(info.seller == tx_context::sender(ctx), E_NOT_SELLER); // Remove listing info let _info = table::remove(&amp;mut market.listings, item_id); // Return item to seller let item: T = dof::remove(&amp;mut market.id, item_id); transfer::public_transfer(item, tx_context::sender(ctx)); market.listing_count = market.listing_count - 1; } } module secure::voting { use sui::object::{Self, UID}; use sui::table::{Self, Table}; const E_ALREADY_VOTED: u64 = 1; public struct Proposal has key { id: UID, /// SECURE: Track votes per address (O(1) check) votes: Table&lt;address, Vote&gt;, /// SECURE: Pre-aggregated totals yes_votes: u64, no_votes: u64, total_weight: u64, } public struct Vote has store, drop { choice: u8, weight: u64, } /// SECURE: O(1) vote with pre-aggregation public entry fun cast_vote( proposal: &amp;mut Proposal, choice: u8, weight: u64, ctx: &amp;mut TxContext ) { let voter = tx_context::sender(ctx); // O(1) duplicate check assert!(!table::contains(&amp;proposal.votes, voter), E_ALREADY_VOTED); // Store vote let vote = Vote { choice, weight }; table::add(&amp;mut proposal.votes, voter, vote); // Update aggregates (no need to iterate later) if (choice == 1) { proposal.yes_votes = proposal.yes_votes + weight; } else { proposal.no_votes = proposal.no_votes + weight; }; proposal.total_weight = proposal.total_weight + weight; } /// SECURE: O(1) result retrieval public fun get_results(proposal: &amp;Proposal): (u64, u64, u64) { (proposal.yes_votes, proposal.no_votes, proposal.total_weight) } } Vector Usage Guidelines When to Use Vectors // GOOD: Small, bounded collections public struct Config has key { admins: vector&lt;address&gt;, // Max 5 admins tags: vector&lt;vector&lt;u8&gt;&gt;, // Max 10 tags } const MAX_ADMINS: u64 = 5; public fun add_admin(config: &amp;mut Config, admin: address) { assert!(vector::length(&amp;config.admins) &lt; MAX_ADMINS, E_MAX_ADMINS); vector::push_back(&amp;mut config.admins, admin); } When to Use Table/VecMap // GOOD: Large or unbounded collections with key lookups public struct UserRegistry has key { users: Table&lt;address, UserInfo&gt;, // Unlimited users, O(1) lookup } // GOOD: When you need to iterate occasionally but lookup often public struct TokenRegistry has key { tokens: VecMap&lt;ID, TokenInfo&gt;, // Ordered, iterable, O(n) lookup } When to Use Dynamic Fields // GOOD: Heterogeneous or large object storage public struct Wallet has key { id: UID, // Items stored as dynamic fields // - No vector bloat // - O(1) access by key // - Items can be different types } public fun store_item&lt;T: key + store&gt;(wallet: &amp;mut Wallet, item: T) { let item_id = object::id(&amp;item); dof::add(&amp;mut wallet.id, item_id, item); } Recommended Mitigations 1. Set Maximum Sizes const MAX_ENTRIES: u64 = 1000; public fun add_entry(collection: &amp;mut Collection, entry: Entry) { assert!(vector::length(&amp;collection.entries) &lt; MAX_ENTRIES, E_MAX_SIZE); vector::push_back(&amp;mut collection.entries, entry); } 2. Use Tables for Large Collections // Replace vector with Table for O(1) operations members: Table&lt;address, MemberInfo&gt;, // Not vector&lt;address&gt; 3. Pre-aggregate Data // Don&#39;t iterate to count - maintain running totals public struct Stats has key { total_votes: u64, // Updated on each vote total_value: u64, // Updated on each deposit } 4. Implement Cleanup Mechanisms // Circular buffer for logs let index = next_index % MAX_SIZE; // Old entries automatically overwritten 5. Use Pagination for Reads public fun get_entries_paginated( collection: &amp;Collection, offset: u64, limit: u64 ): vector&lt;Entry&gt; { // Return only a subset } Testing Checklist Verify all vectors have maximum size limits Test behavior at maximum capacity Confirm O(n) operations are avoided or bounded Test gas consumption with large data sets Verify cleanup mechanisms work correctly Check that Tables are used for key-based lookups Test pagination for large result sets Related Vulnerabilities Unbounded Child Growth Shared Object DoS Inefficient PTB Composition General Move Logic Errors</description>
    </item>
    <item>
      <title>26. Upgrade Boundary Errors</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/upgrade-boundary-errors/index.html</link>
      <pubDate>Wed, 26 Nov 2025 21:18:20 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/upgrade-boundary-errors/index.html</guid>
      <description>Overview Upgrade boundary errors occur when package upgrades break compatibility with existing on-chain state, cause ABI mismatches, or violate upgrade policies. Sui Move packages can be upgraded, but upgrades must maintain compatibility with objects created by previous versions. Failing to handle upgrade boundaries correctly can corrupt state, break integrations, or lock funds permanently.&#xA;Risk Level Critical — Can lead to permanent protocol breakage or locked funds.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A04 (Insecure Design) / A06 (Vulnerable and Outdated Components) CWE-685 (Function Call With Incorrect Number of Arguments), CWE-694 (Use of Multiple Resources with Duplicate Identifier) The Problem Common Upgrade Boundary Issues Issue Risk Description Struct field changes Critical Adding/removing/reordering fields breaks existing objects Function signature changes High Callers using old signatures fail Removing public functions High Dependent packages break Changing type parameters Critical Type mismatches on existing objects Incompatible upgrade policy Medium Upgrades blocked unexpectedly Sui Upgrade Policies Policy Description Restrictions compatible Default, allows compatible changes Cannot change struct layouts additive Only additions allowed No removals or modifications dep_only Dependency updates only No code changes immutable No upgrades allowed Package frozen forever Vulnerable Example // === VERSION 1 (Original Deployment) === module vulnerable::token_v1 { use sui::object::{Self, UID}; use sui::tx_context::TxContext; public struct Token has key, store { id: UID, value: u64, owner: address, } public fun get_value(token: &amp;Token): u64 { token.value } public fun transfer_value( from: &amp;mut Token, to: &amp;mut Token, amount: u64 ) { from.value = from.value - amount; to.value = to.value + amount; } } // === VERSION 2 (BROKEN UPGRADE) === module vulnerable::token_v2 { use sui::object::{Self, UID}; use sui::tx_context::TxContext; /// VULNERABLE: Added field breaks existing Token objects! public struct Token has key, store { id: UID, value: u64, owner: address, created_at: u64, // NEW FIELD - breaks deserialization! } /// VULNERABLE: Changed function signature public fun get_value(token: &amp;Token, _ctx: &amp;TxContext): u64 { // Added parameter breaks all callers token.value } /// VULNERABLE: Removed function breaks dependent packages // transfer_value is gone! /// VULNERABLE: New function with same logic but different name public fun send_value( from: &amp;mut Token, to: &amp;mut Token, amount: u64, fee: u64, // Added fee parameter ) { from.value = from.value - amount - fee; to.value = to.value + amount; } } // === ANOTHER BROKEN PATTERN === module vulnerable::registry_v1 { public struct Registry has key { id: UID, entries: vector&lt;Entry&gt;, } public struct Entry has store { name: vector&lt;u8&gt;, value: u64, } } module vulnerable::registry_v2 { public struct Registry has key { id: UID, /// VULNERABLE: Changed from vector to Table /// Existing Registry objects can&#39;t be read! entries: Table&lt;vector&lt;u8&gt;, Entry&gt;, } /// VULNERABLE: Entry struct layout changed public struct Entry has store { value: u64, // Reordered! name: vector&lt;u8&gt;, active: bool, // Added field } } Breaking Scenarios // Scenario 1: Existing objects become unreadable public entry fun use_old_token(token: &amp;Token) { // After v2 upgrade, Token struct has different layout // Old tokens created in v1 can&#39;t be deserialized // This function will abort! } // Scenario 2: Dependent packages break module other_package::integration { use vulnerable::token_v1; // Compiled against v1 public fun do_transfer(from: &amp;mut Token, to: &amp;mut Token) { // After upgrade, transfer_value doesn&#39;t exist // This call fails at runtime token_v1::transfer_value(from, to, 100); } } // Scenario 3: Type parameter mismatch module vulnerable::pool_v1 { public struct Pool&lt;phantom T&gt; has key { id: UID, balance: Balance&lt;T&gt;, } } module vulnerable::pool_v2 { // Changed phantom to non-phantom - incompatible! public struct Pool&lt;T: store&gt; has key { id: UID, balance: Balance&lt;T&gt;, fee_rate: u64, } } Secure Example // === VERSION 1 (Original - Upgrade-Safe Design) === module secure::token_v1 { use sui::object::{Self, UID}; use sui::tx_context::TxContext; use sui::dynamic_field as df; const VERSION: u64 = 1; /// Core struct - fields are FINAL after deployment public struct Token has key, store { id: UID, value: u64, owner: address, version: u64, // Track version for migrations } /// Use dynamic fields for extensibility public fun set_metadata(token: &amp;mut Token, key: vector&lt;u8&gt;, value: vector&lt;u8&gt;) { df::add(&amp;mut token.id, key, value); } public fun get_value(token: &amp;Token): u64 { token.value } /// Keep original function signature forever public fun transfer_value( from: &amp;mut Token, to: &amp;mut Token, amount: u64 ) { transfer_value_internal(from, to, amount, 0); } /// Internal implementation can change fun transfer_value_internal( from: &amp;mut Token, to: &amp;mut Token, amount: u64, _fee: u64 ) { from.value = from.value - amount; to.value = to.value + amount; } public fun create(value: u64, ctx: &amp;mut TxContext): Token { Token { id: object::new(ctx), value, owner: tx_context::sender(ctx), version: VERSION, } } } // === VERSION 2 (Safe Upgrade) === module secure::token_v2 { use sui::object::{Self, UID}; use sui::tx_context::TxContext; use sui::dynamic_field as df; const VERSION: u64 = 2; const CREATED_AT_KEY: vector&lt;u8&gt; = b&#34;created_at&#34;; /// SECURE: Struct layout is IDENTICAL to v1 public struct Token has key, store { id: UID, value: u64, owner: address, version: u64, } /// SECURE: Original function signature preserved public fun get_value(token: &amp;Token): u64 { token.value } /// SECURE: Original function still works public fun transfer_value( from: &amp;mut Token, to: &amp;mut Token, amount: u64 ) { // Internally uses new logic with zero fee transfer_value_with_fee(from, to, amount, 0); } /// SECURE: New functionality via new function public fun transfer_value_with_fee( from: &amp;mut Token, to: &amp;mut Token, amount: u64, fee: u64 ) { from.value = from.value - amount - fee; to.value = to.value + amount; } /// SECURE: New data stored in dynamic fields public fun set_created_at(token: &amp;mut Token, timestamp: u64) { if (df::exists_(&amp;token.id, CREATED_AT_KEY)) { *df::borrow_mut(&amp;mut token.id, CREATED_AT_KEY) = timestamp; } else { df::add(&amp;mut token.id, CREATED_AT_KEY, timestamp); }; } public fun get_created_at(token: &amp;Token): Option&lt;u64&gt; { if (df::exists_(&amp;token.id, CREATED_AT_KEY)) { option::some(*df::borrow(&amp;token.id, CREATED_AT_KEY)) } else { option::none() } } /// SECURE: Migration function for version updates public fun migrate_token(token: &amp;mut Token, clock: &amp;Clock) { if (token.version &lt; VERSION) { // Perform any necessary migration if (!df::exists_(&amp;token.id, CREATED_AT_KEY)) { df::add(&amp;mut token.id, CREATED_AT_KEY, clock::timestamp_ms(clock)); }; token.version = VERSION; }; } /// New tokens get new features automatically public fun create(value: u64, clock: &amp;Clock, ctx: &amp;mut TxContext): Token { let mut token = Token { id: object::new(ctx), value, owner: tx_context::sender(ctx), version: VERSION, }; df::add(&amp;mut token.id, CREATED_AT_KEY, clock::timestamp_ms(clock)); token } } Upgrade-Safe Patterns Pattern 1: Version Tracking const CURRENT_VERSION: u64 = 3; public struct Protocol has key { id: UID, version: u64, // ... other fields unchanged } public fun check_version(protocol: &amp;Protocol) { assert!(protocol.version &lt;= CURRENT_VERSION, E_FUTURE_VERSION); } public entry fun migrate(protocol: &amp;mut Protocol, ctx: &amp;TxContext) { // Only admin can migrate assert!(is_admin(ctx), E_NOT_ADMIN); if (protocol.version == 1) { // v1 -&gt; v2 migration logic protocol.version = 2; }; if (protocol.version == 2) { // v2 -&gt; v3 migration logic protocol.version = 3; }; } Pattern 2: Dynamic Fields for Extensions /// Never add fields to this struct after deployment public struct CoreData has key { id: UID, essential_field: u64, } /// Extension data lives in dynamic fields public fun add_extension&lt;T: store&gt;(core: &amp;mut CoreData, key: vector&lt;u8&gt;, data: T) { df::add(&amp;mut core.id, key, data); } public fun get_extension&lt;T: store&gt;(core: &amp;CoreData, key: vector&lt;u8&gt;): &amp;T { df::borrow(&amp;core.id, key) } Pattern 3: Wrapper Structs for New Versions // V1 struct - never changes public struct DataV1 has key, store { id: UID, value: u64, } // V2 adds features via wrapper public struct DataV2Wrapper has key { id: UID, inner: DataV1, // Contains V1 data new_field: u64, // New functionality } // Migration function public fun upgrade_to_v2(data: DataV1, ctx: &amp;mut TxContext): DataV2Wrapper { DataV2Wrapper { id: object::new(ctx), inner: data, new_field: 0, } } Pattern 4: Function Deprecation (Not Removal) /// Original function - NEVER remove, keep forever public fun old_function(data: &amp;Data): u64 { // Delegate to new implementation new_function(data, default_options()) } /// New function with more features public fun new_function(data: &amp;Data, options: Options): u64 { // New implementation } /// Mark as deprecated in documentation, not in code /// #[deprecated = &#34;Use new_function instead&#34;] /// But the function still works! Pattern 5: Upgrade Capability Control public struct UpgradeCap has key { id: UID, package_id: ID, policy: u8, // 0=compatible, 1=additive, 2=dep_only } public fun restrict_upgrades(cap: &amp;mut UpgradeCap, new_policy: u8) { // Can only make more restrictive assert!(new_policy &gt;= cap.policy, E_CANNOT_RELAX_POLICY); cap.policy = new_policy; } public fun make_immutable(cap: UpgradeCap) { // Destroy cap - no more upgrades possible let UpgradeCap { id, package_id: _, policy: _ } = cap; object::delete(id); } Upgrade Checklist Before Upgrading 1. [ ] All struct layouts are IDENTICAL to previous version 2. [ ] No fields added, removed, or reordered in existing structs 3. [ ] All public function signatures are unchanged 4. [ ] No public functions removed 5. [ ] Type parameters unchanged (phantom status, constraints) 6. [ ] New functionality uses new functions or dynamic fields 7. [ ] Migration path exists for version-specific logic Compatible Changes (Allowed) // ✅ Add new public functions public fun new_feature() { } // ✅ Add new structs public struct NewData has key { } // ✅ Change function implementations (not signatures) public fun existing_fn(): u64 { // Changed implementation is OK new_calculation() } // ✅ Add private/internal functions fun helper() { } // ✅ Use dynamic fields for new data df::add(&amp;mut obj.id, b&#34;new_data&#34;, value); Incompatible Changes (Forbidden) // ❌ Change struct fields public struct Data { new_field: u64, // BREAKS existing objects } // ❌ Change function signature public fun func(new_param: u64) { } // BREAKS callers // ❌ Remove public function // (deleted function) // BREAKS dependent packages // ❌ Change type parameters public struct Pool&lt;T: store&gt; { } // Was phantom T // ❌ Change abilities public struct Data has key { } // Had key, store Recommended Mitigations 1. Design for Extensibility from Day One public struct Protocol has key { id: UID, version: u64, // Always include version // Core fields only - use dynamic fields for rest } 2. Never Remove Public Functions // Keep old function, delegate to new public fun old_api(): u64 { new_api(defaults()) } public fun new_api(opts: Options): u64 { /* new impl */ } 3. Use Dynamic Fields for Extensions // Add new data without changing struct df::add(&amp;mut obj.id, b&#34;v2_data&#34;, new_data); 4. Test Upgrades Thoroughly #[test] fun test_upgrade_compatibility() { // Create objects with v1 // Upgrade package // Verify v1 objects still work with v2 code } 5. Document Breaking Changes /// UPGRADE NOTES: /// - v1 -&gt; v2: No breaking changes /// - v2 -&gt; v3: Call migrate() on existing objects Testing Checklist Verify existing objects deserialize after upgrade Test all public functions work with old objects Confirm dependent packages still compile Test migration functions handle all versions Verify new features don’t affect old code paths Check upgrade policy allows planned changes Test rollback scenarios if possible Related Vulnerabilities Weak Initializers Ability Misconfiguration Dynamic Field Misuse Ownership Model Confusion</description>
    </item>
    <item>
      <title>27. Event State Inconsistency</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/event-state-inconsistency/index.html</link>
      <pubDate>Wed, 26 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/event-state-inconsistency/index.html</guid>
      <description>Overview Event state inconsistency occurs when emitted events don’t accurately reflect the actual on-chain state changes. In Sui Move, events are the primary mechanism for off-chain systems (indexers, frontends, analytics) to track what happened on-chain. When events are missing, duplicated, emitted before state changes, or contain incorrect data, off-chain systems build an incorrect view of the protocol state.&#xA;Risk Level Medium to High — Can lead to incorrect off-chain state, failed integrations, or user confusion.</description>
    </item>
    <item>
      <title>28. Read API Leakage</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/read-api-leakage/index.html</link>
      <pubDate>Wed, 26 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/read-api-leakage/index.html</guid>
      <description>Overview Read API leakage occurs when view functions or public getters expose sensitive information that should remain private. In Sui Move, while direct state access requires ownership, public functions can inadvertently reveal private keys, internal state, user data, or security-critical information. Attackers can use this leaked information to plan attacks, front-run transactions, or compromise user privacy.&#xA;Risk Level Medium to High — Can enable attacks, compromise privacy, or reveal competitive information.</description>
    </item>
    <item>
      <title>29. Unsafe BCS Parsing</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/unsafe-bcs-parsing/index.html</link>
      <pubDate>Wed, 26 Nov 2025 21:42:46 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/unsafe-bcs-parsing/index.html</guid>
      <description>Overview Unsafe BCS (Binary Canonical Serialization) parsing occurs when smart contracts or off-chain systems improperly deserialize BCS-encoded data, leading to type confusion, buffer overflows, or malformed data acceptance. BCS is Sui’s standard serialization format, used for transaction data, object serialization, and cross-module communication. Improper parsing can lead to security vulnerabilities both on-chain and in off-chain indexers.&#xA;Risk Level High — Can lead to type confusion attacks, data corruption, or off-chain system compromise.</description>
    </item>
    <item>
      <title>30. Unsafe Test Patterns</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/unsafe-test-patterns/index.html</link>
      <pubDate>Wed, 26 Nov 2025 21:44:34 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/unsafe-test-patterns/index.html</guid>
      <description>Overview Unsafe test patterns occur when test-only code, debug functionality, or development shortcuts accidentally make it into production smart contracts. In Sui Move, the #[test_only] attribute should isolate test code, but improper patterns can leak test utilities, create backdoors, or leave vulnerabilities that only manifest in production environments.&#xA;Risk Level High — Can create backdoors, bypass security controls, or cause unexpected production behavior.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A04 (Insecure Design) CWE-704 (Incorrect Type Conversion or Cast), CWE-665 (Improper Initialization) The Problem Common Unsafe Test Patterns Issue Risk Description Test functions without #[test_only] Critical Test code callable in production test_scenario in production Critical Fake context manipulation Debug mint functions Critical Unlimited token creation Hardcoded test addresses High Known addresses exploitable Disabled security checks High Guards removed for testing Mock oracles in production Critical Fake price data accepted Vulnerable Example module vulnerable::token { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; use sui::coin::{Self, Coin, TreasuryCap}; /// VULNERABLE: No #[test_only] - callable in production! public fun mint_for_testing( cap: &amp;mut TreasuryCap&lt;TOKEN&gt;, amount: u64, ctx: &amp;mut TxContext ): Coin&lt;TOKEN&gt; { // Anyone who has the cap can mint unlimited tokens // This was meant for tests only! coin::mint(cap, amount, ctx) } /// VULNERABLE: Debug function left in production public entry fun debug_set_balance( vault: &amp;mut Vault, new_balance: u64, _ctx: &amp;mut TxContext ) { // No access control - was &#34;temporary&#34; for debugging vault.balance = new_balance; } /// VULNERABLE: Test bypass flag public entry fun transfer_tokens( vault: &amp;mut Vault, amount: u64, recipient: address, skip_checks: bool, // &#34;For testing&#34; - but callable by anyone! ctx: &amp;mut TxContext ) { if (!skip_checks) { assert!(vault.owner == tx_context::sender(ctx), E_NOT_OWNER); assert!(amount &lt;= vault.balance, E_INSUFFICIENT); }; // Transfer proceeds even without checks if skip_checks = true vault.balance = vault.balance - amount; // ... } } module vulnerable::oracle { /// VULNERABLE: Test oracle mode in production public struct Oracle has key { id: UID, price: u64, /// VULNERABLE: Allows anyone to set price in &#34;test mode&#34; test_mode: bool, } public entry fun set_price( oracle: &amp;mut Oracle, new_price: u64, _ctx: &amp;mut TxContext ) { if (oracle.test_mode) { // No signature verification in test mode! oracle.price = new_price; } else { // Normal verification... }; } /// VULNERABLE: Anyone can enable test mode public entry fun enable_test_mode( oracle: &amp;mut Oracle, _ctx: &amp;mut TxContext ) { oracle.test_mode = true; } } module vulnerable::admin { /// VULNERABLE: Hardcoded test admin address const TEST_ADMIN: address = @0x1234; public entry fun admin_action( state: &amp;mut State, ctx: &amp;mut TxContext ) { let sender = tx_context::sender(ctx); // VULNERABLE: Test admin works in production! if (sender == TEST_ADMIN || sender == state.admin) { // Perform admin action }; } } module vulnerable::escrow { /// VULNERABLE: Emergency bypass without proper guards const EMERGENCY_WITHDRAW_ENABLED: bool = true; // Forgot to disable! public entry fun emergency_withdraw( escrow: &amp;mut Escrow, ctx: &amp;mut TxContext ) { // VULNERABLE: This was for testing emergencies if (EMERGENCY_WITHDRAW_ENABLED) { // Anyone can withdraw everything! let all_funds = withdraw_all(escrow, ctx); transfer::public_transfer(all_funds, tx_context::sender(ctx)); }; } } // VULNERABLE: Test helper module without #[test_only] module vulnerable::test_helpers { use sui::tx_context::TxContext; /// Should be test_only but isn&#39;t! public fun create_fake_admin_cap(ctx: &amp;mut TxContext): AdminCap { AdminCap { id: object::new(ctx) } } /// Should be test_only but isn&#39;t! public fun set_timestamp(clock: &amp;mut Clock, new_time: u64) { clock.timestamp_ms = new_time; } } Attack Scenario module attack::exploit_test_code { use vulnerable::token; use vulnerable::oracle; use vulnerable::test_helpers; /// Attacker uses &#34;test&#34; functions in production public entry fun exploit( oracle: &amp;mut Oracle, treasury_cap: &amp;mut TreasuryCap&lt;TOKEN&gt;, ctx: &amp;mut TxContext ) { // Step 1: Enable test mode on oracle oracle::enable_test_mode(oracle); // Step 2: Set price to manipulate protocol oracle::set_price(oracle, 1, ctx); // Crash the price // Step 3: Mint unlimited tokens let free_money = token::mint_for_testing(treasury_cap, 1000000000, ctx); // Step 4: Create fake admin cap let fake_admin = test_helpers::create_fake_admin_cap(ctx); // Complete protocol takeover! } } Secure Example module secure::token { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::coin::{Self, Coin, TreasuryCap}; /// SECURE: Test function properly annotated #[test_only] public fun mint_for_testing( cap: &amp;mut TreasuryCap&lt;TOKEN&gt;, amount: u64, ctx: &amp;mut TxContext ): Coin&lt;TOKEN&gt; { coin::mint(cap, amount, ctx) } /// SECURE: No debug functions in production code // debug_set_balance doesn&#39;t exist at all /// SECURE: No bypass flags public entry fun transfer_tokens( vault: &amp;mut Vault, amount: u64, recipient: address, ctx: &amp;mut TxContext ) { // Always enforce security checks assert!(vault.owner == tx_context::sender(ctx), E_NOT_OWNER); assert!(amount &lt;= vault.balance, E_INSUFFICIENT); vault.balance = vault.balance - amount; // Transfer... } } module secure::oracle { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::ed25519; /// SECURE: No test mode in production struct public struct Oracle has key { id: UID, price: u64, last_update: u64, trusted_pubkey: vector&lt;u8&gt;, } /// SECURE: Always requires signature verification public entry fun set_price( oracle: &amp;mut Oracle, new_price: u64, timestamp: u64, signature: vector&lt;u8&gt;, _ctx: &amp;mut TxContext ) { // Always verify signature - no test mode bypass let message = create_price_message(new_price, timestamp); let valid = ed25519::ed25519_verify( &amp;signature, &amp;oracle.trusted_pubkey, &amp;message ); assert!(valid, E_INVALID_SIGNATURE); oracle.price = new_price; oracle.last_update = timestamp; } /// SECURE: Test-only mock oracle #[test_only] public fun create_test_oracle(price: u64, ctx: &amp;mut TxContext): Oracle { Oracle { id: object::new(ctx), price, last_update: 0, trusted_pubkey: vector::empty(), // Not used in tests } } #[test_only] public fun set_price_for_testing(oracle: &amp;mut Oracle, new_price: u64) { oracle.price = new_price; } } module secure::admin { use sui::object::{Self, UID, ID}; use sui::tx_context::{Self, TxContext}; /// SECURE: No hardcoded addresses public struct AdminCap has key { id: UID, state_id: ID, } /// SECURE: Capability-based access control public entry fun admin_action( cap: &amp;AdminCap, state: &amp;mut State, _ctx: &amp;mut TxContext ) { assert!(cap.state_id == object::id(state), E_WRONG_STATE); // Perform admin action } /// SECURE: Test admin cap creation is test-only #[test_only] public fun create_admin_cap_for_testing( state: &amp;State, ctx: &amp;mut TxContext ): AdminCap { AdminCap { id: object::new(ctx), state_id: object::id(state), } } } module secure::escrow { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::clock::{Self, Clock}; const EMERGENCY_DELAY_MS: u64 = 86400000; // 24 hours /// SECURE: Emergency withdraw with proper controls public struct Escrow has key { id: UID, owner: address, balance: u64, emergency_requested_at: Option&lt;u64&gt;, } /// SECURE: Emergency requires time delay and ownership public entry fun request_emergency_withdraw( escrow: &amp;mut Escrow, clock: &amp;Clock, ctx: &amp;mut TxContext ) { assert!(tx_context::sender(ctx) == escrow.owner, E_NOT_OWNER); escrow.emergency_requested_at = option::some(clock::timestamp_ms(clock)); } public entry fun execute_emergency_withdraw( escrow: &amp;mut Escrow, clock: &amp;Clock, ctx: &amp;mut TxContext ) { assert!(tx_context::sender(ctx) == escrow.owner, E_NOT_OWNER); assert!(option::is_some(&amp;escrow.emergency_requested_at), E_NOT_REQUESTED); let requested_at = *option::borrow(&amp;escrow.emergency_requested_at); let now = clock::timestamp_ms(clock); // SECURE: Must wait 24 hours assert!(now &gt;= requested_at + EMERGENCY_DELAY_MS, E_TOO_EARLY); // Proceed with withdrawal escrow.emergency_requested_at = option::none(); // ... } } /// SECURE: Test helpers are properly isolated #[test_only] module secure::test_helpers { use sui::object::{Self, UID}; use sui::tx_context::TxContext; use sui::test_scenario; public fun setup_test_env(sender: address): test_scenario::Scenario { test_scenario::begin(sender) } public fun create_test_clock(timestamp: u64, ctx: &amp;mut TxContext): Clock { // Only available in tests Clock { id: object::new(ctx), timestamp_ms: timestamp, } } } Test Pattern Guidelines Pattern 1: Proper Test Annotation /// Production code - always present public fun calculate_fee(amount: u64): u64 { (amount * FEE_BPS) / 10000 } /// Test code - only compiled in tests #[test_only] public fun calculate_fee_for_testing(amount: u64, custom_bps: u64): u64 { (amount * custom_bps) / 10000 } #[test] fun test_fee_calculation() { assert!(calculate_fee(1000) == 30, 0); // 0.3% fee } Pattern 2: Test-Only Module /// Production module module myprotocol::core { public struct State has key { id: UID, value: u64, } public fun get_value(state: &amp;State): u64 { state.value } // No test utilities here } /// Completely separate test module #[test_only] module myprotocol::core_tests { use myprotocol::core::{Self, State}; use sui::test_scenario; fun create_test_state(ctx: &amp;mut TxContext): State { // Test-only state creation } #[test] fun test_get_value() { // Test implementation } } Pattern 3: Feature Flags (Compile-Time) /// Use Move.toml for environment-specific builds /// NOT runtime flags that can be toggled // In Move.toml: // [package] // name = &#34;MyProtocol&#34; // // [dev-addresses] // myprotocol = &#34;0x0&#34; // // [addresses] // myprotocol = &#34;0xPRODUCTION_ADDRESS&#34; /// Code uses compile-time address module myprotocol::config { // Address is set at compile time, not runtime const ADMIN: address = @myprotocol; } Pattern 4: Separate Test Scenarios #[test_only] module myprotocol::integration_tests { use sui::test_scenario::{Self, Scenario}; use sui::test_utils; const ALICE: address = @0xA; const BOB: address = @0xB; fun setup(): Scenario { let mut scenario = test_scenario::begin(ALICE); // Setup test environment scenario } #[test] fun test_full_flow() { let mut scenario = setup(); // Test as Alice test_scenario::next_tx(&amp;mut scenario, ALICE); { // Alice&#39;s actions }; // Test as Bob test_scenario::next_tx(&amp;mut scenario, BOB); { // Bob&#39;s actions }; test_scenario::end(scenario); } } Pre-Deployment Checklist Code Review Items 1. [ ] Search for &#34;test&#34; in function names - all should be #[test_only] 2. [ ] Search for &#34;debug&#34; in function names - should not exist 3. [ ] Search for &#34;mock&#34; - should be #[test_only] 4. [ ] Search for &#34;skip&#34; or &#34;bypass&#34; parameters - should not exist 5. [ ] Search for hardcoded addresses - should use capabilities 6. [ ] Search for boolean flags that disable security - remove them 7. [ ] Verify no `test_scenario` usage outside #[test_only] 8. [ ] Check for &#34;TODO&#34; or &#34;FIXME&#34; comments about security Automated Checks # Find potential test code in production grep -r &#34;for_testing\|_test\|debug_\|mock_&#34; src/ --include=&#34;*.move&#34; | \ grep -v &#34;#\[test_only\]&#34; | \ grep -v &#34;#\[test\]&#34; # Find bypass flags grep -r &#34;skip_check\|bypass\|test_mode\|debug_mode&#34; src/ --include=&#34;*.move&#34; # Find hardcoded addresses (excluding @0x0, @0x1, @0x2 system addresses) grep -r &#34;@0x[3-9a-fA-F]&#34; src/ --include=&#34;*.move&#34; Recommended Mitigations 1. Always Use #[test_only] #[test_only] public fun any_test_helper(...) { } #[test_only] module mypackage::test_utils { } 2. No Runtime Test Flags // BAD: Runtime flag public struct Config { test_mode: bool, } // GOOD: No test mode in production structs public struct Config { // Only production fields } 3. Capability-Based Access, Not Addresses // BAD: Hardcoded address if (sender == @0x1234) { } // GOOD: Capability check public fun admin_action(cap: &amp;AdminCap) { } 4. Separate Test Modules // Production code in src/ module myprotocol::core { } // Test code with #[test_only] #[test_only] module myprotocol::core_tests { } 5. Pre-Deployment Audit // Before mainnet: // 1. Remove all #[test_only] modules from build // 2. Verify no test patterns in production code // 3. Check for debug/mock functions // 4. Review all public functions Testing Checklist All test functions have #[test_only] attribute No test_scenario usage in production code No debug/mock functions without #[test_only] No hardcoded test addresses No runtime “test mode” or “skip checks” flags No emergency bypasses without proper controls Automated grep checks pass Code review specifically for test patterns Related Vulnerabilities Weak Initializers Access-Control Mistakes Capability Leakage Ability Misconfiguration</description>
    </item>
    <item>
      <title>31. Unvalidated Struct Fields</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/unvalidated-struct-fields/index.html</link>
      <pubDate>Wed, 26 Nov 2025 21:46:35 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/unvalidated-struct-fields/index.html</guid>
      <description>Overview Unvalidated struct fields occur when smart contracts accept user-provided data to populate struct fields without proper validation, leading to invalid state, security bypasses, or protocol corruption. In Sui Move, structs often represent critical protocol state, and failing to validate inputs during construction or modification can have severe consequences.&#xA;Risk Level High — Can lead to invalid state, security bypasses, or financial losses.&#xA;OWASP / CWE Mapping OWASP Top 10 MITRE CWE A04 (Insecure Design) CWE-20 (Improper Input Validation) The Problem Common Validation Failures Issue Risk Description No range validation High Values outside acceptable bounds No format validation Medium Invalid strings or identifiers No relationship validation High Fields that must be consistent No business rule validation High Values that violate protocol logic No sanitization Medium Malicious or unexpected input Vulnerable Example module vulnerable::lending { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; public struct LendingPool has key { id: UID, /// VULNERABLE: No validation on interest rate interest_rate_bps: u64, // Could be 0 or 1000000 /// VULNERABLE: No validation on ratios collateral_ratio_bps: u64, liquidation_ratio_bps: u64, /// VULNERABLE: No validation on addresses treasury: address, oracle: address, } /// VULNERABLE: Accepts any values without validation public entry fun create_pool( interest_rate_bps: u64, collateral_ratio_bps: u64, liquidation_ratio_bps: u64, treasury: address, oracle: address, ctx: &amp;mut TxContext ) { let pool = LendingPool { id: object::new(ctx), interest_rate_bps, // Could be 100% or 0% collateral_ratio_bps, // Could be less than liquidation! liquidation_ratio_bps, treasury, // Could be @0x0 oracle, // Could be attacker&#39;s address }; transfer::share_object(pool); } /// VULNERABLE: Update without validation public entry fun update_rates( pool: &amp;mut LendingPool, new_interest_rate: u64, new_collateral_ratio: u64, _ctx: &amp;mut TxContext ) { // No validation - could set absurd rates pool.interest_rate_bps = new_interest_rate; pool.collateral_ratio_bps = new_collateral_ratio; } } module vulnerable::nft { use sui::object::{Self, UID}; use std::string::{Self, String}; public struct NFT has key, store { id: UID, /// VULNERABLE: No length limits name: String, description: String, /// VULNERABLE: No URL validation image_url: String, /// VULNERABLE: No bounds on attributes rarity: u64, power: u64, } /// VULNERABLE: Accepts any string content public fun create_nft( name: String, description: String, image_url: String, rarity: u64, power: u64, ctx: &amp;mut TxContext ): NFT { // No validation at all! NFT { id: object::new(ctx), name, // Could be empty or 10MB description, // Could contain malicious content image_url, // Could be javascript: or data: URI rarity, // Could be any number power, // Could break game balance } } } module vulnerable::auction { use sui::object::{Self, UID}; use sui::clock::Clock; public struct Auction has key { id: UID, /// VULNERABLE: Time relationships not validated start_time: u64, end_time: u64, /// VULNERABLE: Price relationships not validated starting_price: u64, reserve_price: u64, minimum_increment: u64, } /// VULNERABLE: Invalid time/price configurations allowed public fun create_auction( start_time: u64, end_time: u64, starting_price: u64, reserve_price: u64, minimum_increment: u64, ctx: &amp;mut TxContext ): Auction { // No validation of relationships! Auction { id: object::new(ctx), start_time, // Could be in the past end_time, // Could be before start_time! starting_price, // Could be higher than reserve reserve_price, minimum_increment, // Could be 0 } } } module vulnerable::user { public struct UserProfile has key { id: UID, owner: address, /// VULNERABLE: No validation on username username: vector&lt;u8&gt;, /// VULNERABLE: Tier can be set to anything tier: u8, /// VULNERABLE: Points can be manipulated points: u64, } /// VULNERABLE: User can set their own tier public entry fun create_profile( username: vector&lt;u8&gt;, tier: u8, // User chooses their tier! points: u64, // User chooses their points! ctx: &amp;mut TxContext ) { let profile = UserProfile { id: object::new(ctx), owner: tx_context::sender(ctx), username, tier, points, }; transfer::transfer(profile, tx_context::sender(ctx)); } } Attack Scenarios module attack::exploit_unvalidated { use vulnerable::lending; use vulnerable::user; /// Set up a lending pool that always liquidates public entry fun create_trap_pool(ctx: &amp;mut TxContext) { lending::create_pool( 10000, // 100% interest rate 5000, // 50% collateral ratio 9999, // 99.99% liquidation ratio (&gt; collateral!) @attacker, // Treasury to attacker @attacker, // Fake oracle ctx ); } /// Create an admin-tier user profile public entry fun become_admin(ctx: &amp;mut TxContext) { user::create_profile( b&#34;hacker&#34;, 255, // Max tier = admin? 1000000000, // Billion points ctx ); } } Secure Example module secure::lending { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; const E_INVALID_INTEREST_RATE: u64 = 1; const E_INVALID_COLLATERAL_RATIO: u64 = 2; const E_INVALID_LIQUIDATION_RATIO: u64 = 3; const E_RATIOS_INCONSISTENT: u64 = 4; const E_INVALID_ADDRESS: u64 = 5; const E_NOT_ADMIN: u64 = 6; /// Bounds for validation const MIN_INTEREST_RATE_BPS: u64 = 0; const MAX_INTEREST_RATE_BPS: u64 = 5000; // Max 50% APR const MIN_COLLATERAL_RATIO_BPS: u64 = 10000; // Min 100% const MAX_COLLATERAL_RATIO_BPS: u64 = 30000; // Max 300% const MIN_LIQUIDATION_RATIO_BPS: u64 = 10000; // Min 100% const LIQUIDATION_BUFFER_BPS: u64 = 500; // 5% buffer required public struct AdminCap has key { id: UID, pool_id: ID, } public struct LendingPool has key { id: UID, interest_rate_bps: u64, collateral_ratio_bps: u64, liquidation_ratio_bps: u64, treasury: address, oracle: address, } /// SECURE: Validates all inputs before creating pool public entry fun create_pool( interest_rate_bps: u64, collateral_ratio_bps: u64, liquidation_ratio_bps: u64, treasury: address, oracle: address, ctx: &amp;mut TxContext ) { // Validate interest rate assert!( interest_rate_bps &gt;= MIN_INTEREST_RATE_BPS &amp;&amp; interest_rate_bps &lt;= MAX_INTEREST_RATE_BPS, E_INVALID_INTEREST_RATE ); // Validate collateral ratio assert!( collateral_ratio_bps &gt;= MIN_COLLATERAL_RATIO_BPS &amp;&amp; collateral_ratio_bps &lt;= MAX_COLLATERAL_RATIO_BPS, E_INVALID_COLLATERAL_RATIO ); // Validate liquidation ratio assert!( liquidation_ratio_bps &gt;= MIN_LIQUIDATION_RATIO_BPS, E_INVALID_LIQUIDATION_RATIO ); // SECURE: Validate relationship between ratios // Collateral must be higher than liquidation + buffer assert!( collateral_ratio_bps &gt;= liquidation_ratio_bps + LIQUIDATION_BUFFER_BPS, E_RATIOS_INCONSISTENT ); // Validate addresses assert!(treasury != @0x0, E_INVALID_ADDRESS); assert!(oracle != @0x0, E_INVALID_ADDRESS); let pool = LendingPool { id: object::new(ctx), interest_rate_bps, collateral_ratio_bps, liquidation_ratio_bps, treasury, oracle, }; let pool_id = object::id(&amp;pool); // Create admin cap for authorized updates let admin_cap = AdminCap { id: object::new(ctx), pool_id, }; transfer::share_object(pool); transfer::transfer(admin_cap, tx_context::sender(ctx)); } /// SECURE: Validated updates with admin authorization public entry fun update_rates( admin_cap: &amp;AdminCap, pool: &amp;mut LendingPool, new_interest_rate: u64, new_collateral_ratio: u64, _ctx: &amp;mut TxContext ) { // Verify admin assert!(admin_cap.pool_id == object::id(pool), E_NOT_ADMIN); // Validate new values assert!( new_interest_rate &lt;= MAX_INTEREST_RATE_BPS, E_INVALID_INTEREST_RATE ); assert!( new_collateral_ratio &gt;= MIN_COLLATERAL_RATIO_BPS &amp;&amp; new_collateral_ratio &gt;= pool.liquidation_ratio_bps + LIQUIDATION_BUFFER_BPS, E_INVALID_COLLATERAL_RATIO ); pool.interest_rate_bps = new_interest_rate; pool.collateral_ratio_bps = new_collateral_ratio; } } module secure::nft { use sui::object::{Self, UID}; use std::string::{Self, String}; use std::vector; const E_NAME_TOO_SHORT: u64 = 1; const E_NAME_TOO_LONG: u64 = 2; const E_DESCRIPTION_TOO_LONG: u64 = 3; const E_INVALID_URL: u64 = 4; const E_INVALID_RARITY: u64 = 5; const E_INVALID_POWER: u64 = 6; const E_INVALID_CHARACTERS: u64 = 7; const MIN_NAME_LENGTH: u64 = 3; const MAX_NAME_LENGTH: u64 = 64; const MAX_DESCRIPTION_LENGTH: u64 = 1024; const MAX_URL_LENGTH: u64 = 256; const MAX_RARITY: u64 = 5; const MAX_POWER: u64 = 100; public struct NFT has key, store { id: UID, name: String, description: String, image_url: String, rarity: u64, power: u64, } /// SECURE: Validates all NFT fields public fun create_nft( name: String, description: String, image_url: String, rarity: u64, power: u64, ctx: &amp;mut TxContext ): NFT { // Validate name let name_len = string::length(&amp;name); assert!(name_len &gt;= MIN_NAME_LENGTH, E_NAME_TOO_SHORT); assert!(name_len &lt;= MAX_NAME_LENGTH, E_NAME_TOO_LONG); assert!(is_valid_name(&amp;name), E_INVALID_CHARACTERS); // Validate description assert!(string::length(&amp;description) &lt;= MAX_DESCRIPTION_LENGTH, E_DESCRIPTION_TOO_LONG); // Validate URL assert!(string::length(&amp;image_url) &lt;= MAX_URL_LENGTH, E_INVALID_URL); assert!(is_valid_url(&amp;image_url), E_INVALID_URL); // Validate numeric fields assert!(rarity &lt;= MAX_RARITY, E_INVALID_RARITY); assert!(power &lt;= MAX_POWER, E_INVALID_POWER); NFT { id: object::new(ctx), name, description, image_url, rarity, power, } } /// Validate name contains only allowed characters fun is_valid_name(name: &amp;String): bool { let bytes = string::bytes(name); let len = vector::length(bytes); let mut i = 0; while (i &lt; len) { let byte = *vector::borrow(bytes, i); // Allow alphanumeric, space, hyphen, underscore let valid = (byte &gt;= 48 &amp;&amp; byte &lt;= 57) || // 0-9 (byte &gt;= 65 &amp;&amp; byte &lt;= 90) || // A-Z (byte &gt;= 97 &amp;&amp; byte &lt;= 122) || // a-z byte == 32 || byte == 45 || byte == 95; // space, -, _ if (!valid) { return false }; i = i + 1; }; true } /// Validate URL format (basic check) fun is_valid_url(url: &amp;String): bool { let bytes = string::bytes(url); let len = vector::length(bytes); // Must have minimum length for https://x if (len &lt; 10) { return false }; // Must start with https:// let https_prefix = b&#34;https://&#34;; let mut i = 0; while (i &lt; 8) { if (*vector::borrow(bytes, i) != *vector::borrow(&amp;https_prefix, i)) { return false }; i = i + 1; }; // Block dangerous schemes // (Already handled by requiring https://) true } } module secure::auction { use sui::object::{Self, UID}; use sui::clock::{Self, Clock}; const E_INVALID_START_TIME: u64 = 1; const E_INVALID_END_TIME: u64 = 2; const E_DURATION_TOO_SHORT: u64 = 3; const E_DURATION_TOO_LONG: u64 = 4; const E_INVALID_STARTING_PRICE: u64 = 5; const E_RESERVE_BELOW_START: u64 = 6; const E_INCREMENT_TOO_SMALL: u64 = 7; const MIN_DURATION_MS: u64 = 3600_000; // 1 hour const MAX_DURATION_MS: u64 = 604800_000; // 1 week const MIN_INCREMENT_BPS: u64 = 100; // 1% minimum increment public struct Auction has key { id: UID, start_time: u64, end_time: u64, starting_price: u64, reserve_price: u64, minimum_increment: u64, } /// SECURE: Validates time and price relationships public fun create_auction( start_time: u64, end_time: u64, starting_price: u64, reserve_price: u64, minimum_increment: u64, clock: &amp;Clock, ctx: &amp;mut TxContext ): Auction { let now = clock::timestamp_ms(clock); // Validate start time is in the future assert!(start_time &gt; now, E_INVALID_START_TIME); // Validate end time is after start time assert!(end_time &gt; start_time, E_INVALID_END_TIME); // Validate duration bounds let duration = end_time - start_time; assert!(duration &gt;= MIN_DURATION_MS, E_DURATION_TOO_SHORT); assert!(duration &lt;= MAX_DURATION_MS, E_DURATION_TOO_LONG); // Validate prices assert!(starting_price &gt; 0, E_INVALID_STARTING_PRICE); assert!(reserve_price &gt;= starting_price, E_RESERVE_BELOW_START); // Validate minimum increment let min_increment = (starting_price * MIN_INCREMENT_BPS) / 10000; assert!(minimum_increment &gt;= min_increment, E_INCREMENT_TOO_SMALL); Auction { id: object::new(ctx), start_time, end_time, starting_price, reserve_price, minimum_increment, } } } module secure::user { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; const E_USERNAME_TOO_SHORT: u64 = 1; const E_USERNAME_TOO_LONG: u64 = 2; const E_INVALID_USERNAME: u64 = 3; const MIN_USERNAME_LENGTH: u64 = 3; const MAX_USERNAME_LENGTH: u64 = 20; const STARTING_TIER: u8 = 0; const STARTING_POINTS: u64 = 0; public struct UserProfile has key { id: UID, owner: address, username: vector&lt;u8&gt;, tier: u8, points: u64, } /// SECURE: System-controlled tier and points public entry fun create_profile( username: vector&lt;u8&gt;, ctx: &amp;mut TxContext ) { // Validate username let len = vector::length(&amp;username); assert!(len &gt;= MIN_USERNAME_LENGTH, E_USERNAME_TOO_SHORT); assert!(len &lt;= MAX_USERNAME_LENGTH, E_USERNAME_TOO_LONG); assert!(is_valid_username(&amp;username), E_INVALID_USERNAME); let profile = UserProfile { id: object::new(ctx), owner: tx_context::sender(ctx), username, tier: STARTING_TIER, // SECURE: System-set, not user input points: STARTING_POINTS, // SECURE: System-set, not user input }; transfer::transfer(profile, tx_context::sender(ctx)); } /// SECURE: Only authorized upgrades public entry fun upgrade_tier( admin_cap: &amp;AdminCap, profile: &amp;mut UserProfile, new_tier: u8, ) { // Only admin can change tier profile.tier = new_tier; } fun is_valid_username(username: &amp;vector&lt;u8&gt;): bool { let len = vector::length(username); let mut i = 0; while (i &lt; len) { let byte = *vector::borrow(username, i); // Alphanumeric and underscore only let valid = (byte &gt;= 48 &amp;&amp; byte &lt;= 57) || // 0-9 (byte &gt;= 65 &amp;&amp; byte &lt;= 90) || // A-Z (byte &gt;= 97 &amp;&amp; byte &lt;= 122) || // a-z byte == 95; // _ if (!valid) { return false }; i = i + 1; }; true } } Validation Patterns Pattern 1: Range Validation const MIN_VALUE: u64 = 1; const MAX_VALUE: u64 = 1000; fun validate_range(value: u64) { assert!(value &gt;= MIN_VALUE &amp;&amp; value &lt;= MAX_VALUE, E_OUT_OF_RANGE); } Pattern 2: Relationship Validation fun validate_time_range(start: u64, end: u64, now: u64) { assert!(start &gt; now, E_START_IN_PAST); assert!(end &gt; start, E_END_BEFORE_START); assert!(end - start &lt;= MAX_DURATION, E_DURATION_TOO_LONG); } Pattern 3: Builder Pattern public struct ConfigBuilder { interest_rate: Option&lt;u64&gt;, collateral_ratio: Option&lt;u64&gt;, } public fun new_builder(): ConfigBuilder { ConfigBuilder { interest_rate: option::none(), collateral_ratio: option::none(), } } public fun with_interest_rate(builder: ConfigBuilder, rate: u64): ConfigBuilder { assert!(rate &lt;= MAX_RATE, E_INVALID_RATE); ConfigBuilder { interest_rate: option::some(rate), ..builder } } public fun build(builder: ConfigBuilder): Config { assert!(option::is_some(&amp;builder.interest_rate), E_MISSING_FIELD); assert!(option::is_some(&amp;builder.collateral_ratio), E_MISSING_FIELD); Config { interest_rate: option::extract(&amp;mut builder.interest_rate), collateral_ratio: option::extract(&amp;mut builder.collateral_ratio), } } Pattern 4: Whitelist Validation const ALLOWED_TIERS: vector&lt;u8&gt; = vector[0, 1, 2, 3]; fun validate_tier(tier: u8) { let mut valid = false; let len = vector::length(&amp;ALLOWED_TIERS); let mut i = 0; while (i &lt; len) { if (*vector::borrow(&amp;ALLOWED_TIERS, i) == tier) { valid = true; break }; i = i + 1; }; assert!(valid, E_INVALID_TIER); } Recommended Mitigations 1. Define Clear Bounds const MIN_VALUE: u64 = X; const MAX_VALUE: u64 = Y; 2. Validate All Inputs public fun create(value: u64): Object { validate_value(value); // Always validate Object { value } } 3. Validate Relationships assert!(end_time &gt; start_time, E_INVALID_TIME_RANGE); assert!(collateral &gt; liquidation, E_INVALID_RATIO); 4. System-Controlled Sensitive Fields // User provides: username // System controls: tier, points, permissions 5. Sanitize String Inputs assert!(is_valid_format(input), E_INVALID_FORMAT); assert!(length &lt;= MAX_LENGTH, E_TOO_LONG); Testing Checklist Test boundary values (min, max, min-1, max+1) Test zero values where inappropriate Test maximum length strings Test invalid characters in strings Test inconsistent relationship values Test that sensitive fields cannot be user-set Test validation error messages are informative Fuzz test with random inputs Related Vulnerabilities Numeric / Bitwise Pitfalls Access-Control Mistakes Unsafe BCS Parsing General Move Logic Errors</description>
    </item>
    <item>
      <title>32. Inefficient PTB Composition</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/inefficient-ptb-composition/index.html</link>
      <pubDate>Wed, 26 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/inefficient-ptb-composition/index.html</guid>
      <description>Overview Inefficient PTB (Programmable Transaction Block) composition occurs when transactions are structured in ways that waste gas, hit execution limits, or create unnecessary complexity. Sui’s PTB model allows composing multiple operations in a single transaction, but poor composition can lead to gas exhaustion attacks, failed transactions, or denial of service through resource exhaustion.&#xA;Risk Level Medium to High — Can lead to gas exhaustion, transaction failures, or denial of service.</description>
    </item>
    <item>
      <title>33. Overuse of Shared Objects</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/overuse-of-shared-objects/index.html</link>
      <pubDate>Thu, 15 May 2025 00:00:00 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/overuse-of-shared-objects/index.html</guid>
      <description>Overview Overuse of Shared Objects occurs when developers unnecessarily use shared objects where owned objects would suffice, or when they design systems with excessive sharing that creates contention, reduces throughput, and introduces security risks. In Sui, shared objects require consensus ordering while owned objects can be processed in parallel without consensus. Overusing shared objects not only degrades performance but can also introduce access control vulnerabilities and state manipulation risks.&#xA;Risk Level Severity: Medium to High</description>
    </item>
    <item>
      <title>34. Parent-Child Authority</title>
      <link>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/parent-child-authority/index.html</link>
      <pubDate>Thu, 15 May 2025 00:00:00 +0000</pubDate>
      <guid>https://f76f6398.frontier-scetrov-live.pages.dev/devsecops/vulns/parent-child-authority/index.html</guid>
      <description>Overview Parent-Child Authority vulnerabilities occur when developers make incorrect assumptions about the authority relationships between parent and child objects in Sui’s object model. In Sui, objects can own other objects (dynamic fields, wrapped objects), creating hierarchical relationships. Security issues arise when code assumes that access to a parent object automatically implies authorization over child objects, or conversely, when child objects can be manipulated independently in ways that violate intended invariants.</description>
    </item>
  </channel>
</rss>