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
41 changes: 41 additions & 0 deletions contracts/calendar-contract/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
51 changes: 51 additions & 0 deletions contracts/calendar-contract/src/contract.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
10 changes: 10 additions & 0 deletions contracts/calendar-contract/src/error.rs
Original file line number Diff line number Diff line change
@@ -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,
}
13 changes: 13 additions & 0 deletions contracts/calendar-contract/src/events.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#![allow(deprecated)]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

soroban-sdk symbol_short deprecated alternative 2025

💡 Result:

symbol_short! macro in soroban-sdk is not deprecated. It remains the recommended way to create short Symbol constants (up to 9 characters) at compile time for efficiency. For longer symbols (up to 32 characters), use Symbol::new(&env, "string"). Official docs.rs documentation for latest soroban-sdk v25.3.0 (published 2026-03-07) actively documents and exemplifies symbol_short! usage. Migration guides (v23->v25, v22->v23) list other breaking changes but do not mention deprecation of symbol_short!. Some older Symbol::short method may be deprecated in favor of the macro (per secondary docs), but the macro itself is current best practice. No 2025-specific deprecation found in changelogs, releases, or official Stellar sources.

Citations:


🏁 Script executed:

cat -n contracts/calendar-contract/src/events.rs

Repository: LightForgeHub/SkillSphere-Contracts

Length of output: 591


Remove the unnecessary #![allow(deprecated)] attribute.

The symbol_short! macro is not deprecated in soroban-sdk v25.3.0 and remains the recommended way to create short Symbol constants. This module does not contain any deprecated code, so the attribute can be safely removed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/calendar-contract/src/events.rs` at line 1, Remove the unnecessary
crate attribute by deleting the top-level #![allow(deprecated)] in events.rs;
this module contains no deprecated items (symbol_short! from soroban-sdk v25.3.0
is not deprecated), so simply remove that attribute line to avoid suppressing
warnings incorrectly and keep the file clean.

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()));
}
Comment on lines +4 to +13
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing contract_upgraded event per issue #56.

The linked issue #56 specifies three events: contract_paused, admin_transferred, and contract_upgraded. Only the first two are implemented here. The upgrade_contract function in contract.rs currently performs the upgrade without emitting an event.

Proposed addition for contract_upgraded event
+pub fn contract_upgraded(env: &Env, new_wasm_hash: &soroban_sdk::BytesN<32>) {
+    let topics = (symbol_short!("upgraded"),);
+    env.events().publish(topics, new_wasm_hash.clone());
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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()));
}
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()));
}
pub fn contract_upgraded(env: &Env, new_wasm_hash: &soroban_sdk::BytesN<32>) {
let topics = (symbol_short!("upgraded"),);
env.events().publish(topics, new_wasm_hash.clone());
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/calendar-contract/src/events.rs` around lines 4 - 13, Add a new
contract_upgraded event function and emit it from the upgrade path: implement
pub fn contract_upgraded(env: &Env, old_code_hash: &BytesN<32>, new_code_hash:
&BytesN<32>) { let topics = (symbol_short!("contract_upgraded"),);
env.events().publish(topics, (old_code_hash.clone(), new_code_hash.clone())); }
(use the same pattern as contract_paused and admin_transferred) and call
contract_upgraded(...) from the upgrade_contract function in contract.rs at the
point where the upgrade is applied, passing the previous and new code-hash
values so the upgrade is recorded.

