Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions packages/package-policy/Move.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# @generated by Move, please check-in and do not edit manually.

[move]
version = 3
manifest_digest = "A5F43F46A0CE31873451CC2076FB7A253D89E2B8EDEB3EA0861F6EF455C5C7A8"
deps_digest = "F9B494B64F0615AED0E98FC12A85B85ECD2BC5185C22D30E7F67786BB52E507C"
dependencies = [
{ id = "Bridge", name = "Bridge" },
{ id = "MoveStdlib", name = "MoveStdlib" },
{ id = "Sui", name = "Sui" },
{ id = "SuiSystem", name = "SuiSystem" },
]

[[move.package]]
id = "Bridge"
source = { git = "https://github.com/MystenLabs/sui.git", rev = "2c930c25f8d3", subdir = "crates/sui-framework/packages/bridge" }

dependencies = [
{ id = "MoveStdlib", name = "MoveStdlib" },
{ id = "Sui", name = "Sui" },
{ id = "SuiSystem", name = "SuiSystem" },
]

[[move.package]]
id = "MoveStdlib"
source = { git = "https://github.com/MystenLabs/sui.git", rev = "2c930c25f8d3", subdir = "crates/sui-framework/packages/move-stdlib" }

[[move.package]]
id = "Sui"
source = { git = "https://github.com/MystenLabs/sui.git", rev = "2c930c25f8d3", subdir = "crates/sui-framework/packages/sui-framework" }

dependencies = [
{ id = "MoveStdlib", name = "MoveStdlib" },
]

[[move.package]]
id = "SuiSystem"
source = { git = "https://github.com/MystenLabs/sui.git", rev = "2c930c25f8d3", subdir = "crates/sui-framework/packages/sui-system" }

dependencies = [
{ id = "MoveStdlib", name = "MoveStdlib" },
{ id = "Sui", name = "Sui" },
]

[move.toolchain-version]
compiler-version = "1.51.5"
edition = "2024.beta"
flavor = "sui"
7 changes: 7 additions & 0 deletions packages/package-policy/Move.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "package_policy"
version = "0.0.1"
edition = "2024.beta"

[addresses]
package_policy = "0x0"
47 changes: 47 additions & 0 deletions packages/package-policy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Package Policy

This package implements a generic, enforceable, event-emitting custom upgrade policy for Sui Move packages. It provides a transparent, on-chain, and auditable process for managing package upgrades.

## 1. Motivation

The primary motivation for this package is to enhance the transparency and verifiability of smart contract governance on Sui. Standard package upgrades do not emit on-chain events, making it difficult for users and community members to track code changes.

This policy addresses that by ensuring every step of the upgrade process is accompanied by a descriptive on-chain event, aligning with a core philosophy of "do not trust, verify." By making governance actions explicit and observable, we build greater trust with our users.

Although developed for a specific use case, this package is intentionally generic and can be used to govern any Sui package.

## 2. Technical Implementation

This policy uses **Direct Wrapping** to implement an **object locking** pattern. This is a standard and secure method for creating enforceable custom upgrade policies by locking the real `sui::package::UpgradeCap` inside our custom `PolicyCap` object.

This pattern is detailed in the official Sui documentation:

