Skip to content
Closed
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
41 changes: 38 additions & 3 deletions contracts/allocation_logic/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ pub enum DataKey {
Strategy(String),
CommitmentCore,
Admin,
PendingAdmin,
Initialized,
ReentrancyGuard,
PoolRegistry, // Vec<u32> of all pool IDs
Expand Down Expand Up @@ -740,12 +741,46 @@ impl AllocationStrategiesContract {
read_version(&env)
}

/// Update admin (admin-only).
pub fn set_admin(env: Env, caller: Address, new_admin: Address) -> Result<(), Error> {
/// Return the pending admin, if a handoff has been proposed.
pub fn get_pending_admin(env: Env) -> Option<Address> {
env.storage().instance().get(&DataKey::PendingAdmin)
}

/// Propose a new admin (admin-only).
///
/// The proposed admin must call [`Self::accept_admin`] before control transfers.
pub fn propose_admin(env: Env, caller: Address, new_admin: Address) -> Result<(), Error> {
caller.require_auth();
Self::require_initialized(&env)?;
Self::require_admin(&env, &caller)?;
env.storage().instance().set(&DataKey::Admin, &new_admin);
env.storage().instance().set(&DataKey::PendingAdmin, &new_admin);
Ok(())
}

/// Accept a pending admin handoff.
///
/// Only the pending admin can finalize the transfer.
pub fn accept_admin(env: Env, caller: Address) -> Result<(), Error> {
caller.require_auth();
Self::require_initialized(&env)?;
let pending: Address = env
.storage()
.instance()
.get(&DataKey::PendingAdmin)
.ok_or(Error::Unauthorized)?;
if caller != pending {
return Err(Error::Unauthorized);
}
env.storage().instance().set(&DataKey::Admin, &caller);
env.storage().instance().remove(&DataKey::PendingAdmin);
Ok(())
}

/// Deprecated compatibility wrapper for proposing a new admin.
///
/// Does not transfer control until `new_admin` calls [`Self::accept_admin`].
pub fn set_admin(env: Env, caller: Address, new_admin: Address) -> Result<(), Error> {
Self::propose_admin(env, caller, new_admin)?;
Ok(())
}

Expand Down
36 changes: 36 additions & 0 deletions contracts/allocation_logic/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,42 @@ fn test_migrate_rejects_non_admin() {
assert_eq!(client.get_version(), CURRENT_VERSION);
}

#[test]
fn test_admin_handoff_requires_pending_admin_acceptance() {
let env = Env::default();
env.mock_all_auths();
let (admin, _core_id, client) = create_contract(&env);
let pending = Address::generate(&env);
let wrong = Address::generate(&env);

assert_eq!(client.try_propose_admin(&wrong, &pending), Err(Ok(Error::Unauthorized)));
assert_eq!(client.try_set_admin(&admin, &pending), Ok(Ok(())));
assert_eq!(client.get_pending_admin(), Some(pending.clone()));
assert_eq!(client.try_register_pool(&pending, &99, &RiskLevel::Low, &400, &1_000), Err(Ok(Error::Unauthorized)));
assert_eq!(client.try_accept_admin(&wrong), Err(Ok(Error::Unauthorized)));
assert_eq!(client.try_accept_admin(&pending), Ok(Ok(())));
assert_eq!(client.get_pending_admin(), None);
assert_eq!(client.try_register_pool(&admin, &99, &RiskLevel::Low, &400, &1_000), Err(Ok(Error::Unauthorized)));
assert_eq!(client.try_register_pool(&pending, &99, &RiskLevel::Low, &400, &1_000), Ok(Ok(())));
}

#[test]
fn test_admin_handoff_reproposal_overwrites_pending_admin() {
let env = Env::default();
env.mock_all_auths();
let (admin, _core_id, client) = create_contract(&env);
let first_pending = Address::generate(&env);
let second_pending = Address::generate(&env);

assert_eq!(client.try_propose_admin(&admin, &first_pending), Ok(Ok(())));
assert_eq!(client.get_pending_admin(), Some(first_pending.clone()));
assert_eq!(client.try_propose_admin(&admin, &second_pending), Ok(Ok(())));
assert_eq!(client.get_pending_admin(), Some(second_pending.clone()));
assert_eq!(client.try_accept_admin(&first_pending), Err(Ok(Error::Unauthorized)));
assert_eq!(client.try_accept_admin(&second_pending), Ok(Ok(())));
assert_eq!(client.get_pending_admin(), None);
}

// ============================================================================
// COMPREHENSIVE REBALANCE TESTS - Issue #236
// Focus: Owner Match, Strategy Persistence, Summary Correctness
Expand Down
45 changes: 42 additions & 3 deletions contracts/attestation_engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ pub enum AttestationError {
pub enum DataKey {
/// Admin address
Admin,
/// Proposed admin waiting to accept the handoff.
PendingAdmin,
/// Core contract address
CoreContract,
/// Verifier whitelist (Address -> bool)
Expand Down Expand Up @@ -499,10 +501,47 @@ impl AttestationEngineContract {
read_version(&e)
}

/// Update admin (admin-only).
pub fn set_admin(e: Env, caller: Address, new_admin: Address) -> Result<(), AttestationError> {
/// Return the pending admin, if a handoff has been proposed.
pub fn get_pending_admin(e: Env) -> Option<Address> {
e.storage().instance().get(&DataKey::PendingAdmin)
}

/// Propose a new admin (admin-only).
///
/// The proposed admin must call [`Self::accept_admin`] before control transfers.
pub fn propose_admin(
e: Env,
caller: Address,
new_admin: Address,
) -> Result<(), AttestationError> {
require_admin(&e, &caller)?;
e.storage().instance().set(&DataKey::Admin, &new_admin);
e.storage().instance().set(&DataKey::PendingAdmin, &new_admin);
Ok(())
}

/// Accept a pending admin handoff.
///
/// Only the pending admin can finalize the transfer.
pub fn accept_admin(e: Env, caller: Address) -> Result<(), AttestationError> {
caller.require_auth();
let pending: Address = e
.storage()
.instance()
.get(&DataKey::PendingAdmin)
.ok_or(AttestationError::Unauthorized)?;
if caller != pending {
return Err(AttestationError::Unauthorized);
}
e.storage().instance().set(&DataKey::Admin, &caller);
e.storage().instance().remove(&DataKey::PendingAdmin);
Ok(())
}

/// Deprecated compatibility wrapper for proposing a new admin.
///
/// Does not transfer control until `new_admin` calls [`Self::accept_admin`].
pub fn set_admin(e: Env, caller: Address, new_admin: Address) -> Result<(), AttestationError> {
Self::propose_admin(e, caller, new_admin)?;
Ok(())
}

Expand Down
63 changes: 63 additions & 0 deletions contracts/attestation_engine/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,69 @@ fn test_migrate_rejects_non_admin() {
assert_eq!(client.get_version(), 0);
}

#[test]
fn test_admin_handoff_requires_pending_admin_acceptance() {
let e = Env::default();
e.mock_all_auths();
let contract_id = e.register_contract(None, AttestationEngineContract);
let client = AttestationEngineContractClient::new(&e, &contract_id);
let admin = Address::generate(&e);
let pending = Address::generate(&e);
let wrong = Address::generate(&e);
let core = Address::generate(&e);

client.initialize(&admin, &core);

assert_eq!(
client.try_propose_admin(&wrong, &pending),
Err(Ok(AttestationError::Unauthorized))
);
assert_eq!(client.try_set_admin(&admin, &pending), Ok(Ok(())));
assert_eq!(client.get_admin(), admin);
assert_eq!(client.get_pending_admin(), Some(pending.clone()));
assert_eq!(
client.try_add_verifier(&pending, &pending),
Err(Ok(AttestationError::Unauthorized))
);
assert_eq!(
client.try_accept_admin(&wrong),
Err(Ok(AttestationError::Unauthorized))
);
assert_eq!(client.try_accept_admin(&pending), Ok(Ok(())));
assert_eq!(client.get_admin(), pending.clone());
assert_eq!(client.get_pending_admin(), None);
assert_eq!(
client.try_add_verifier(&admin, &admin),
Err(Ok(AttestationError::Unauthorized))
);
assert_eq!(client.try_add_verifier(&pending, &pending), Ok(Ok(())));
}

#[test]
fn test_admin_handoff_reproposal_overwrites_pending_admin() {
let e = Env::default();
e.mock_all_auths();
let contract_id = e.register_contract(None, AttestationEngineContract);
let client = AttestationEngineContractClient::new(&e, &contract_id);
let admin = Address::generate(&e);
let first_pending = Address::generate(&e);
let second_pending = Address::generate(&e);
let core = Address::generate(&e);

client.initialize(&admin, &core);

assert_eq!(client.try_propose_admin(&admin, &first_pending), Ok(Ok(())));
assert_eq!(client.get_pending_admin(), Some(first_pending.clone()));
assert_eq!(client.try_propose_admin(&admin, &second_pending), Ok(Ok(())));
assert_eq!(client.get_pending_admin(), Some(second_pending.clone()));
assert_eq!(
client.try_accept_admin(&first_pending),
Err(Ok(AttestationError::Unauthorized))
);
assert_eq!(client.try_accept_admin(&second_pending), Ok(Ok(())));
assert_eq!(client.get_admin(), second_pending);
}

#[test]
fn test_get_health_metrics_cross_reads_commitment_core_state() {
let e = Env::default();
Expand Down
41 changes: 38 additions & 3 deletions contracts/commitment_nft/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ pub struct TransferParams {
pub enum DataKey {
/// Admin address (singleton)
Admin,
/// Proposed admin waiting to accept the handoff.
PendingAdmin,
/// Counter for generating unique token IDs / Total supply
TokenCounter,
/// NFT data storage (token_id -> CommitmentNFT)
Expand Down Expand Up @@ -575,15 +577,48 @@ impl CommitmentNFTContract {
read_version(&e)
}

/// Update admin (admin-only).
pub fn set_admin(e: Env, caller: Address, new_admin: Address) -> Result<(), ContractError> {
/// Return the pending admin, if a handoff has been proposed.
pub fn get_pending_admin(e: Env) -> Option<Address> {
e.storage().instance().get(&DataKey::PendingAdmin)
}

/// Propose a new admin (admin-only).
///
/// The proposed admin must call [`Self::accept_admin`] before control transfers.
pub fn propose_admin(e: Env, caller: Address, new_admin: Address) -> Result<(), ContractError> {
require_admin(&e, &caller)?;

if is_zero_address(&e, &new_admin) {
return Err(ContractError::InvalidAddress);
}

e.storage().instance().set(&DataKey::Admin, &new_admin);
e.storage().instance().set(&DataKey::PendingAdmin, &new_admin);
Ok(())
}

/// Accept a pending admin handoff.
///
/// Only the pending admin can finalize the transfer.
pub fn accept_admin(e: Env, caller: Address) -> Result<(), ContractError> {
caller.require_auth();
let pending: Address = e
.storage()
.instance()
.get(&DataKey::PendingAdmin)
.ok_or(ContractError::NotAuthorized)?;
if caller != pending {
return Err(ContractError::NotAuthorized);
}
e.storage().instance().set(&DataKey::Admin, &caller);
e.storage().instance().remove(&DataKey::PendingAdmin);
Ok(())
}

/// Deprecated compatibility wrapper for proposing a new admin.
///
/// Does not transfer control until `new_admin` calls [`Self::accept_admin`].
pub fn set_admin(e: Env, caller: Address, new_admin: Address) -> Result<(), ContractError> {
Self::propose_admin(e, caller, new_admin)?;
Ok(())
}

Expand Down
51 changes: 51 additions & 0 deletions contracts/commitment_nft/src/smoke_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,57 @@ fn test_initialize_sets_admin_and_zero_supply() {
assert_eq!(client.total_supply(), 0);
}

#[test]
fn test_admin_handoff_requires_pending_admin_acceptance() {
let e = Env::default();
let (admin, client) = setup_contract(&e);
let pending = Address::generate(&e);
let wrong = Address::generate(&e);

assert_eq!(
client.try_propose_admin(&wrong, &pending),
Err(Ok(ContractError::NotAuthorized))
);
assert_eq!(client.try_set_admin(&admin, &pending), Ok(Ok(())));
assert_eq!(client.get_admin(), admin);
assert_eq!(client.get_pending_admin(), Some(pending.clone()));
assert_eq!(
client.try_add_authorized_contract(&pending, &pending),
Err(Ok(ContractError::NotAuthorized))
);
assert_eq!(
client.try_accept_admin(&wrong),
Err(Ok(ContractError::NotAuthorized))
);
assert_eq!(client.try_accept_admin(&pending), Ok(Ok(())));
assert_eq!(client.get_admin(), pending.clone());
assert_eq!(client.get_pending_admin(), None);
assert_eq!(
client.try_add_authorized_contract(&admin, &admin),
Err(Ok(ContractError::NotAuthorized))
);
assert_eq!(client.try_add_authorized_contract(&pending, &pending), Ok(Ok(())));
}

#[test]
fn test_admin_handoff_reproposal_overwrites_pending_admin() {
let e = Env::default();
let (admin, client) = setup_contract(&e);
let first_pending = Address::generate(&e);
let second_pending = Address::generate(&e);

assert_eq!(client.try_propose_admin(&admin, &first_pending), Ok(Ok(())));
assert_eq!(client.get_pending_admin(), Some(first_pending.clone()));
assert_eq!(client.try_propose_admin(&admin, &second_pending), Ok(Ok(())));
assert_eq!(client.get_pending_admin(), Some(second_pending.clone()));
assert_eq!(
client.try_accept_admin(&first_pending),
Err(Ok(ContractError::NotAuthorized))
);
assert_eq!(client.try_accept_admin(&second_pending), Ok(Ok(())));
assert_eq!(client.get_admin(), second_pending);
}

#[test]
fn test_mint_and_settle_as_core_updates_supply_and_activity() {
let e = Env::default();
Expand Down
Loading
Loading