Skip to content
Merged
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
89 changes: 89 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ pub enum RevoraError {
SignerKeyNotRegistered = 29,
/// Cross-contract token transfer failed.
TransferFailed = 30,
/// Contract is already at the target version; no migration needed.
AlreadyAtTargetVersion = 31,
/// Target version is lower than the current deployed version.
MigrationDowngradeNotAllowed = 32,
/// Admin rotation failed: new admin cannot be the same as current.
AdminRotationSameAddress = 33,
/// Admin rotation failed: another rotation is already pending.
AdminRotationPending = 34,
}

// ── Event symbols ────────────────────────────────────────────
Expand Down Expand Up @@ -5138,6 +5146,87 @@ impl RevoraRevenueShare {
(amount * fee_bps).checked_div(BPS_DENOMINATOR).unwrap_or(0)
}

/// Migrate the contract state to the current CONTRACT_VERSION.
///
/// This function is intended to be called by the admin after a WASM upgrade.
/// It executes versioned migration hooks sequentially from the last recorded version
/// to the current `CONTRACT_VERSION`.
///
/// Security properties:
/// - Only the current admin can call this.
/// - Respects the contract freeze state.
/// - Prevents downgrades or re-running the same migration.
/// - Updates `DataKey::DeployedVersion` only on success.
pub fn migrate(env: Env) -> Result<u32, RevoraError> {
Self::require_not_frozen(&env)?;

let admin: Address = env
.storage()
.persistent()
.get(&DataKey::Admin)
.ok_or(RevoraError::NotInitialized)?;

admin.require_auth();

let stored_version = env
.storage()
.persistent()
.get(&DataKey::DeployedVersion)
.unwrap_or(0u32);

if stored_version == CONTRACT_VERSION {
return Err(RevoraError::AlreadyAtTargetVersion);
}

if stored_version > CONTRACT_VERSION {
return Err(RevoraError::MigrationDowngradeNotAllowed);
}

// Run migration hooks sequentially
for version in (stored_version + 1)..=CONTRACT_VERSION {
Self::run_migration_hook(&env, version)?;
}

env.storage().persistent().set(&DataKey::DeployedVersion, &CONTRACT_VERSION);

env.events().publish(
(symbol_short!("migrated"), admin),
(stored_version, CONTRACT_VERSION),
);

Ok(CONTRACT_VERSION)
}

/// Internal helper to run migration logic for a specific version bump.
fn run_migration_hook(env: &Env, version: u32) -> Result<(), RevoraError> {
match version {
1 => {
// Initial version setup if needed (usually handled by initialize)
}
2 => {
// Example v2 migration logic
}
3 => {
// Example v3 migration logic
}
4 => {
// Example v4 migration logic
}
_ => {
// Future versions will be handled here
}
}
Ok(())
}

/// Return the current deployed version of the contract state.
pub fn get_deployed_version(env: Env) -> u32 {
env.storage()
.persistent()
.get(&DataKey::DeployedVersion)
.unwrap_or(0)
}

/// Return the current contract version (#23). Used for upgrade compatibility and migration.
pub fn get_version(env: Env) -> u32 {
let _ = env;
Expand Down
69 changes: 69 additions & 0 deletions src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8259,6 +8259,75 @@ mod regression {
assert_eq!(client.get_version(), v0);
}

// ---------------------------------------------------------------------------
// Versioned Migration Hooks (#44)
// ---------------------------------------------------------------------------

#[test]
fn test_migrate_success() {
let env = Env::default();
env.mock_all_auths();
let cid = env.register_contract(None, RevoraRevenueShare);
let client = RevoraRevenueShareClient::new(&env, &cid);
let admin = Address::generate(&env);
client.initialize(&admin, &None, &None);

// Initial deployed version should be 0 (since we didn't update initialize)
assert_eq!(client.get_deployed_version(), 0);

// Migrate to current CONTRACT_VERSION
let result = client.migrate();
assert_eq!(result, crate::CONTRACT_VERSION);
assert_eq!(client.get_deployed_version(), crate::CONTRACT_VERSION);
}

#[test]
#[should_panic(expected = "HostError: Error(Contract, #19)")]
fn test_migrate_unauthorized() {
let env = Env::default();
let cid = env.register_contract(None, RevoraRevenueShare);
let client = RevoraRevenueShareClient::new(&env, &cid);
let admin = Address::generate(&env);
client.initialize(&admin, &None, &None);

// Call migrate without auth (will fail because mock_all_auths is not set)
client.migrate();
}

#[test]
fn test_migrate_already_at_target() {
let env = Env::default();
env.mock_all_auths();
let cid = env.register_contract(None, RevoraRevenueShare);
let client = RevoraRevenueShareClient::new(&env, &cid);
let admin = Address::generate(&env);
client.initialize(&admin, &None, &None);

client.migrate();
assert_eq!(client.get_deployed_version(), crate::CONTRACT_VERSION);

// Try to migrate again
let result = client.try_migrate();
assert_eq!(result, Err(Ok(RevoraError::AlreadyAtTargetVersion)));
}

#[test]
fn test_migrate_respects_freeze() {
let env = Env::default();
env.mock_all_auths();
let cid = env.register_contract(None, RevoraRevenueShare);
let client = RevoraRevenueShareClient::new(&env, &cid);
let admin = Address::generate(&env);
client.initialize(&admin, &None, &None);

// Freeze contract
client.set_frozen(&true);

// Try to migrate
let result = client.try_migrate();
assert_eq!(result, Err(Ok(RevoraError::ContractFrozen)));
}

// ---------------------------------------------------------------------------
// Input parameter validation (#35)
// ---------------------------------------------------------------------------
Expand Down
Loading