Agent skill
package-version-safety
Trigger Pattern PACKAGE_UPGRADE flag (UpgradeCap detected, multiple package versions, upgrade policy references) - Inject Into Breadth agents, depth-external
Install this agent skill to your Project
npx add-skill https://github.com/PlamenTSV/plamen/tree/main/agents/skills/sui/package-version-safety
SKILL.md
Skill: PACKAGE_VERSION_SAFETY (Sui)
Trigger Pattern: PACKAGE_UPGRADE flag (UpgradeCap detected, multiple package versions, upgrade policy references) Inject Into: Breadth agents, depth-external Finding prefix:
[PV-N]Rules referenced: R4, R8, R9, R10
Sui packages are immutable once published. "Upgrading" a package means publishing a NEW version at a NEW on-chain address, linked to the original via the UpgradeCap lineage. The old version's code remains callable forever. This creates a fundamentally different upgrade risk model compared to EVM proxies: instead of replacing logic in-place, Sui packages accumulate versions -- and shared objects may be accessible by ALL versions simultaneously.
Trigger Patterns
UpgradeCap|upgrade_policy|package::make_immutable|compatible|additive|dep_only|version|
migrate|old_version|new_version
Step 1: Upgrade Policy Inventory
For each package in scope:
| # | Package | UpgradeCap Location | UpgradeCap Holder | Has store? |
Upgrade Policy | Destroyed? |
|---|---|---|---|---|---|---|
| 1 | {pkg_name} | {init function:line} | {address / shared / wrapped in governance} | YES/NO | {compatible/additive/dep_only} | YES (immutable) / NO |
Checks:
-
Where is UpgradeCap stored?
- Owned by deployer address: Single point of failure. Key loss -> permanent immutability. Key theft -> attacker can upgrade.
- Shared object: DANGEROUS -- anyone can pass it to upgrade functions.
- Wrapped in governance object: Good pattern -- upgrade requires governance approval.
- Destroyed via
make_immutable(): Package is permanently immutable. No upgrade risk. - Transferred to
@0x0or burn address: Effectively immutable.
-
Can UpgradeCap be transferred?
- UpgradeCap has
key + storeby default -> freely transferable viapublic_transfer. - Is there a custom wrapper restricting transfer? (e.g.,
GovernanceCapwrappingUpgradeCap) - If transferable and held by EOA -> attacker stealing key can transfer UpgradeCap.
- UpgradeCap has
-
Can UpgradeCap be destroyed?
sui::package::make_immutable(cap)consumes UpgradeCap -> permanent immutability.- If UpgradeCap has
dropvia wrapper -> accidental destruction possible.
1b. UpgradeCap Governance Assessment
| Governance Model | Risk Level | Assessment |
|---|---|---|
| Single EOA | CRITICAL | One key compromise replaces all package logic |
| Multisig (2/3 or lower) | HIGH | Low collusion threshold |
| Multisig (3/5+) | MEDIUM | Requires majority collusion |
| Multisig + timelock | LOW | Users can exit before malicious upgrade takes effect |
| DAO/governance contract | LOW | Distributed control, but check voter distribution |
| Destroyed (immutable) | NONE | Cannot upgrade, but also cannot patch bugs |
Step 2: Version Consistency Check
For shared objects created by this package:
| Shared Object | Created By (Version) | Current Version Field? | V1 Functions Access? | V2 Functions Access? | Consistency Risk |
|---|---|---|---|---|---|
| {obj_type} | V1 init() |
YES: version: u64 / NO |
{list funcs} | {list funcs} | {describe} |
What happens when package is upgraded?
- Existing shared objects created by V1 remain at their original address
- V2 functions CAN access V1-created shared objects (types are preserved in compatible upgrades)
- V1 functions are STILL callable and CAN access the same shared objects
- This dual-access is the primary version safety concern
Can old-version and new-version calls on same shared object create inconsistency?
- V1 function writes field A based on formula F1
- V2 function writes field A based on formula F2
- User calls V1 then V2 in separate transactions -> field A has inconsistent state
- Especially dangerous: V2 adds a new check that V1 lacks. Attacker calls V1 to bypass V2's check.
Step 3: Dependency Version Pinning
For each dependency in Move.toml:
| Dependency | Source | Pinned To | Immutable? | Upgrade Risk |
|---|---|---|---|---|
| Sui Framework | sui = "..." |
{git rev or latest} | Upgraded by validators | Framework upgrade could change behavior |
| MoveStdlib | MoveStdlib = "..." |
{git rev} | Upgraded with framework | Same as above |
| {third_party} | {git url or on-chain} | {specific rev / branch / on-chain version} | YES/NO | {describe} |
Checks:
- Are third-party dependencies pinned to specific git revisions? If pinned to
main-> upstream changes included on recompile. - For on-chain published dependencies: is the dependency package immutable? If it has active UpgradeCap -> behavior can change.
- Can a dependency upgrade break our package's invariants?
- Are there transitive dependencies with their own upgrade risks?
Can dependency upgrade break our package?
- Compatible dependency upgrade: function implementations can change but signatures preserved. Our calls still compile but behavior may differ.
- Additive dependency upgrade: only new functions/types added. Existing behavior frozen.
- Framework upgrades:
sui::*packages upgraded by validators. Can change Move VM behavior, gas costs, object model rules.
Step 4: Type Compatibility Across Versions
When package V2 adds new types or fields:
| Type | V1 Definition | V2 Changes | Compatible Upgrade Rule | Migration Needed? |
|---|---|---|---|---|
| {struct_name} | {fields} | {cannot change for compatible} | Struct layouts FROZEN | NO -- same layout |
| {new_struct} | N/A | {new in V2} | New types allowed | N/A |
Sui type rules for compatible upgrades:
- Existing struct field layouts CANNOT change (enforced by validator during upgrade)
- New structs CAN be added
- Existing function signatures CANNOT change
- Function bodies CAN change (this is where logic vulnerabilities occur)
- Generic type parameters must remain the same
Can V1 objects be used with V2 functions?
- YES for compatible upgrades: types are identical, V2 functions accept V1 objects.
- NO for separate package deployment: different package address = different types.
Can V2 objects be used with V1 functions?
- V2 does not create new object types that V1 knows about (V1 code is frozen).
- But V2 functions can modify shared objects that V1 functions then read -- state corruption possible.
Dynamic field implications:
- Dynamic fields keyed by type. If V2 changes key/value types for dynamic fields -> V1-era entries orphaned.
- Check: does V2 change any dynamic field key types?
Step 5: Upgrade Migration Safety
Does the package have migration functions to update shared objects from V1->V2 state?
| Migration Function | Trigger | What It Updates | Reversible? | Access Control |
|---|---|---|---|---|
| {migrate_func} | {admin call / automatic} | {version field, new state} | NO | {AdminCap / anyone} |
Version guard pattern: Shared objects contain version: u64. V1 functions check assert!(version == 1). V2 migration function sets version = 2. After migration, V1 functions abort because version != 1.
Check:
- Is version guard implemented? If NOT -> old functions remain callable indefinitely -> FINDING.
- Can migration be triggered by unauthorized parties?
- Is migration atomic? Can it be partially completed?
- What happens to user-owned objects during migration? (Owned objects cannot be modified by admin migration)
Step 6: UpgradeCap Governance
If UpgradeCap is owned by a single address:
| Risk | Description | Severity |
|---|---|---|
| Full logic replacement | Attacker upgrades package with malicious code. All shared objects now interact with attacker's logic. | CRITICAL |
| Subtle parameter change | Attacker upgrades to change a fee calculation or threshold in function body. Hard to detect. | HIGH |
| Dependency manipulation | Attacker upgrades to change dependency versions, pulling in vulnerable code. | HIGH |
| Policy escalation blocked | Upgrade policies can only be tightened (compatible -> additive -> dep_only -> immutable). Attacker cannot escalate from additive to compatible. | Mitigation |
Mitigations to check:
- Is UpgradeCap behind multisig?
- Is there an upgrade timelock (users can exit before upgrade takes effect)?
- Is there a multi-party approval mechanism for upgrades?
- Has the upgrade policy been tightened from the default
compatible? - Is
make_immutable()called ininit()for packages that should never upgrade?
Key Questions (Must Answer All)
- UpgradeCap security: Who holds it? What is the attack surface if compromised?
- Upgrade policy: Is the policy appropriate? Could it be tightened without losing needed functionality?
- Cross-version bypass: Can old functions bypass security checks added in new versions?
- Version guard: Is there a mechanism to disable old functions after upgrade?
- Type compatibility: Are all types compatible? Are dynamic fields accessible across versions?
Common False Positives
- Immutable package: UpgradeCap destroyed or
make_immutablecalled -> no upgrade risk - No shared objects: If old and new packages share no objects, cross-version interaction impossible
- Version guard implemented: Shared objects check version field, old functions abort after migration
- Capability migrated: Old package's capabilities consumed by new package, old functions uncallable
additiveordep_onlypolicy: Existing logic frozen (but new functions can still access shared objects -- check Step 3c)
Output Schema
## Finding [PV-N]: Title
**Verdict**: CONFIRMED / PARTIAL / REFUTED / CONTESTED
**Step Execution**: check1,2,3,4,5,6 | skip(reason) | uncertain
**Rules Applied**: [R4:___, R8:___, R9:___, R10:___]
**Severity**: Critical/High/Medium/Low/Info
**Location**: sources/{module}.move:LineN
**Upgrade Risk Type**: UPGRADECAP_MANAGEMENT / POLICY_INAPPROPRIATE / CROSS_VERSION_BYPASS / TYPE_INCOMPATIBILITY / DEPENDENCY_RISK / MISSING_VERSION_GUARD
**Package Version**: V{N} -> V{N+1}
**Shared Objects Affected**: {list}
**Description**: What is wrong
**Impact**: What can happen (logic replacement, security bypass, stranded assets, type mismatch)
**Evidence**: Code showing vulnerability
**Recommendation**: How to fix (tighten policy, add version guard, migrate capabilities, destroy UpgradeCap)
Step Execution Checklist (MANDATORY)
| Step | Required | Completed? | Notes |
|---|---|---|---|
| 1. Upgrade Policy Inventory | YES | UpgradeCap location, holder, policy | |
| 1b. UpgradeCap Governance Assessment | YES | Risk level for each package | |
| 2. Version Consistency Check | YES | All shared objects checked for dual-version access | |
| 3. Dependency Version Pinning | YES | Move.toml analyzed | |
| 4. Type Compatibility Across Versions | YES | Dynamic fields included | |
| 5. Upgrade Migration Safety | YES | Version guard pattern checked | |
| 6. UpgradeCap Governance | IF single-address holder | Multisig/timelock/approval checks |
Cross-Reference Markers
After Step 1: If UpgradeCap held by single address -> immediate finding (minimum HIGH).
After Step 2: If cross-version bypass possible -> cross-reference with MIGRATION_ANALYSIS Step 5 for shared object function enumeration.
After Step 3: Feed dependency risks to DEPENDENCY_AUDIT for transitive dependency analysis.
After Step 5: If no version guard AND shared objects hold user funds -> minimum HIGH finding.
If any step skipped, document valid reason (N/A, package is immutable, no shared objects, no third-party dependencies).
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
integration-hazard-research
Protocol Type Trigger NAMED_EXTERNAL_PROTOCOL (detected when recon finds import/interface for an identifiable external protocol — not standard libraries). Researches known integration hazards of the target protocol.
outcome-determinism
Protocol Type Trigger outcome_determinism - detected when EITHER of these code patterns are present - - Selection from finite depletable pool with fallback behavior (while(full)...
governance-attack-vectors
Protocol Type Trigger governance (detected when Governor, Timelock, voting, proposal, quorum, delegate patterns found) - Inject Into Breadth agents, depth-external, depth-edge-case
vault-accounting
Protocol Type Trigger vault (detected in recon TASK 0 Step 1) - Inject Into Core state agent OR economic design agent (merge via M4 hierarchy)
lending-protocol-security
Protocol Type Trigger lending (detected when recon finds liquidate|borrow|repay|collateral|lend|loan|LTV|healthFactor|interestRate|debtToken) - Inject Into Breadth agents, depth...
dex-integration-security
Protocol Type Trigger dex_integration (detected when recon finds swap|addLiquidity|removeLiquidity|IUniswapV2Router|ISwapRouter|amountOutMin|amountOutMinimum|slippage - AND the...
Didn't find tool you were looking for?