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
57 changes: 57 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ pub struct Proposal {
const EVENT_SNAP_CONFIG: Symbol = symbol_short!("snap_cfg");

const EVENT_INIT: Symbol = symbol_short!("init");
const EVENT_LAYOUT_VERSION: Symbol = symbol_short!("layout_v");
const EVENT_PAUSED: Symbol = symbol_short!("paused");
const EVENT_UNPAUSED: Symbol = symbol_short!("unpaused");
/// Versioned pause event carrying the tier (SoftPaused / HardPaused / NotPaused).
Expand Down Expand Up @@ -357,6 +358,8 @@ const EVENT_CLAIM_DELAY_SET: Symbol = symbol_short!("dly_set");
// ── Data structures ──────────────────────────────────────────
/// Contract version identifier (#23). Bumped when storage or semantics change; used for migration and compatibility.
pub const CONTRACT_VERSION: u32 = 23;
/// Persistent storage layout version. Bump when adding/renaming DataKey variants.
pub const STORAGE_LAYOUT_VERSION: u32 = 1;

#[contracttype]
#[derive(Clone, Debug, PartialEq)]
Expand Down Expand Up @@ -674,6 +677,8 @@ pub enum DataKey {
EventOnlyMode,
/// Last migrated storage version for upgrade hooks.
DeployedVersion,
/// Persistent storage layout version stamp. Set during `initialize` and migrations.
StorageLayoutVersion,

/// Platform fee in basis points.
PlatformFeeBps,
Expand Down Expand Up @@ -989,13 +994,40 @@ impl RevoraRevenueShare {

/// Returns error if contract is frozen (#32). Call at start of state-mutating entrypoints.
fn require_not_frozen(env: &Env) -> Result<(), RevoraError> {
// Ensure on-chain storage layout is compatible with this binary.
Self::assert_storage_layout_compatible(env)?;

let key = DataKey::Frozen;
if env.storage().persistent().get::<DataKey, bool>(&key).unwrap_or(false) {
return Err(RevoraError::ContractFrozen);
}
Ok(())
}

/// Ensure the on-chain storage layout is compatible with this binary.
///
/// - If the on-chain layout version is greater than the compiled `STORAGE_LAYOUT_VERSION`,
/// reject with `MigrationDowngradeNotAllowed`.
/// - If the on-chain layout version is absent or older, stamp the storage with the
/// compiled `STORAGE_LAYOUT_VERSION` and emit `EVENT_LAYOUT_VERSION` to signal migration.
fn assert_storage_layout_compatible(env: &Env) -> Result<(), RevoraError> {
let key = DataKey::StorageLayoutVersion;
if let Some(stored_v) = env.storage().persistent().get::<DataKey, u32>(&key) {
if stored_v > STORAGE_LAYOUT_VERSION {
return Err(RevoraError::MigrationDowngradeNotAllowed);
}
if stored_v < STORAGE_LAYOUT_VERSION {
env.storage().persistent().set(&key, &STORAGE_LAYOUT_VERSION);
env.events().publish((EVENT_LAYOUT_VERSION,), STORAGE_LAYOUT_VERSION);
}
} else {
// No layout stamp found: stamp it now (first-time initialize/migration path).
env.storage().persistent().set(&key, &STORAGE_LAYOUT_VERSION);
env.events().publish((EVENT_LAYOUT_VERSION,), STORAGE_LAYOUT_VERSION);
}
Ok(())
}

/// Returns true if the contract is in testnet mode (relaxed validation).
fn is_testnet_mode(env: Env) -> bool {
env.storage().persistent().get::<DataKey, bool>(&DataKey::TestnetMode).unwrap_or(false)
Expand Down Expand Up @@ -1605,6 +1637,25 @@ impl RevoraRevenueShare {
Ok(())
}

/// Read-only accessor for the on-chain storage layout version stamp.
pub fn storage_layout_version(env: Env) -> Option<u32> {
env.storage().persistent().get(&DataKey::StorageLayoutVersion)
}

/// Admin-only setter to adjust the stored layout version (used by migrations/tests).
/// Emits `EVENT_LAYOUT_VERSION` when the stored value is changed.
pub fn set_storage_layout_version(env: Env, caller: Address, v: u32) -> Result<(), RevoraError> {
let admin: Address =
env.storage().persistent().get(&DataKey::Admin).ok_or(RevoraError::NotInitialized)?;
admin.require_auth();
if caller != admin {
return Err(RevoraError::NotAuthorized);
}
env.storage().persistent().set(&DataKey::StorageLayoutVersion, &v);
env.events().publish((EVENT_LAYOUT_VERSION,), v);
Ok(())
}

pub fn get_pending_issuer_transfer(
env: Env,
issuer: Address,
Expand Down Expand Up @@ -1969,6 +2020,12 @@ impl RevoraRevenueShare {
env.storage().persistent().set(&DataKey::Paused, &PauseState::NotPaused);
let eo = event_only.unwrap_or(false);
env.storage().persistent().set(&DataKey2::ContractFlags, &(false, eo));
// Stamp storage layout version for future compatibility checks.
env.storage()
.persistent()
.set(&DataKey::StorageLayoutVersion, &STORAGE_LAYOUT_VERSION);
env.events().publish((EVENT_LAYOUT_VERSION,), STORAGE_LAYOUT_VERSION);

env.events().publish((EVENT_INIT, admin.clone()), (safety, eo));
}

Expand Down
59 changes: 59 additions & 0 deletions src/test_storage_layout_version.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#![cfg(test)]

use soroban_sdk::{testutils::Address as _, Address, Env};

use crate::{RevoraRevenueShareClient, RevoraError, RevoraRevenueShare, STORAGE_LAYOUT_VERSION};

#[test]
fn initialize_writes_storage_layout_version() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, RevoraRevenueShare);
let client = RevoraRevenueShareClient::new(&env, &contract_id);

let admin = Address::generate(&env);
client.initialize(&admin, &None::<Address>, &None::<bool>);

let v = client.storage_layout_version();
assert_eq!(v, Some(STORAGE_LAYOUT_VERSION));
}

#[test]
fn downgrade_attempt_is_rejected() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, RevoraRevenueShare);
let client = RevoraRevenueShareClient::new(&env, &contract_id);

let admin = Address::generate(&env);
client.initialize(&admin, &None::<Address>, &None::<bool>);

// Admin sets the on-chain layout to a newer value (simulating a migrated state)
client.set_storage_layout_version(&admin, &(STORAGE_LAYOUT_VERSION + 1)).unwrap();

// Any state-changing entrypoint should now be rejected due to newer on-chain layout.
let res = client.set_testnet_mode(&true);
match res {
Err(Ok(RevoraError::MigrationDowngradeNotAllowed)) => {}
other => panic!("expected MigrationDowngradeNotAllowed, got: {:?}", other),
}
}

#[test]
fn upgrade_path_allows_operation_and_stamps_layout() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, RevoraRevenueShare);
let client = RevoraRevenueShareClient::new(&env, &contract_id);

let admin = Address::generate(&env);
client.initialize(&admin, &None::<Address>, &None::<bool>);

// Simulate older on-chain layout (0) and ensure operations proceed and storage is stamped.
client.set_storage_layout_version(&admin, &0).unwrap();

// This should succeed and cause the contract to stamp the layout to the compiled value.
client.set_testnet_mode(&true).unwrap();
let v = client.storage_layout_version();
assert_eq!(v, Some(STORAGE_LAYOUT_VERSION));
}
Loading