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
7 changes: 7 additions & 0 deletions .github/workflows/contracts-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ jobs:
run: cargo fmt --all -- --check
working-directory: ./contract/contract

- name: Install wasm32v1-none target
run: rustup target add wasm32v1-none

- name: Build contract WASM (required for upgrade integration test)
run: cargo build --target wasm32v1-none --release
working-directory: ./contract/contract

- name: Run contract tests
run: cargo test
working-directory: ./contract/contract
Expand Down
34 changes: 31 additions & 3 deletions contract/contract/src/base/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,32 @@ pub enum EventStatus {
#[contracttype]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EventDetails {
pub id: u64,
pub id: BytesN<32>,
pub title: String,
pub creator: Address,
pub ticket_price: i128,
pub max_attendees: u32,
pub deadline: u64,
pub token: Address,
}

#[contracttype]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EventMetrics {
pub tickets_sold: u32,
}

impl Default for EventMetrics {
fn default() -> Self {
Self::new()
}
}

impl EventMetrics {
/// Creates zero-initialized metrics for a new event.
pub fn new() -> Self {
Self { tickets_sold: 0 }
}
}

/// Represents the type of a ticket.
Expand Down Expand Up @@ -290,6 +310,10 @@ pub enum StorageKey {
EventPlatformFees(u64),
// Track if someone bought a ticket
UserTicket(u64, Address),
// Event details keyed by event id
Event(BytesN<32>),
// Per-event metrics (tickets sold, etc.)
EventMetrics(BytesN<32>),
}

#[cfg(test)]
Expand Down Expand Up @@ -462,18 +486,22 @@ mod tests {
use soroban_sdk::testutils::Address as _;
let env = Env::default();
let creator = soroban_sdk::Address::generate(&env);
let token = soroban_sdk::Address::generate(&env);
let id = soroban_sdk::BytesN::from_array(&env, &[1u8; 32]);
let event = EventDetails {
id: 1,
id: id.clone(),
title: String::from_str(&env, "Nevo Launch"),
creator: creator.clone(),
ticket_price: 500,
max_attendees: 100,
deadline: 1_700_000_000,
token: token.clone(),
};
assert_eq!(event.id, 1);
assert_eq!(event.id, id);
assert_eq!(event.ticket_price, 500);
assert_eq!(event.max_attendees, 100);
assert_eq!(event.deadline, 1_700_000_000);
assert_eq!(event.creator, creator);
assert_eq!(event.token, token);
}
}
38 changes: 28 additions & 10 deletions contract/contract/src/crowdfunding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ use crate::base::{
},
types::{
CampaignDetails, CampaignLifecycleStatus, CampaignMetrics, Contribution,
EmergencyWithdrawal, MultiSigConfig, PoolConfig, PoolContribution, PoolMetadata,
PoolMetrics, PoolState, StorageKey, MAX_DESCRIPTION_LENGTH, MAX_HASH_LENGTH,
MAX_STRING_LENGTH, MAX_URL_LENGTH,
EmergencyWithdrawal, EventDetails, EventMetrics, MultiSigConfig, PoolConfig,
PoolContribution, PoolMetadata, PoolMetrics, PoolState, StorageKey, MAX_DESCRIPTION_LENGTH,
MAX_HASH_LENGTH, MAX_STRING_LENGTH, MAX_URL_LENGTH,
},
};
use crate::interfaces::crowdfunding::CrowdfundingTrait;
Expand Down Expand Up @@ -1922,16 +1922,34 @@ impl SecondCrowdfundingTrait for CrowdfundingContract {

fn create_event(
env: Env,
_id: BytesN<32>,
id: BytesN<32>,
title: String,
_creator: Address,
_ticket_price: i128,
_max_attendees: u32,
_deadline: u64,
_token: Address,
creator: Address,
ticket_price: i128,
max_attendees: u32,
deadline: u64,
token: Address,
) -> Result<(), SecondCrowdfundingError> {
Self::validate_string_length(&title)?;
let _ = env;

let details = EventDetails {
id: id.clone(),
title,
creator,
ticket_price,
max_attendees,
deadline,
token,
};

env.storage()
.instance()
.set(&StorageKey::Event(id.clone()), &details);

env.storage()
.instance()
.set(&StorageKey::EventMetrics(id), &EventMetrics::new());

Ok(())
}
}
89 changes: 79 additions & 10 deletions contract/contract/test/create_event_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
use soroban_sdk::{testutils::Address as _, Address, BytesN, Env, String};

use crate::{
base::errors::SecondCrowdfundingError, crowdfunding::CrowdfundingContract,
base::{
errors::SecondCrowdfundingError,
types::{EventDetails, EventMetrics, StorageKey},
},
crowdfunding::CrowdfundingContract,
interfaces::second_crowdfunding::SecondCrowdfundingTrait,
};

