/contract-upgradeability
Trigger Pattern update_current_contract_wasm detected in codebase - Inject Into Breadth agents, depth-state-trace
$ npx -y skills add PlamenTSV/plamen --skill contract-upgradeability --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/contract-upgradeability
Context preview
The summary Claude sees to decide when to auto-load this skill.
Trigger Pattern update_current_contract_wasm detected in codebase - Inject Into Breadth agents, depth-state-trace
SKILL.md
contract-upgradeability.SKILL.mdname: "contract-upgradeability"
description: "Trigger Pattern update_current_contract_wasm detected in codebase - Inject Into Breadth agents, depth-state-trace"
CONTRACT_UPGRADEABILITY Skill (Soroban)
> **Trigger Pattern**: `update_current_contract_wasm` detected in codebase > **Inject Into**: Breadth agents, depth-state-trace > **Finding prefix**: `[CU-N]` > **Rules referenced**: R6, R10, R12, R13
Soroban provides `env.deployer().update_current_contract_wasm(new_wasm_hash)` for in-place contract upgrades. This is more powerful than EVM proxy patterns — it directly replaces the executing contract's WASM bytecode without changing the contract address or storage. Unrestricted upgrade capability is an absolute control vector; the upgrade gate must be airtight.
1. Upgrade Access Control
Locate every call to `update_current_contract_wasm` and verify the auth gate:
| Location | Auth Check Present? | Auth Address | Auth Type | Sufficient? | |----------|--------------------|--------------|-----------|-----------| | `{file:line}` | YES/NO | `{admin / multisig / NONE}` | `require_auth` / `require_auth_for_args` / NONE | YES/NO |
**Minimum requirement**: The upgrade function MUST call `require_auth()` or `require_auth_for_args()` on a privileged address before calling `update_current_contract_wasm`.
**Patterns to flag as insufficient**:
- No auth check at all (anyone can upgrade)
- Auth check on a non-admin address (e.g., any token holder)
- Auth check after `update_current_contract_wasm` is called (too late — code already replaced)
- The privileged address is stored in Temporary storage (can be deleted/expired, unlocking upgrade for anyone)
**Also check**:
- Is the admin address itself protected from replacement without auth? (see Section 3)
- Can the upgrade function be called during initialization before admin is set? (init race)
2. Migration Safety
An upgrade replaces WASM but preserves ALL storage. If the new WASM has a different storage layout or new required keys, the upgrade function must handle migration:
| Concern | Addressed? | Evidence | Risk if Not Addressed | |---------|-----------|----------|----------------------| | New storage keys initialized after upgrade | YES/NO | `{fn:line or NONE}` | Panics on first access of uninitialized key | | Removed storage keys cleaned up | YES/NO | `{fn:line or NONE}` | Bloat only (low risk unless size-bounded) | | Struct fields added/removed (ABI break) | YES/NO | `{fn:line or NONE}` | Deserialization panic on old data | | Version discriminator stored | YES/NO | `{fn:line or NONE}` | Cannot detect state of migration |
**Migration pattern check**:
pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) {
// Step 1: Auth gate
let admin = env.storage().instance().get::<_, Address>(&DataKey::Admin).unwrap();
admin.require_auth();
// Step 2: Replace WASM
env.deployer().update_current_contract_wasm(new_wasm_hash);
// Step 3: Migrate storage (if needed)
// env.storage().instance().set(&DataKey::NewField, &default_value);
}**Absence of migration**: If the upgraded WASM accesses storage keys or uses different struct layouts than the currently-stored data, ALL post-upgrade operations will panic. This is effectively a self-inflicted DoS on upgrade.
3. Admin Key Management
The admin address used to gate upgrades is itself a critical piece of state. Trace how it is set, updated, and protected:
| Operation | Location | Auth Required? | Two-Step Transfer? | Notes | |-----------|----------|---------------|-------------------|-------| | Initial admin set (init) | `{file:line}` | N/A (first call) | N/A | Is there a re-init guard? | | Admin transfer/update | `{file:line}` | YES/NO | YES/NO | Single-step is dangerous | | Admin stored in | `{DataKey}` | — | — | Instance/Persistent/Temporary? |
**Critical checks**:
- Admin stored as Temporary: if it expires, the contract becomes permanently non-upgradeable AND the upgrade slot is open to whoever sets themselves as admin via any unguarded init path
- Single-step admin transfer: `set_admin(new_admin)` without two-step handshake means a mistaken address transfer is irreversible
- Admin key not set on init: if the initialize function does not set the admin, the first caller of any admin function can claim admin
- Admin set to the zero address or contract address by accident
**Two-step transfer pattern (recommended)**:
// Step 1: current admin proposes new admin
pub fn propose_admin(env: Env, new_admin: Address) { ... }
// Step 2: new admin accepts
pub fn accept_admin(env: Env) { ... }4. Upgrade Event Emission
The Soroban host automatically emits a `contract_upgraded` system event on WASM replacement. However, the contract should also emit its own application-level event for indexer/monitoring visibility:
| Concern | Status | Notes | |---------|--------|-------| | Host system event auto-emitted | ALWAYS (host behavior) | Not controllable by contract | | Contract-level upgrade event emitted | YES/NO | Recommended for off-chain monitoring | | Event includes new WASM hash | YES/NO | Enables tracing what was deployed | | Event includes timestamp / ledger | YES/NO | Enables timeline reconstruction |
**Finding threshold**: Missing contract-level upgrade event is Low/Informational severity — the host event provides a baseline. Flag as Medium if the protocol's stated design includes monitoring hooks that depend on contract events.
5. Immutability Option
Some protocols intend to make contracts permanently immutable after a stabilization period. Check whether such a mechanism exists and is correctly implemented:
| Mechanism | Present? | Implementation | Correctness | |-----------|---------|---------------|------------| | Upgrade function can be permanently disabled | YES/NO | `{description or NONE}` | YES/NO/N/A | | Immutability flag stored | YES/NO | `{DataKey}` | Stored as Persistent? | | Immutability flag checked before upgrade | YES/
Read more
name: "contract-upgradeability" description: "Trigger Pattern update_current_contract_wasm detected in codebase - Inject Into Breadth agents, depth-state-trace"
CONTRACT_UPGRADEABILITY Skill (Soroban)
> **Trigger Pattern**: `update_current_contract_wasm` detected in codebase > **Inject Into**: Breadth agents, depth-state-trace > **Finding prefix**: `[CU-N]` > **Rules referenced**: R6, R10, R12, R13
Soroban provides `env.deployer().update_current_contract_wasm(new_wasm_hash)` for in-place contract upgrades. This is more powerful than EVM proxy patterns — it directly replaces the executing contract's WASM bytecode without changing the contract address or storage. Unrestricted upgrade capability is an absolute control vector; the upgrade gate must be airtight.
1. Upgrade Access Control
Locate every call to `update_current_contract_wasm` and verify the auth gate:
| Location | Auth Check Present? | Auth Address | Auth Type | Sufficient? | |----------|--------------------|--------------|-----------|-----------| | `{file:line}` | YES/NO | `{admin / multisig / NONE}` | `require_auth` / `require_auth_for_args` / NONE | YES/NO |
**Minimum requirement**: The upgrade function MUST call `require_auth()` or `require_auth_for_args()` on a privileged address before calling `update_current_contract_wasm`.
**Patterns to flag as insufficient**:
- No auth check at all (anyone can upgrade)
- Auth check on a non-admin address (e.g., any token holder)
- Auth check after `update_current_contract_wasm` is called (too late — code already replaced)
- The privileged address is stored in Temporary storage (can be deleted/expired, unlocking upgrade for anyone)
**Also check**:
- Is the admin address itself protected from replacement without auth? (see Section 3)
- Can the upgrade function be called during initialization before admin is set? (init race)
2. Migration Safety
An upgrade replaces WASM but preserves ALL storage. If the new WASM has a different storage layout or new required keys, the upgrade function must handle migration:
| Concern | Addressed? | Evidence | Risk if Not Addressed | |---------|-----------|----------|----------------------| | New storage keys initialized after upgrade | YES/NO | `{fn:line or NONE}` | Panics on first access of uninitialized key | | Removed storage keys cleaned up | YES/NO | `{fn:line or NONE}` | Bloat only (low risk unless size-bounded) | | Struct fields added/removed (ABI break) | YES/NO | `{fn:line or NONE}` | Deserialization panic on old data | | Version discriminator stored | YES/NO | `{fn:line or NONE}` | Cannot detect state of migration |
**Migration pattern check**:
pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) {
// Step 1: Auth gate
let admin = env.storage().instance().get::<_, Address>(&DataKey::Admin).unwrap();
admin.require_auth();
// Step 2: Replace WASM
env.deployer().update_current_contract_wasm(new_wasm_hash);
// Step 3: Migrate storage (if needed)
// env.storage().instance().set(&DataKey::NewField, &default_value);
}**Absence of migration**: If the upgraded WASM accesses storage keys or uses different struct layouts than the currently-stored data, ALL post-upgrade operations will panic. This is effectively a self-inflicted DoS on upgrade.
3. Admin Key Management
The admin address used to gate upgrades is itself a critical piece of state. Trace how it is set, updated, and protected:
| Operation | Location | Auth Required? | Two-Step Transfer? | Notes | |-----------|----------|---------------|-------------------|-------| | Initial admin set (init) | `{file:line}` | N/A (first call) | N/A | Is there a re-init guard? | | Admin transfer/update | `{file:line}` | YES/NO | YES/NO | Single-step is dangerous | | Admin stored in | `{DataKey}` | — | — | Instance/Persistent/Temporary? |
**Critical checks**:
- Admin stored as Temporary: if it expires, the contract becomes permanently non-upgradeable AND the upgrade slot is open to whoever sets themselves as admin via any unguarded init path
- Single-step admin transfer: `set_admin(new_admin)` without two-step handshake means a mistaken address transfer is irreversible
- Admin key not set on init: if the initialize function does not set the admin, the first caller of any admin function can claim admin
- Admin set to the zero address or contract address by accident
**Two-step transfer pattern (recommended)**:
// Step 1: current admin proposes new admin
pub fn propose_admin(env: Env, new_admin: Address) { ... }
// Step 2: new admin accepts
pub fn accept_admin(env: Env) { ... }4. Upgrade Event Emission
The Soroban host automatically emits a `contract_upgraded` system event on WASM replacement. However, the contract should also emit its own application-level event for indexer/monitoring visibility:
| Concern | Status | Notes | |---------|--------|-------| | Host system event auto-emitted | ALWAYS (host behavior) | Not controllable by contract | | Contract-level upgrade event emitted | YES/NO | Recommended for off-chain monitoring | | Event includes new WASM hash | YES/NO | Enables tracing what was deployed | | Event includes timestamp / ledger | YES/NO | Enables timeline reconstruction |
**Finding threshold**: Missing contract-level upgrade event is Low/Informational severity — the host event provides a baseline. Flag as Medium if the protocol's stated design includes monitoring hooks that depend on contract events.
5. Immutability Option
Some protocols intend to make contracts permanently immutable after a stabilization period. Check whether such a mechanism exists and is correctly implemented:
| Mechanism | Present? | Implementation | Correctness | |-----------|---------|---------------|------------| | Upgrade function can be permanently disabled | YES/NO | `{description or NONE}` | YES/NO/N/A | | Immutability flag stored | YES/NO | `{DataKey}` | Stored as Persistent? | | Immutability flag checked before upgrade | YES/
Autonomous Web3 security auditor for Claude Code and OpenAI Codex CLI. Orchestrates 18-100 AI agents across 40+ phases to produce audit reports with verified PoC exploits — for smart contracts and L1 node-client infrastructure.
Repo: PlamenTSV/plamen
Other skills on plamen.
- /ability-analysis
Trigger Pattern Always (Aptos Move) - foundational security check - Inject Into Breadth agents, depth agents
Open skill - /bit-shift-safety
Trigger Pattern Always (Aptos Move) - Move VM aborts on shift = bit width - Inject Into Breadth agents, depth-edge-case
Open skill - /centralization-risk
Trigger Protocol has privileged roles (admin, operator, governance, resource account owner) - Covers Single points of failure, privilege escalation, external governance dependen...
Open skill - /cross-chain-timing
Trigger Pattern wormhole|layerzero|ccip|bridge|cross_chain|vaa|guardian|emitter|relay|remote_chain|payload|nonce.sequence - Inject Into Breadth agents, depth-external
Open skill - /dependency-audit
Trigger EXTERNAL_LIB flag detected (protocol uses third-party Move dependencies) - Used by Breadth agents, depth-external
Open skill - /economic-design-audit
Trigger Pattern MONETARY_PARAMETER flag (required) - Inject Into Breadth agents (merged via M4 hierarchy)
Open skill