- [Custom Upgrade Policies](https://docs.sui.io/concepts/sui-move-concepts/packages/custom-policies)
- [Wrapped Objects](https://docs.sui.io/concepts/object-ownership/wrapped)

The core components are:

- **`PolicyCap`**: A custom capability struct that "locks" the real `sui::package::UpgradeCap` inside it. The owner of this `PolicyCap` has the authority to upgrade the governed package.
- **Events**: The policy emits three distinct events to provide a full audit trail:
- `PolicyCreated`: Emitted once when an `UpgradeCap` is locked into the policy.
- `UpgradeAuthorized`: Emitted when an upgrade is initiated, signaling the intent to upgrade.
- `ContractUpgraded`: Emitted after the upgrade is finalized, recording the old and new package IDs.

### Lifecycle

1. **Locking the Policy**: To enforce this policy, the owner of a package performs a one-time call to `policy::create`, passing in their `UpgradeCap`. This function consumes the original `UpgradeCap` and returns a `PolicyCap` object to the owner.
2. **Performing Upgrades**: Once the policy is locked, all future upgrades must be performed by calling `policy::authorize_upgrade` and `policy::commit_upgrade`, which require the owner to present their `PolicyCap`. This guarantees that the corresponding events are always emitted.

## 3. Notes and Considerations

- **Enforcement is a One-Way Action**: Locking an `UpgradeCap` by calling `policy::create` is an irreversible action. Once the `UpgradeCap` is wrapped in a `PolicyCap`, it cannot be retrieved. The policy becomes mandatory for the lifetime of that package.

- **Security Model**: The security of any package governed by this policy rests entirely on the security of the account holding the `PolicyCap`.

### Hardening the Policy Contract

For maximum security and trust, the `package-policy` contract itself should be hardened. There are two primary strategies to achieve this, each offering different trade-offs:

1. **Self-Locking for Transparent Upgrades**: The owner of this `package-policy` contract can lock its own `UpgradeCap` inside a `PolicyCap` generated by this same module. This ensures that any future upgrades to the policy contract itself must follow the policy's rules, emitting events and providing full on-chain transparency. This approach maintains the flexibility to add new features (like timelocks) while ensuring all changes are auditable.

2. **Permanent Immutability for Absolute Finality**: For the strongest possible guarantee that the policy rules will _never_ change, the owner can make the `package-policy` contract immutable by calling `sui::package::make_immutable` on its own `UpgradeCap`. This action is irreversible and means no bug fixes or future enhancements can ever be deployed. This option should be considered if the policy is deemed absolutely complete and final.
104 changes: 104 additions & 0 deletions packages/package-policy/sources/policy.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/// This module implements a generic, enforceable, event-emitting custom upgrade policy.
/// The code within is general-purpose and can be used to govern
/// the upgradeability of any Sui package.
///
/// It uses the **Direct Wrapping** mechanism to implement an **object locking** pattern,
/// where the real `sui::package::UpgradeCap` is locked inside a custom `PolicyCap` object.
///
/// This implementation is based on the official Sui documentation:
/// - Custom Upgrade Policies: https://docs.sui.io/concepts/sui-move-concepts/packages/custom-policies
/// - Wrapped Objects: https://docs.sui.io/concepts/object-ownership/wrapped
///
/// To enforce this policy for a given package, its owner must
/// perform a one-time call to `policy::create`, which consumes the original
/// `UpgradeCap` and returns the `PolicyCap`.
/// All future upgrades for that package must then use the functions in this module.
module package_policy::policy;

use sui::event;
use sui::package::{Self, UpgradeCap, UpgradeReceipt, UpgradeTicket};

/// A custom capability object that wraps the real `UpgradeCap`.
/// The owner of this object has the authority to upgrade the target package
/// according to the rules in this module.
public struct PolicyCap has key, store {
id: UID,
/// The original UpgradeCap for the package this policy governs.
upgrade_cap: UpgradeCap,
}

/// Event emitted when a new PolicyCap is created, locking an UpgradeCap.
public struct PolicyCreated has copy, drop {
/// The ID of the newly created PolicyCap object.
policy_cap_id: ID,
/// The ID of the package this policy now governs.
governed_package_id: ID,
}

/// Event emitted when an upgrade is authorized, before it is committed.
public struct UpgradeAuthorized has copy, drop {
/// The ID of the PolicyCap object used for authorization.
policy_cap_id: ID,
/// The ID of the package being upgraded.
governed_package_id: ID,
/// The digest of the new package bytecode.
digest: vector<u8>,
}

/// Event emitted when the contract is upgraded.
public struct ContractUpgraded has copy, drop {
old_contract: ID,
new_contract: ID,
}

/// Creates the `PolicyCap`, locking the original `UpgradeCap` inside it.
/// Emits a `PolicyCreated` event.
/// The new `PolicyCap` is returned to be handled by the caller.
public fun create(upgrade_cap: UpgradeCap, ctx: &mut TxContext): PolicyCap {
let governed_package_id = upgrade_cap.package();

let policy_cap = PolicyCap {
id: object::new(ctx),
upgrade_cap,
};

event::emit(PolicyCreated {
policy_cap_id: object::id(&policy_cap),
governed_package_id,
});

policy_cap
}

/// Authorize a contract upgrade, emitting an `UpgradeAuthorized` event.
/// This returns an `UpgradeTicket` and requires the `PolicyCap`.
public fun authorize_upgrade(
policy_cap: &mut PolicyCap,
policy: u8,
digest: vector<u8>,
): UpgradeTicket {
event::emit(UpgradeAuthorized {
policy_cap_id: object::id(policy_cap),
governed_package_id: policy_cap.upgrade_cap.package(),
digest,
});

package::authorize_upgrade(&mut policy_cap.upgrade_cap, policy, digest)
}

/// Finalize an upgrade and emit the `ContractUpgraded` event.
/// This consumes the `UpgradeReceipt` and requires the `PolicyCap`.
public fun commit_upgrade(policy_cap: &mut PolicyCap, receipt: UpgradeReceipt) {
// Get the old package ID from the cap before it's mutated.
let old_contract = policy_cap.upgrade_cap.package();

// Commit the upgrade. This consumes the receipt and mutates the
// `upgrade_cap` field, updating its `package` field to the new ID.
package::commit_upgrade(&mut policy_cap.upgrade_cap, receipt);

// Get the new package ID from the now-updated cap.
let new_contract = policy_cap.upgrade_cap.package();

// Emit an event reflecting the package ID change.
event::emit(ContractUpgraded { old_contract, new_contract });
}