Expand All @@ -14,6 +18,7 @@ fn string_of_len(env: &Env, len: usize) -> String {
#[test]
fn test_create_event_success_path_returns_ok_for_valid_titles() {
let env = Env::default();
let contract_id = env.register(CrowdfundingContract, ());

let creator = Address::generate(&env);
let token = Address::generate(&env);
Expand All @@ -25,21 +30,85 @@ fn test_create_event_success_path_returns_ok_for_valid_titles() {
let id = BytesN::from_array(&env, &[(index + 1) as u8; 32]);
let title = string_of_len(&env, title_len);

let result = env.as_contract(&contract_id, || {
<CrowdfundingContract as SecondCrowdfundingTrait>::create_event(
env.clone(),
id,
title,
creator.clone(),
100,
500,
base_deadline + index as u64,
token.clone(),
)
});

assert_eq!(
result,
Ok::<(), SecondCrowdfundingError>(()),
"create_event should succeed for title length {title_len}"
);
}
}

#[test]
fn test_create_event_stores_event_details_and_initializes_metrics() {
let env = Env::default();
let contract_id = env.register(CrowdfundingContract, ());

let creator = Address::generate(&env);
let token = Address::generate(&env);
let id = BytesN::from_array(&env, &[42u8; 32]);
let title = String::from_str(&env, "Soroban Hackathon");
let ticket_price: i128 = 250;
let max_attendees: u32 = 100;
let deadline: u64 = env.ledger().timestamp() + 7 * 86_400;

env.as_contract(&contract_id, || {
let result = <CrowdfundingContract as SecondCrowdfundingTrait>::create_event(
env.clone(),
id,
title,
id.clone(),
title.clone(),
creator.clone(),
100,
500,
base_deadline + index as u64,
ticket_price,
max_attendees,
deadline,
token.clone(),
);

assert_eq!(result, Ok(()), "create_event should return Ok");

// Verify EventDetails stored correctly
let stored_details: EventDetails = env
.storage()
.instance()
.get(&StorageKey::Event(id.clone()))
.expect("EventDetails should be stored");

assert_eq!(stored_details.id, id, "id mismatch");
assert_eq!(stored_details.title, title, "title mismatch");
assert_eq!(stored_details.creator, creator, "creator mismatch");
assert_eq!(
result,
Ok::<(), SecondCrowdfundingError>(()),
"create_event should succeed for title length {title_len}"
stored_details.ticket_price, ticket_price,
"ticket_price mismatch"
);
}
assert_eq!(
stored_details.max_attendees, max_attendees,
"max_attendees mismatch"
);
assert_eq!(stored_details.deadline, deadline, "deadline mismatch");
assert_eq!(stored_details.token, token, "token mismatch");

// Verify EventMetrics initialized with 0 tickets sold
let stored_metrics: EventMetrics = env
.storage()
.instance()
.get(&StorageKey::EventMetrics(id.clone()))
.expect("EventMetrics should be stored");

assert_eq!(
stored_metrics.tickets_sold, 0,
"tickets_sold should be initialized to 0"
);
});
}
48 changes: 39 additions & 9 deletions contract/contract/test/upgrade_contract_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ use crate::{
crowdfunding::{CrowdfundingContract, CrowdfundingContractClient},
};

// Import the compiled WASM of this same contract to use as the "new" version
// in the upgrade integration test.
mod upgraded_contract {
soroban_sdk::contractimport!(file = "../target/wasm32v1-none/release/hello_world.wasm");
}

fn setup(env: &Env) -> (CrowdfundingContractClient<'_>, Address) {
env.mock_all_auths();
let contract_id = env.register(CrowdfundingContract, ());
Expand All @@ -22,21 +28,45 @@ fn setup(env: &Env) -> (CrowdfundingContractClient<'_>, Address) {
(client, admin)
}

/// Integration test: proves the full upgrade path works end-to-end.
/// Uploads a real WASM binary, calls upgrade_contract, and verifies the
/// contract remains functional (storage intact) after the upgrade.
#[test]
fn test_upgrade_contract_auth_success() {
fn test_upgrade_contract_succeeds_with_valid_wasm() {
let env = Env::default();
let (client, _admin) = setup(&env);
env.mock_all_auths();

let new_wasm_hash = BytesN::from_array(&env, &[0u8; 32]);
let contract_id = env.register(CrowdfundingContract, ());
let client = CrowdfundingContractClient::new(&env, &contract_id);

let admin = Address::generate(&env);
let token = Address::generate(&env);
client.initialize(&admin, &token, &0);

// Upload the contract's own compiled WASM — gives a valid on-ledger hash.
let new_wasm_hash: BytesN<32> = env.deployer().upload_contract_wasm(upgraded_contract::WASM);

// Upgrade must succeed: admin is authorized and WASM hash is valid.
client.upgrade_contract(&new_wasm_hash);

// Admin call with mock_all_auths should pass the auth check.
// It will then fail on the deployer call because of the invalid WASM hash,
// which is expected and confirms we passed the auth check.
// Contract is still callable after upgrade — storage is preserved.
let result = client.try_get_pool_remaining_time(&999u64);
assert_eq!(result, Err(Ok(CrowdfundingError::PoolNotFound)));
}

#[test]
fn test_upgrade_contract_not_initialized_fails() {
let env = Env::default();
env.mock_all_auths();

// Register without calling initialize — no Admin in storage.
let contract_id = env.register(CrowdfundingContract, ());
let client = CrowdfundingContractClient::new(&env, &contract_id);

let new_wasm_hash = BytesN::from_array(&env, &[0u8; 32]);
let result = client.try_upgrade_contract(&new_wasm_hash);

assert!(result.is_err());
// We can't easily check the error type if it's a HostError,
// but we know it reached the contract because of previous diagnostic tests.
assert_eq!(result, Err(Ok(CrowdfundingError::NotInitialized)));
}

#[test]
Expand Down
Loading