[{"data":1,"prerenderedAt":20},["ShallowReactive",2],{"blog-detail:sui-move-security-why-legacy-packages-can-still-put-shared-objects-at-risk-after-an-upgrade-fb5a67":3},{"category":4,"article":5},null,{"slug":6,"category":7,"categoryLabel":8,"subcategoryLabel":9,"authorName":10,"title":11,"excerpt":12,"contentHtml":13,"coverUrl":14,"publishedAt":15,"updatedAt":16,"readTime":17,"cover":14,"hasCover":18,"coverAlt":19},"sui-move-security-why-legacy-packages-can-still-put-shared-objects-at-risk-after-an-upgrade-fb5a67","research","Research","Industry Research","MoveBit","Sui Move Security: Why Legacy Packages Can Still Put Shared Objects at Risk After an Upgrade","Upgrading a Sui Move Package does not automatically disable its previous versions. Each upgrade publishes a new Package ID, while earlier versions remain on-chain. If other modules, routers, or clients continue to call a legacy Package, and multiple versions can modify the same Shared Object, conflicting assumptions about shared state can introduce cross-version vulnerabilities.","\u003Ch1>The BlueMove DEX Exploit: Cross-Version Reserve Desynchronization and LP Inflation\u003C\u002Fh1>\n\u003Cp class=\"docx-align-justify\">\u003Cstrong>Author:\u003C\u002Fstrong> MoveBit (@MoveBit_)\u003Cbr>\u003Cstrong>Date:\u003C\u002Fstrong> October 21, 2026\u003Cbr>\u003Cstrong>Source:\u003C\u002Fstrong> \u003Ca href=\"https:\u002F\u002Fx.com\u002FMoveBit_\u002Fstatus\u002F2112781636664463510\" rel=\"nofollow\">X Post\u003C\u002Fa>\u003C\u002Fp>\n\u003Chr>\n\u003Ch2>Introduction: Why Upgrading a Package Does Not Eliminate Legacy Security Risks\u003C\u002Fh2>\n\u003Cp class=\"docx-align-justify\">Upgrading a Sui Move Package does not automatically disable its previous versions. Each upgrade publishes a new Package ID, while earlier versions remain on-chain. If other modules, routers, or clients continue to call a legacy Package, and multiple versions can modify the same Shared Object, conflicting assumptions about shared state can introduce cross-version vulnerabilities.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">This risk is particularly significant in DeFi protocols. Changes to asset storage, reserve accounting, or business logic can leave legacy execution paths operating under outdated assumptions. When different Package versions modify the same state, individually valid operations can combine to violate the protocol&#39;s accounting invariants and create exploitable discrepancies.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">On July 11, 2026, BlueMove, a DEX in the Sui ecosystem, suffered an exploit. A technical analysis published on the Sui Developer Forum in August identified inconsistent updates to the shared \u003Ccode>reserve_x\u003C\u002Fcode> field across callable Package versions as a key factor. The analysis estimated that approximately 714,000 SUI was drained over roughly 23 minutes, representing a loss of about $528,000 at the time.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">\u003Cstrong>The key takeaway:\u003C\u002Fstrong> securing a Sui upgrade requires verifying every still-callable Package version, not just the latest implementation. All versions must preserve the same set of Shared Object state invariants.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">MoveBit, a flagship sub-brand of BitsLab, examines the incident through the lens of Sui Package versioning, Shared Object security, and Move smart contract auditing. Drawing on publicly available bytecode decompilation and on-chain analysis, this article traces the reserve-accounting flaw, explains how it inflated LP minting and enabled withdrawals, and examines practical approaches to isolating legacy code and migrating state safely.\u003C\u002Fp>\n\u003Cblockquote>\n\u003Cp class=\"docx-align-justify\">BlueMove&#39;s original source code has not been publicly released. The technical analysis below is based on publicly available bytecode decompilation and on-chain transaction research. All code snippets are simplified illustrations of the underlying mechanisms, not verbatim reproductions of the project&#39;s source code.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Chr>\n\u003Ch2>1. Sui Package Upgrades: Why Legacy Code Remains Callable\u003C\u002Fh2>\n\u003Ch3>1.1 Each Upgrade Creates a New Package ID\u003C\u002Fh3>\n\u003Cp class=\"docx-align-justify\">Sui Move Packages are immutable on-chain code objects. An upgrade publishes a new Package version rather than modifying the existing version in place.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">A protocol may therefore have multiple deployed versions:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>V1\u003C\u002Fstrong> — Package ID: \u003Ccode>0x...\u003C\u002Fcode> (original)\u003C\u002Fli>\n\u003Cli>\u003Cstrong>V2\u003C\u002Fstrong> — Package ID: \u003Ccode>0x...\u003C\u002Fcode> (upgrade)\u003C\u002Fli>\n\u003Cli>\u003Cstrong>V3\u003C\u002Fstrong> — Package ID: \u003Ccode>0x...\u003C\u002Fcode> (latest)\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp class=\"docx-align-justify\">Each version has its own Package ID. A transaction invokes the version referenced by its call path. Dependencies that reference an older Package do not automatically switch to the latest version when an upstream Package is upgraded.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">Deploying V3 therefore does not, by itself, invalidate V1. If legacy functions remain callable and can access the relevant objects, they may continue to execute.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">\u003Cstrong>Reference:\u003C\u002Fstrong> \u003Ca href=\"https:\u002F\u002Fdocs.sui.io\u002Fconcepts\u002Fsui-move-concepts\u002Fpackages\u002Fupgrade\" rel=\"nofollow\">Sui Documentation: Package Versioning\u003C\u002Fa>\u003C\u002Fp>\n\u003Ch3>1.2 Packages Define Logic; Shared Objects Hold State\u003C\u002Fh3>\n\u003Cp class=\"docx-align-justify\">In an automated market maker (AMM), the Package defines swap, liquidity provision, withdrawal, and reserve-update logic. Shared Objects hold the pool&#39;s balances, reserves, and LP supply.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">The architecture can be represented as follows:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>┌─────────────────────────────────────────┐\n│              Shared Pool Object          │\n│  ┌─────────────────────────────────┐    │\n│  │ reserve_x \u002F reserve_y           │    │\n│  │ lp_supply                       │    │\n│  │ token_x \u002F token_y (or Escrow)   │    │\n│  └─────────────────────────────────┘    │\n└─────────────────────────────────────────┘\n         ↑                    ↑\n         │                    │\n    ┌────┴────┐          ┌────┴────┐\n    │  V1     │          │  V3     │\n    │ Package │          │ Package │\n    └─────────┘          └─────────┘\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp class=\"docx-align-justify\">When multiple Package versions can modify the same Pool, they must preserve consistent state invariants.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">Suppose V1 stores assets directly in \u003Ccode>pool.token_x\u003C\u002Fcode>, while V3 moves them into \u003Ccode>EscrowCoinsV2\u003C\u002Fcode>. If both versions update \u003Ccode>reserve_x\u003C\u002Fcode> using different balance sources, the reserve field can become inconsistent with the actual distribution of assets.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">Each version may behave as designed in isolation. The vulnerability emerges when their differing assumptions collide on the same Shared Object.\u003C\u002Fp>\n\u003Ch3>1.3 Upgrade Compatibility Does Not Guarantee Business-Logic Consistency\u003C\u002Fh3>\n\u003Cp class=\"docx-align-justify\">Sui&#39;s Package upgrade compatibility rules govern aspects such as public function signatures and struct layouts. Passing these checks does not guarantee that different versions preserve the same business invariants.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">For example, V1 may treat \u003Ccode>reserve_x\u003C\u002Fcode> as the Pool&#39;s directly held balance, while V3 treats it as the balance held in Escrow. If both versions can update the same field, its value may no longer have a consistent meaning across the protocol.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">A secure upgrade therefore requires more than checking the latest code. It also requires validating shared-state semantics, business invariants, and execution paths across Package versions.\u003C\u002Fp>\n\u003Chr>\n\u003Ch2>2. Root Cause: Two Package Versions Update \u003Ccode>reserve_x\u003C\u002Fcode> Using Different Balances\u003C\u002Fh2>\n\u003Cp class=\"docx-align-justify\">Public bytecode analysis identifies two callable Package versions as central to the BlueMove incident.\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>V1\u003C\u002Fstrong> stores assets in the Pool&#39;s \u003Ccode>token_x\u003C\u002Fcode> and \u003Ccode>token_y\u003C\u002Fcode> fields.\u003C\u002Fli>\n\u003Cli>The newer version uses \u003Ccode>EscrowCoinsV2\u003C\u002Fcode> for asset storage while continuing to maintain reserve values in shared Pool fields.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3>2.1 V1 Reserve-Update Logic\u003C\u002Fh3>\n\u003Cp class=\"docx-align-justify\">The relevant V1 logic can be represented as follows:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002F\u002F Simplified V1 reserve update\nfun update_reserves(pool: &amp;mut Pool) {\n    pool.reserve_x = pool.token_x.value();\n    pool.reserve_y = pool.token_y.value();\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp class=\"docx-align-justify\">V1 derives the reserves from the balances held directly in the Pool. After this update, the following invariant holds:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>pool.reserve_x == pool.token_x.value()\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp class=\"docx-align-justify\">This relationship remains valid as long as subsequent V1 calculations use the same balance source.\u003C\u002Fp>\n\u003Ch3>2.2 Reserve-Update Logic in the Newer Version\u003C\u002Fh3>\n\u003Cp class=\"docx-align-justify\">The newer version derives its reserves from Escrow:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002F\u002F Simplified newer version reserve update\nfun update_reserves(pool: &amp;mut Pool, escrow: &amp;EscrowCoinsV2) {\n    pool.reserve_x = escrow.token_x.value();\n    pool.reserve_y = escrow.token_y.value();\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp class=\"docx-align-justify\">This implementation maintains a different invariant:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>pool.reserve_x == escrow.token_x.value()\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp class=\"docx-align-justify\">The difference is fundamental:\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Version\u003C\u002Fth>\n\u003Cth>Balance Source for \u003Ccode>reserve_x\u003C\u002Fcode>\u003C\u002Fth>\n\u003Cth>Meaning of \u003Ccode>reserve_x\u003C\u002Fcode>\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>V1\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>pool.token_x\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Direct Pool balance\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Newer\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>escrow.token_x\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Escrow balance\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp class=\"docx-align-justify\">When the two balances differ, the versions can write different values into the same field.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">The last reserve update can change the meaning of \u003Ccode>reserve_x\u003C\u002Fcode>: V1 treats it as the Pool&#39;s direct balance, while the newer version treats it as the Escrow balance.\u003C\u002Fp>\n\u003Ch3>2.3 Why This Is a Cross-Version Vulnerability\u003C\u002Fh3>\n\u003Cp class=\"docx-align-justify\">Consider the following state:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>Escrow balance:       350,000 SUI\nPool direct balance:    7,022 SUI\nShared reserve_x:       7,022 SUI\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp class=\"docx-align-justify\">Here, \u003Ccode>reserve_x\u003C\u002Fcode> matches the Pool&#39;s direct balance but is roughly 50 times smaller than the Escrow balance.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">If the newer version uses \u003Ccode>reserve_x\u003C\u002Fcode> as the denominator in its LP minting formula, it can misinterpret the difference between these balances as newly added liquidity.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">V1&#39;s reserve update may be internally consistent, and the newer version&#39;s LP formula may be arithmetically correct. The flaw arises when the newer version interprets a value written under V1&#39;s accounting model as though it represented the Escrow balance.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">This is why cross-version security analysis must cover historical Packages, shared objects, and state transitions across functions rather than focusing exclusively on the latest implementation.\u003C\u002Fp>\n\u003Chr>\n\u003Ch2>3. How LP Minting Amplifies the Accounting Flaw\u003C\u002Fh2>\n\u003Ch3>3.1 LP Minting Must Reflect Actual Deposits\u003C\u002Fh3>\n\u003Cp class=\"docx-align-justify\">In a simplified AMM, the amount of LP tokens minted can be expressed as:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>ΔLP = S × (Δx \u002F R_x)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp class=\"docx-align-justify\">Where:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>Δx\u003C\u002Fcode> is the amount of assets actually deposited.\u003C\u002Fli>\n\u003Cli>\u003Ccode>S\u003C\u002Fcode> is the LP supply before minting.\u003C\u002Fli>\n\u003Cli>\u003Ccode>R_x\u003C\u002Fcode> is the corresponding reserve under the authoritative accounting model.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp class=\"docx-align-justify\">Both the deposit amount and the reserve must reflect the same asset state. If \u003Ccode>reserve_x\u003C\u002Fcode> is inconsistent with the actual reserve, the formula can produce an inflated result without any arithmetic overflow.\u003C\u002Fp>\n\u003Ch3>3.2 The BlueMove Flaw: Miscalculating the Deposit Amount\u003C\u002Fh3>\n\u003Cp class=\"docx-align-justify\">The relevant minting logic can be simplified as follows:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002F\u002F Simplified LP minting (newer version)\nfun mint_lp(pool: &amp;mut Pool, deposit_x: u64) -&gt; u64 {\n    let current_x = escrow.token_x.value();  \u002F\u002F Actual Escrow balance\n    let delta = current_x - pool.reserve_x;  \u002F\u002F Assumes reserve_x == previous Escrow\n    let lp_to_mint = pool.lp_supply * delta \u002F pool.reserve_x;\n    \u002F\u002F ...\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp class=\"docx-align-justify\">The calculation assumes that \u003Ccode>pool.reserve_x\u003C\u002Fcode> represents the Escrow balance before the deposit.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">If V1 has overwritten \u003Ccode>reserve_x\u003C\u002Fcode> with the Pool&#39;s direct balance, then:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>current_x\u003C\u002Fcode> = Escrow balance\u003C\u002Fli>\n\u003Cli>\u003Ccode>reserve_x\u003C\u002Fcode> = Pool direct balance\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp class=\"docx-align-justify\">The expression \u003Ccode>current_x - reserve_x\u003C\u002Fcode> no longer measures the assets deposited in the current transaction. Instead, it includes the historical difference between two separate balances.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">The denominator is also too small, further inflating the LP output.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">The attacker can therefore exploit inconsistent accounting semantics without triggering an integer overflow.\u003C\u002Fp>\n\u003Chr>\n\u003Ch2>4. Numerical Walkthrough: How a Small Deposit Can Produce Excessive LP Shares\u003C\u002Fh2>\n\u003Cp class=\"docx-align-justify\">The following figures illustrate the mechanism described in the public analysis. They are representative values, not a complete historical snapshot of a specific pool.\u003C\u002Fp>\n\u003Ch3>4.1 Initial State\u003C\u002Fh3>\n\u003Cpre>\u003Ccode>Escrow balance:   350,000 SUI\nPool balance:       7,022 SUI\nreserve_x:          7,022 SUI\nLP supply:          1,000\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp class=\"docx-align-justify\">A V1 swap updates \u003Ccode>reserve_x\u003C\u002Fcode> to the Pool&#39;s direct balance. Although Escrow holds 350,000 SUI, the newer version uses 7,022 SUI as its reserve baseline.\u003C\u002Fp>\n\u003Ch3>4.2 Step One: Add Liquidity Through the Newer Version\u003C\u002Fh3>\n\u003Cp class=\"docx-align-justify\">The attacker deposits approximately 0.057 SUI into the Escrow used by the newer version, alongside the corresponding amount of the other token.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">The updated Escrow balance becomes:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>350,000 + 0.057 = 350,000.057 SUI\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp class=\"docx-align-justify\">However, \u003Ccode>reserve_x\u003C\u002Fcode> remains at 7,022 SUI.\u003C\u002Fp>\n\u003Cpre>\u003Ccode>Δx (perceived) = 350,000.057 - 7,022 ≈ 342,978 SUI\nΔLP = 1,000 × (342,978 \u002F 7,022) ≈ 48,843 LP tokens\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp class=\"docx-align-justify\">A deposit of approximately 0.057 SUI thus produces around 48,843 LP tokens, roughly 48.8 times the existing LP supply.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">The problem is the incorrect comparison between Escrow&#39;s balance and the Pool&#39;s direct balance, not an arithmetic overflow.\u003C\u002Fp>\n\u003Ch3>4.3 Step Two: Withdraw Liquidity Through V1\u003C\u002Fh3>\n\u003Cp class=\"docx-align-justify\">According to the public analysis, the attack then calls V1&#39;s \u003Ccode>remove_liquidity\u003C\u002Fcode> function. The legacy withdrawal logic reads the Pool&#39;s direct balance rather than the Escrow balance.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">Its calculation can be simplified as follows:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>Attacker share = new_lp \u002F total_lp\nWithdrawal = pool.token_x.value() × attacker_share\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp class=\"docx-align-justify\">Using the representative figures:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>Existing LP supply = 1,000\nNew LP minted      = 48,843\nTotal LP supply    = 49,843\n\nAttacker share = 48,843 \u002F 49,843 ≈ 97.99%\n\nWithdrawal ≈ 7,022 × 0.9799 ≈ 6,881 SUI\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp class=\"docx-align-justify\">The attacker can therefore withdraw approximately 6,881 SUI from the Pool&#39;s direct balance.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">These assets come from \u003Ccode>pool.token_x\u003C\u002Fcode>, not directly from the 350,000 SUI held in Escrow. The Escrow balance inflates the LP minting calculation, allowing the attacker to claim a disproportionate share of the Pool&#39;s directly held assets.\u003C\u002Fp>\n\u003Ch3>4.4 Why the Attack Works Within a Single Transaction\u003C\u002Fh3>\n\u003Cp class=\"docx-align-justify\">The public analysis indicates that the attacker used a Programmable Transaction Block (PTB) to call the newer version&#39;s \u003Ccode>add_liquidity_returns\u003C\u002Fcode> function and then V1&#39;s \u003Ccode>remove_liquidity\u003C\u002Fcode> function within the same transaction.\u003C\u002Fp>\n\u003Cpre>\u003Ccode>PTB:\n  1. Call newer.add_liquidity_returns(...) → returns LP tokens\n  2. Call V1.remove_liquidity(LP tokens)   → withdraws from Pool\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp class=\"docx-align-justify\">The LP tokens returned by \u003Ccode>add_liquidity_returns\u003C\u002Fcode> can be passed to subsequent commands. This allows the attacker to combine the newer version&#39;s minting logic with the legacy withdrawal logic in a single transaction.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">The public analysis describes a transaction containing 49 programmable commands and repeated call sequences across multiple pools. It estimates that approximately 363 pools were affected, with total transfers of around 714,000 SUI.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">\u003Cstrong>Source:\u003C\u002Fstrong> \u003Ca href=\"https:\u002F\u002Fforums.sui.io\u002Ft\u002Fbluemove-was-not-an-overflow-bug-how-a-cross-version-reserve-desync-drained-714-000-sui\u002F49409\" rel=\"nofollow\">Sui Developer Forum: BlueMove Cross-Version Reserve Desynchronization Analysis\u003C\u002Fa>\u003C\u002Fp>\n\u003Chr>\n\u003Ch2>5. Why Destroying the UpgradeCap Does Not Disable Legacy Code\u003C\u002Fh2>\n\u003Cp class=\"docx-align-justify\">The public analysis reports that BlueMove&#39;s \u003Ccode>UpgradeCap\u003C\u002Fcode> was destroyed on June 3, 2026, while the exploit occurred on July 11.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">Destroying an \u003Ccode>UpgradeCap\u003C\u002Fcode> can permanently prevent further upgrades to a Package. It does not remove historical Packages from the blockchain or invalidate their functions. Legacy code can remain callable through its original Package ID.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">Upgrade authorization and legacy-code control are separate security concerns. Preventing further upgrades does not prevent an older version from modifying shared state.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">\u003Cstrong>Reference:\u003C\u002Fstrong> \u003Ca href=\"https:\u002F\u002Fdocs.sui.io\u002Fconcepts\u002Fsui-move-concepts\u002Fpackages\u002Fupgrade\" rel=\"nofollow\">Sui Documentation: Upgrading Packages\u003C\u002Fa>\u003C\u002Fp>\n\u003Chr>\n\u003Ch2>6. Remediation: Object Version Checks and Legacy-State Isolation\u003C\u002Fh2>\n\u003Cp class=\"docx-align-justify\">The appropriate remediation depends on whether the protocol incorporated version control into its Shared Object design from the outset.\u003C\u002Fp>\n\u003Ch3>6.1 Approach One: Add Object Version Checks in the Initial Design\u003C\u002Fh3>\n\u003Cp class=\"docx-align-justify\">Sui recommends designing Package version management into systems that interact with Shared Objects.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">The following example illustrates a version check:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>const VERSION: u64 = 1;\n\nstruct Pool has key {\n    id: UID,\n    version: u64,\n    \u002F\u002F ... other fields\n}\n\npublic fun swap(pool: &amp;mut Pool, ...) {\n    assert!(pool.version == VERSION, E_WRONG_VERSION);\n    \u002F\u002F ... swap logic\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp class=\"docx-align-justify\">When publishing a new version, the module&#39;s \u003Ccode>VERSION\u003C\u002Fcode> can be increased to 2, with a migration function protected by an appropriate capability:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>const VERSION: u64 = 2;\n\npublic fun migrate(pool: &amp;mut Pool, _: &amp;AdminCap) {\n    assert!(pool.version == 1, E_WRONG_VERSION);\n    pool.version = 2;\n    \u002F\u002F ... migrate state if needed\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp class=\"docx-align-justify\">These snippets illustrate the mechanism rather than provide a complete implementation. Production code must account for the actual module types, authorization rules, and migration requirements. Passing an \u003Ccode>AdminCap\u003C\u002Fcode> illustrates how a capability object can restrict migration privileges.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">After migration, the Pool&#39;s version becomes 2. A legacy function will abort on a version mismatch only if that version already checks \u003Ccode>pool.version == VERSION\u003C\u002Fcode> at the relevant entry points.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">\u003Cstrong>The critical limitation is that adding a version check to the new Package cannot change the behavior of legacy code that never implemented one.\u003C\u002Fstrong> If V1 lacks the check, adding it to V2 does not disable V1.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">Sui also imposes struct-layout compatibility requirements on upgrades. Developers cannot assume that arbitrary fields can be added to an already published struct. Version fields and migration mechanisms should therefore be considered during the initial design.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">\u003Cstrong>Reference:\u003C\u002Fstrong> \u003Ca href=\"https:\u002F\u002Fdocs.sui.io\u002Fconcepts\u002Fsui-move-concepts\u002Fpackages\u002Fupgrade\" rel=\"nofollow\">Sui Documentation: Upgrading Packages\u003C\u002Fa>\u003C\u002Fp>\n\u003Ch3>6.2 Approach Two: Introduce a New Object Type When Legacy Code Lacks Version Checks\u003C\u002Fh3>\n\u003Cp class=\"docx-align-justify\">If a deployed legacy version lacks version checks and its behavior cannot be changed through a compatible upgrade, the protocol may need to introduce a new object type and migrate the state into it.\u003C\u002Fp>\n\u003Cpre>\u003Ccode>struct PoolV2 has key {\n    id: UID,\n    \u002F\u002F ... new fields with correct accounting\n}\n\npublic fun migrate_to_v2(old_pool: Pool, _: &amp;AdminCap): PoolV2 {\n    \u002F\u002F Transfer assets, validate balances\n    \u002F\u002F Return new PoolV2\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp class=\"docx-align-justify\">The migration process should:\u003C\u002Fp>\n\u003Col>\n\u003Cli>Verify authorization through a protected administrative entry point.\u003C\u002Fli>\n\u003Cli>Transfer assets and required state from the legacy object to the new \u003Ccode>PoolV2\u003C\u002Fcode> object.\u003C\u002Fli>\n\u003Cli>Validate balances, reserves, and LP supply before and after migration.\u003C\u002Fli>\n\u003Cli>Emit migration events recording the relationship between the old and new objects.\u003C\u002Fli>\n\u003Cli>Update routers, SDKs, and other dependencies to use the new object.\u003C\u002Fli>\n\u003Cli>Deprecate the old object and prevent it from being used in normal protocol operations.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp class=\"docx-align-justify\">Creating \u003Ccode>PoolV2\u003C\u002Fcode> alone does not disable the old Package. The protocol must verify that legacy execution paths cannot affect the new authoritative state or leave users exposed to obsolete asset-handling functions.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">Sui&#39;s upgrade documentation discusses introducing new types and migration functions when legacy Shared Objects contain flawed logic. The new implementation can then accept only the new object type, breaking the compatibility relationship with the legacy object.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">Effective remediation must address Package versions, object types, state migration, and dependent callers together. Adding access controls to the latest version does not change the behavior of historical Packages.\u003C\u002Fp>\n\u003Chr>\n\u003Ch2>7. Regression Testing: Verifying the Cross-Version Fix\u003C\u002Fh2>\n\u003Cp class=\"docx-align-justify\">Testing should cover interactions between Package versions, not just the latest implementation. The goal is to verify that legacy execution paths cannot violate the protocol&#39;s state invariants.\u003C\u002Fp>\n\u003Ch3>7.1 Ensure Legacy Functions Cannot Operate on Migrated Objects\u003C\u002Fh3>\n\u003Cp class=\"docx-align-justify\">If object version checks are used, test the expected behavior:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>Given:\n    Pool.version = 2\n    V1.VERSION = 1\n\nWhen:\n    V1.swap(Pool)\n\nThen:\n    Transaction aborts with E_WRONG_VERSION\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp class=\"docx-align-justify\">This test is valid only if V1 already contains the version check. Testing V2 alone cannot prove that legacy code without such a check has been blocked.\u003C\u002Fp>\n\u003Ch3>7.2 Test Cross-Version Call Sequences\u003C\u002Fh3>\n\u003Cp class=\"docx-align-justify\">Test combinations such as:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>V1.swap\u003C\u002Fcode> → \u003Ccode>V2.add_liquidity\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>\u003Ccode>V2.swap\u003C\u002Fcode> → \u003Ccode>V1.remove_liquidity\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>\u003Ccode>V1.update\u003C\u002Fcode> → \u003Ccode>V2.mint\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>\u003Ccode>V2.migrate\u003C\u002Fcode> → \u003Ccode>V1.swap\u003C\u002Fcode>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp class=\"docx-align-justify\">Operations that are prohibited by design should abort or fail because of type isolation. Permitted combinations must preserve consistent asset balances, reserves, and LP supply.\u003C\u002Fp>\n\u003Ch3>7.3 Verify LP Minting Against Actual Deposits\u003C\u002Fh3>\n\u003Cp class=\"docx-align-justify\">Construct a test state similar to the one described in the BlueMove analysis:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>Escrow balance = 350,000 SUI\nPool balance   =   7,022 SUI\nreserve_x      =   7,022 SUI\nLP supply      =   1,000\nDeposit        =       0.057 SUI\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp class=\"docx-align-justify\">When \u003Ccode>reserve_x\u003C\u002Fcode> differs from the authoritative reserve, the protocol should reject unsafe minting or ensure that historical balance differences cannot be counted as newly deposited assets.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">Tests should verify the resulting LP amount against the actual deposit and the correct reserve baseline, rather than checking only whether the function executes successfully.\u003C\u002Fp>\n\u003Ch3>7.4 Verify Invariants Before and After Migration\u003C\u002Fh3>\n\u003Cp class=\"docx-align-justify\">For an AMM, regression tests should cover:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Actual asset balances held by the Pool and Escrow.\u003C\u002Fli>\n\u003Cli>Consistency between \u003Ccode>reserve_x\u003C\u002Fcode>, \u003Ccode>reserve_y\u003C\u002Fcode>, and the authoritative asset storage.\u003C\u002Fli>\n\u003Cli>Total LP supply and withdrawal proportions.\u003C\u002Fli>\n\u003Cli>Authorization requirements for migration functions.\u003C\u002Fli>\n\u003Cli>Potential asset loss or duplicate accounting if migration fails.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp class=\"docx-align-justify\">Tests should include successful and repeated migrations, unauthorized calls, residual legacy objects, and attempts to invoke legacy code after migration.\u003C\u002Fp>\n\u003Chr>\n\u003Ch2>8. Conclusion: Upgrade Security Must Cover Every Still-Callable Code Path\u003C\u002Fh2>\n\u003Cp class=\"docx-align-justify\">Sui&#39;s immutable Package model allows protocols to evolve while preserving historical code. When multiple versions can still access the same Shared Object, changes to storage structures, reserve calculations, and business rules can leave those versions operating against inconsistent state assumptions.\u003C\u002Fp>\n\u003Cp class=\"docx-align-justify\">Addressing this risk requires coordinated Package version management, Shared Object version checks, legacy-object migration, dependency updates, and cross-version regression testing. True upgrade security means verifying that every still-callable Package version preserves the same set of state invariants.\u003C\u002Fp>\n\n\n\u003Cp class=\"docx-align-justify\">BitsLab is a Web3 security company focused on smart contract auditing and security infrastructure for emerging blockchain ecosystems, including Sui, Aptos, Bitcoin, and TON. MoveBit, a flagship sub-brand of BitsLab, specializes in Move ecosystem security, providing smart contract audits and security research for projects building on Sui, Aptos, and other Move-based platforms.\u003C\u002Fp>\n\u003Ch3>Related Resources\u003C\u002Fh3>\n\u003Cp class=\"docx-align-justify\">\u003Cstrong>Sui Official Documentation\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fdocs.sui.io\u002Fconcepts\u002Fsui-move-concepts\u002Fpackages\u002Fupgrade\" rel=\"nofollow\">Upgrading Packages\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fdocs.sui.io\u002Fconcepts\u002Fsui-move-concepts\u002Fpackages\u002Fupgrade\" rel=\"nofollow\">Package Versioning\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fdocs.sui.io\u002Fguides\u002Fdeveloper\u002Fsui-101\u002Fsecurity\" rel=\"nofollow\">Security Best Practices\u003C\u002Fa>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp class=\"docx-align-justify\">\u003Cstrong>MoveBit Research\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fx.com\u002FMoveBit_\u002Fstatus\u002F2109486777723748495\" rel=\"nofollow\">Sui Shared Object Security: How Access Control Flaws Put DeFi Protocols at Risk\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fforums.sui.io\u002Ft\u002Fbluemove-was-not-an-overflow-bug-how-a-cross-version-reserve-desync-drained-714-000-sui\u002F49409\" rel=\"nofollow\">BlueMove Cross-Version Reserve Desynchronization Analysis\u003C\u002Fa>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp class=\"docx-align-justify\">\u003Cstrong>Official Websites\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fmovebit.xyz\u002F\" rel=\"nofollow\">MoveBit\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fwww.bitslab.xyz\u002F\" rel=\"nofollow\">BitsLab\u003C\u002Fa>\u003C\u002Fli>\n\u003C\u002Ful>","\u002Fmedia\u002Fmovebit\u002F2026\u002F10\u002Fc694b8c5b203afefafc2aefa5bd1a5c1.png","2026-10-22T02:22:49Z","2026-10-22T10:22:49Z","15 min read",true,"Sui Move Security: Why Legacy Packages Can Still Put Shared Objects at Risk After an Upgrade cover image",1792635791890]