diff --git a/contracts/calendar-contract/Cargo.toml b/contracts/calendar-contract/Cargo.toml new file mode 100644 index 0000000..4510c5e --- /dev/null +++ b/contracts/calendar-contract/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "calendar-contract" +version = "0.0.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib", "lib"] +doctest = false + +[dependencies] +soroban-sdk = { workspace = true } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } + +[profile.release] +opt-level = 'z' +overflow-checks = true +debug = 0 +strip = true +debug-assertions = false +panic = 'abort' +codegen-units = 1 +lto = true + +[profile.test] +opt-level = 0 +debug = true +debug-assertions = true +overflow-checks = true +lto = false +panic = 'unwind' +incremental = true +codegen-units = 256 +rpath = false + +[package.metadata.soroban] +generate-snapshots = false +snapshot-dir = "test_snapshots" +verbose-snapshots = false diff --git a/contracts/calendar-contract/src/contract.rs b/contracts/calendar-contract/src/contract.rs new file mode 100644 index 0000000..6f91fce --- /dev/null +++ b/contracts/calendar-contract/src/contract.rs @@ -0,0 +1,51 @@ +use crate::error::CalendarError; +use crate::events; +use crate::storage; +use soroban_sdk::{Address, BytesN, Env}; + +pub fn initialize( + env: &Env, + admin: &Address, + vault_address: &Address, +) -> Result<(), CalendarError> { + if storage::has_admin(env) { + return Err(CalendarError::AlreadyInitialized); + } + storage::set_admin(env, admin); + storage::set_vault_address(env, vault_address); + Ok(()) +} + +pub fn pause(env: &Env) -> Result<(), CalendarError> { + let admin = storage::get_admin(env).ok_or(CalendarError::NotInitialized)?; + admin.require_auth(); + storage::set_paused(env, true); + events::contract_paused(env, true); + Ok(()) +} + +pub fn unpause(env: &Env) -> Result<(), CalendarError> { + let admin = storage::get_admin(env).ok_or(CalendarError::NotInitialized)?; + admin.require_auth(); + storage::set_paused(env, false); + events::contract_paused(env, false); + Ok(()) +} + +pub fn transfer_admin(env: &Env, new_admin: &Address) -> Result<(), CalendarError> { + let admin = storage::get_admin(env).ok_or(CalendarError::NotInitialized)?; + admin.require_auth(); + if storage::is_paused(env) { + return Err(CalendarError::ContractPaused); + } + storage::set_admin(env, new_admin); + events::admin_transferred(env, &admin, new_admin); + Ok(()) +} + +pub fn upgrade_contract(env: &Env, new_wasm_hash: BytesN<32>) -> Result<(), CalendarError> { + let admin = storage::get_admin(env).ok_or(CalendarError::NotInitialized)?; + admin.require_auth(); + env.deployer().update_current_contract_wasm(new_wasm_hash); + Ok(()) +} diff --git a/contracts/calendar-contract/src/error.rs b/contracts/calendar-contract/src/error.rs new file mode 100644 index 0000000..b7afd48 --- /dev/null +++ b/contracts/calendar-contract/src/error.rs @@ -0,0 +1,10 @@ +use soroban_sdk::contracterror; + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum CalendarError { + NotInitialized = 1, + AlreadyInitialized = 2, + ContractPaused = 3, +} diff --git a/contracts/calendar-contract/src/events.rs b/contracts/calendar-contract/src/events.rs new file mode 100644 index 0000000..e5687b4 --- /dev/null +++ b/contracts/calendar-contract/src/events.rs @@ -0,0 +1,13 @@ +#![allow(deprecated)] +use soroban_sdk::{symbol_short, Address, Env}; + +pub fn contract_paused(env: &Env, paused: bool) { + let topics = (symbol_short!("paused"),); + env.events().publish(topics, paused); +} + +pub fn admin_transferred(env: &Env, old_admin: &Address, new_admin: &Address) { + let topics = (symbol_short!("adm_xfer"),); + env.events() + .publish(topics, (old_admin.clone(), new_admin.clone())); +} diff --git a/contracts/calendar-contract/src/lib.rs b/contracts/calendar-contract/src/lib.rs new file mode 100644 index 0000000..b781830 --- /dev/null +++ b/contracts/calendar-contract/src/lib.rs @@ -0,0 +1,38 @@ +#![no_std] + +mod contract; +mod error; +mod events; +mod storage; +#[cfg(test)] +mod test; +mod types; + +use crate::error::CalendarError; +use soroban_sdk::{contract, contractimpl, Address, BytesN, Env}; + +#[contract] +pub struct CalendarContract; + +#[contractimpl] +impl CalendarContract { + pub fn init(env: Env, admin: Address, vault_address: Address) -> Result<(), CalendarError> { + contract::initialize(&env, &admin, &vault_address) + } + + pub fn pause(env: Env) -> Result<(), CalendarError> { + contract::pause(&env) + } + + pub fn unpause(env: Env) -> Result<(), CalendarError> { + contract::unpause(&env) + } + + pub fn transfer_admin(env: Env, new_admin: Address) -> Result<(), CalendarError> { + contract::transfer_admin(&env, &new_admin) + } + + pub fn upgrade_contract(env: Env, new_wasm_hash: BytesN<32>) -> Result<(), CalendarError> { + contract::upgrade_contract(&env, new_wasm_hash) + } +} diff --git a/contracts/calendar-contract/src/storage.rs b/contracts/calendar-contract/src/storage.rs new file mode 100644 index 0000000..df52468 --- /dev/null +++ b/contracts/calendar-contract/src/storage.rs @@ -0,0 +1,46 @@ +use soroban_sdk::{contracttype, Address, Env}; + +#[contracttype] +#[derive(Clone)] +pub enum DataKey { + Admin, + VaultAddress, + IsPaused, +} + +// --- Admin --- + +pub fn has_admin(env: &Env) -> bool { + env.storage().instance().has(&DataKey::Admin) +} + +pub fn set_admin(env: &Env, admin: &Address) { + env.storage().instance().set(&DataKey::Admin, admin); +} + +pub fn get_admin(env: &Env) -> Option
{ + env.storage().instance().get(&DataKey::Admin) +} + +// --- Vault --- + +pub fn set_vault_address(env: &Env, vault: &Address) { + env.storage().instance().set(&DataKey::VaultAddress, vault); +} + +pub fn get_vault_address(env: &Env) -> Option
{ + env.storage().instance().get(&DataKey::VaultAddress) +} + +// --- Pause --- + +pub fn is_paused(env: &Env) -> bool { + env.storage() + .instance() + .get(&DataKey::IsPaused) + .unwrap_or(false) +} + +pub fn set_paused(env: &Env, paused: bool) { + env.storage().instance().set(&DataKey::IsPaused, &paused); +} diff --git a/contracts/calendar-contract/src/test.rs b/contracts/calendar-contract/src/test.rs new file mode 100644 index 0000000..7bf28f3 --- /dev/null +++ b/contracts/calendar-contract/src/test.rs @@ -0,0 +1,140 @@ +#![cfg(test)] + +use super::*; +use crate::error::CalendarError; +use soroban_sdk::{testutils::Address as _, testutils::Events, Address, Env, Symbol, TryIntoVal}; + +fn setup() -> (Env, Address, Address, CalendarContractClient<'static>) { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(CalendarContract, ()); + let client = CalendarContractClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let vault = Address::generate(&env); + (env, admin, vault, client) +} + +#[test] +fn test_initialize() { + let (_env, admin, vault, client) = setup(); + let res = client.try_init(&admin, &vault); + assert!(res.is_ok()); +} + +#[test] +fn test_initialize_twice_fails() { + let (_env, admin, vault, client) = setup(); + client.init(&admin, &vault); + let res = client.try_init(&admin, &vault); + assert_eq!(res, Err(Ok(CalendarError::AlreadyInitialized))); +} + +#[test] +fn test_pause() { + let (env, admin, vault, client) = setup(); + client.init(&admin, &vault); + client.pause(); + + let events = env.events().all(); + let last = events.last().unwrap(); + let topic: Symbol = last.1.get(0).unwrap().try_into_val(&env).unwrap(); + assert_eq!(topic, Symbol::new(&env, "paused")); +} + +#[test] +fn test_unpause() { + let (env, admin, vault, client) = setup(); + client.init(&admin, &vault); + client.pause(); + client.unpause(); + + let events = env.events().all(); + let last = events.last().unwrap(); + let topic: Symbol = last.1.get(0).unwrap().try_into_val(&env).unwrap(); + assert_eq!(topic, Symbol::new(&env, "paused")); +} + +#[test] +fn test_pause_not_initialized() { + let (_env, _admin, _vault, client) = setup(); + let res = client.try_pause(); + assert_eq!(res, Err(Ok(CalendarError::NotInitialized))); +} + +#[test] +fn test_transfer_admin() { + let (env, admin, vault, client) = setup(); + client.init(&admin, &vault); + let new_admin = Address::generate(&env); + let res = client.try_transfer_admin(&new_admin); + assert!(res.is_ok()); +} + +#[test] +fn test_transfer_admin_emits_event() { + let (env, admin, vault, client) = setup(); + client.init(&admin, &vault); + let new_admin = Address::generate(&env); + client.transfer_admin(&new_admin); + + let events = env.events().all(); + let last = events.last().unwrap(); + let topic: Symbol = last.1.get(0).unwrap().try_into_val(&env).unwrap(); + assert_eq!(topic, Symbol::new(&env, "adm_xfer")); +} + +#[test] +fn test_pause_blocks_transfer_admin() { + let (env, admin, vault, client) = setup(); + client.init(&admin, &vault); + client.pause(); + let new_admin = Address::generate(&env); + let res = client.try_transfer_admin(&new_admin); + assert_eq!(res, Err(Ok(CalendarError::ContractPaused))); +} + +#[test] +fn test_unpause_restores_transfer_admin() { + let (env, admin, vault, client) = setup(); + client.init(&admin, &vault); + client.pause(); + client.unpause(); + let new_admin = Address::generate(&env); + let res = client.try_transfer_admin(&new_admin); + assert!(res.is_ok()); +} + +#[test] +#[should_panic] +fn test_pause_requires_auth() { + let env = Env::default(); + let contract_id = env.register(CalendarContract, ()); + let client = CalendarContractClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let vault = Address::generate(&env); + + // Init with mocked auth + env.mock_all_auths(); + client.init(&admin, &vault); + + // Clear auth — pause should panic + env.mock_auths(&[]); + client.pause(); +} + +#[test] +#[should_panic] +fn test_transfer_admin_requires_auth() { + let env = Env::default(); + let contract_id = env.register(CalendarContract, ()); + let client = CalendarContractClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let vault = Address::generate(&env); + let new_admin = Address::generate(&env); + + env.mock_all_auths(); + client.init(&admin, &vault); + + env.mock_auths(&[]); + client.transfer_admin(&new_admin); +} diff --git a/contracts/calendar-contract/src/types.rs b/contracts/calendar-contract/src/types.rs new file mode 100644 index 0000000..e69de29