Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ target
*.rs.bk
.DS_Store
.env
temp_auto_push.bat
.env.*
/target/
/target_local/
.md-*
hidden
.gitignore
branch_structure.json
21 changes: 17 additions & 4 deletions contracts/fee/src/test_ttl_bump.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ use soroban_sdk::testutils::storage::Instance as _;
use soroban_sdk::testutils::{Address as _, Ledger as _};
use soroban_sdk::{Address, Env};

fn read_ttl(env: &Env, contract_id: &Address) -> u32 {
env.as_contract(contract_id, || env.storage().instance().get_ttl())
}

fn setup_ttl() -> (Env, Address, Address, Address, FeeContractClient<'static>) {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let token = env.register_stellar_asset_contract(Address::generate(&env));
let contract_id = env.register_contract(None, FeeContract);
let client = FeeContractClient::new(&env, &contract_id);
(env, admin, token, contract_id, client)
}

fn setup() -> (Env, Address, FeeContractClient<'static>) {
let env = Env::default();
env.mock_all_auths();
Expand All @@ -20,10 +34,9 @@ fn setup() -> (Env, Address, FeeContractClient<'static>) {
#[test]
#[ignore = "stale test: fails against current contract; see docs/KNOWN_ISSUES.md"]
fn test_init_bumps_instance_ttl() {
let (env, admin, client) = setup();

client.init(&admin);
let ttl = env.storage().instance().get_ttl();
let (env, admin, token, contract_id, client) = setup_ttl();
client.init(&admin, &token, &0u32);
let ttl = read_ttl(&env, &contract_id);
assert_eq!(ttl, INSTANCE_BUMP_AMOUNT);
}

Expand Down
10 changes: 4 additions & 6 deletions contracts/settlement/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

use soroban_sdk::{Address, Env};

use crate::types::AdminMigrationEvent;
use crate::{events, timelock, CalloraSettlement, SettlementError, StorageKey};

fn require_admin(env: &Env, caller: &Address) {
Expand Down Expand Up @@ -115,11 +114,10 @@ pub(crate) fn execute_balance_migration(env: &Env, caller: &Address, from: &Addr
env,
from,
&migration.to.clone(),
AdminMigrationEvent {
from: from.clone(),
to: migration.to,
amount: migration.amount,
executed_at,
crate::types::AdminMigrationEvent {
old_admin: from.clone(),
new_admin: migration.to.clone(),
migrated_by: caller.clone(),
},
);
}
28 changes: 13 additions & 15 deletions contracts/settlement/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,30 +327,28 @@ pub struct DeveloperForceCreditedEvent {
pub token: Address,
}

/// Emitted when the admin proposes or executes a timelock'd developer balance
/// migration (address rotation).
#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub struct AdminMigrationEvent {
pub from: Address,
pub to: Address,
pub amount: i128,
pub executed_at: u64,
}

/// Emitted when the admin proposes a timelocked WASM upgrade.
/// Emitted when an admin proposes a timelocked WASM upgrade.
#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub struct UpgradeProposedEvent {
pub proposer: Address,
pub wasm_hash: soroban_sdk::BytesN<32>,
pub proposed_at: u64,
pub execute_after: u64,
}

/// Emitted when a pending WASM upgrade is cancelled by the admin.
/// Emitted when a pending WASM upgrade proposal is cancelled.
#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub struct UpgradeCancelledEvent {
pub canceller: Address,
pub wasm_hash: soroban_sdk::BytesN<32>,
pub cancelled_at: u64,
}

/// Emitted when an admin migration is recorded.
#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub struct AdminMigrationEvent {
pub old_admin: Address,
pub new_admin: Address,
pub migrated_by: Address,
}
4 changes: 2 additions & 2 deletions contracts/stake/tests/auth_snap.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#![cfg(test)]

#[cfg(test)]
extern crate std;

use callora_stake::{CalloraStake, CalloraStakeClient, SUPPORTED_CAPABILITIES};
Expand Down Expand Up @@ -47,6 +46,7 @@ fn capability_bits_map_to_entrypoints() {
let client = create_contract(&env);

let caps = client.capabilities();
assert_eq!(caps, 0);

// Bit 0 -> stake/unstake entrypoints must exist in the generated client.
// None are exported today, so the bit must be clear.
Expand Down
26 changes: 24 additions & 2 deletions contracts/upgrade/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
//! Enforces a cooldown period between critical upgrade actions to prevent
//! rapid abuse. This implements the requirements for the GrantFox FWC26 campaign.
//!
//! `check_and_record_upgrade` is restricted to the admin stored via [`init_admin`].
//! Any other signer receives `Unauthorized` without mutating state or emitting events.
//!
//! `set_cooldown` is restricted to the admin stored via [`init_admin`] and the
//! new value must lie in `MIN_COOLDOWN_SECONDS..=MAX_COOLDOWN_SECONDS`.
//!
Expand Down Expand Up @@ -112,10 +115,17 @@ pub fn get_cooldown(env: &Env) -> u64 {

/// Verify that the cooldown period has elapsed since the last upgrade.
///
/// Requires auth from the caller. Returns `Ok(())` if the cooldown has elapsed
/// or if this is the first upgrade. Otherwise, returns `UpgradeError::CooldownNotElapsed`.
/// Checks, in order: caller auth, admin is initialized, caller is the admin,
/// then the cooldown. Returns `Ok(())` if the cooldown has elapsed or if this
/// is the first upgrade. Otherwise, returns `UpgradeError::CooldownNotElapsed`.
/// Also updates the last upgrade time to the current ledger timestamp upon success.
///
/// # Errors
/// - `NotInitialized` if no admin is stored.
/// - `Unauthorized` if `caller` is not the stored admin.
/// - `CooldownNotElapsed` if the cooldown has not elapsed since the last upgrade.
/// - `Overflow` if the ledger timestamp is before the last upgrade time.
///
/// # Events
/// On success, emits two events in order:
/// 1. `upgrade_started` β€” topics `(upgrade_started, caller)`, data `(current_timestamp, cooldown)`.
Expand All @@ -127,6 +137,11 @@ pub fn get_cooldown(env: &Env) -> u64 {
pub fn check_and_record_upgrade(env: &Env, caller: &Address) -> Result<(), UpgradeError> {
caller.require_auth();

let admin = get_admin(env).ok_or(UpgradeError::NotInitialized)?;
if *caller != admin {
return Err(UpgradeError::Unauthorized);
}

let current_time = env.ledger().timestamp();
let last_time = env
.storage()
Expand Down Expand Up @@ -155,6 +170,13 @@ pub fn check_and_record_upgrade(env: &Env, caller: &Address) -> Result<(), Upgra
.instance()
.set(&Symbol::new(env, LAST_UPGRADE_TIME_KEY), &current_time);

// Keep the cooldown state live. The host contract owns instance TTL policy;
// this only refreshes the TTL of the instance entry holding the record.
env.storage().instance().extend_ttl(
crate::admin::INSTANCE_TTL_THRESHOLD,
crate::admin::INSTANCE_TTL_EXTEND,
);

// Timestamp is now persisted β€” confirm the record.
env.events()
.publish((events::event_upgrade_recorded(env), caller), current_time);
Expand Down
Loading
Loading