38 changes: 38 additions & 0 deletions contracts/calendar-contract/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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)
}
}
46 changes: 46 additions & 0 deletions contracts/calendar-contract/src/storage.rs
Original file line number Diff line number Diff line change
@@ -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<Address> {
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<Address> {
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);
Comment on lines +13 to +45
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

soroban instance storage extend_ttl best practices 2025

💡 Result:

Best practices for extending TTL on Soroban instance storage, based on official Rust SDK documentation: Instance storage is for small, contract-level data like admin addresses, config, metadata (keep under ~100KB, ideally <10KB). It shares TTL with the contract instance and code; expiration makes the entire contract inaccessible. Use env.storage.instance.extend_ttl(threshold, extend_to): - threshold: Extend only if current TTL < this (in ledgers, e.g., 100_000 ~5.5 days, 518_400 ~30 days) - extend_to: New TTL (e.g., 5_250_000 ~1 year). Do not exceed env.storage.max_ttl. Key practices: 1. Extend regularly: On initialization, admin ops, every interaction for critical contracts. 2. Use threshold to avoid redundant extensions. 3. Guidelines: Short-lived: 100k ledgers; Medium: 1M; Long-term/critical: 5.25M+. 4. Example: pub fn bump_instance(env: Env) { env.storage.instance.extend_ttl(518_400, 5_250_000); } Extend proactively to avoid restoration costs. Unlike persistent/temp, no key param—extends whole instance/code. No 2025-specific changes noted; practices from current SDK (2026-relevant). Official Stellar docs pages were 404, but SDK sources authoritative.

Citations:


🏁 Script executed:

# First, find and read the full storage.rs file to see complete context
find . -name "storage.rs" -path "*/calendar-contract/*" | head -5

Repository: LightForgeHub/SkillSphere-Contracts

Length of output: 123


🏁 Script executed:

# Search for extend_ttl calls in the entire contract codebase
rg "extend_ttl" --type rust

Repository: LightForgeHub/SkillSphere-Contracts

Length of output: 411


🏁 Script executed:

# Check contract initialization and admin operations
rg -A 5 "pub fn set_admin|pub fn get_admin|pub fn new|fn init" --type rust contracts/

Repository: LightForgeHub/SkillSphere-Contracts

Length of output: 7125


🏁 Script executed:

# Read the full calendar-contract storage.rs to see complete context
cat -n contracts/calendar-contract/src/storage.rs

Repository: LightForgeHub/SkillSphere-Contracts

Length of output: 1389


🏁 Script executed:

# Check how identity-registry-contract implements TTL extension
rg -B 5 -A 10 "extend_ttl" contracts/identity-registry-contract/src/storage.rs | head -40

Repository: LightForgeHub/SkillSphere-Contracts

Length of output: 1298


🏁 Script executed:

# Check calendar-contract initialization function for TTL calls
cat -n contracts/calendar-contract/src/contract.rs | head -100

Repository: LightForgeHub/SkillSphere-Contracts

Length of output: 2030


🏁 Script executed:

# Check if other calendar-contract contracts have extend_ttl for instance storage
cat -n contracts/payment-vault-contract/src/storage.rs | head -80

Repository: LightForgeHub/SkillSphere-Contracts

Length of output: 3217


🏁 Script executed:

# Check if reputation-scoring-contract has extend_ttl for instance storage
cat -n contracts/reputation-scoring-contract/src/storage.rs | head -80

Repository: LightForgeHub/SkillSphere-Contracts

Length of output: 1148


Instance storage TTL extension is missing and critical for contract longevity.

Soroban instance storage entries expire after their TTL if not extended. Without calling extend_ttl(), the contract's admin address, vault address, and pause state will become inaccessible when the instance storage TTL expires, rendering the entire contract unusable. Extending TTL on initialization and during admin operations is a required Soroban best practice.

Add an extend_instance_ttl() function and call it during initialization and in critical admin paths (initialize, transfer_admin, pause, unpause):

Proposed TTL extension pattern
+const INSTANCE_LIFETIME_THRESHOLD: u32 = 17280; // ~1 day
+const INSTANCE_BUMP_AMOUNT: u32 = 518400;       // ~30 days
+
+pub fn extend_instance_ttl(env: &Env) {
+    env.storage()
+        .instance()
+        .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT);
+}

Then call extend_instance_ttl(env) in initialize() and after get_admin() reads in contract operations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/calendar-contract/src/storage.rs` around lines 13 - 45, Add a
helper extend_instance_ttl(env: &Env) that calls
env.storage().instance().extend_ttl(...) and update storage accessors and admin
flows to extend TTL: call extend_instance_ttl() from initialize() after setting
Admin/Vault/IsPaused, and call it in transfer_admin(), pause(), unpause() and
any admin-sensitive paths (i.e., immediately after successful
get_admin()/set_admin(), set_vault_address(), set_paused() operations) so Admin,
VaultAddress and IsPaused instance keys do not expire; ensure references to
has_admin, set_admin, get_admin, set_vault_address, get_vault_address,
is_paused, set_paused are updated to invoke the TTL extension where those state
changes or admin reads occur.

}
140 changes: 140 additions & 0 deletions contracts/calendar-contract/src/test.rs
Original file line number Diff line number Diff line change
@@ -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);
}
Empty file.
Loading