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
86 changes: 86 additions & 0 deletions tests/integration/admin_role_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,89 @@ fn test_multi_admin_management_and_multisig_ops() {
assert_eq!(admins.len(), 2);
assert!(!token_client.has_role(&minter_admin, &AdminRole::Minter));
}

#[test]
fn test_multi_admin_role_enforcement_revocation_and_events() {
let setup = setup_chainlearn_env();
let env = &setup.env;
let primary_admin = &setup.admin;
env.mock_all_auths();

let token_client = LearnTokenClient::new(env, &setup.token_contract_id);

let admin_minter = Address::generate(env);
let admin_pauser = Address::generate(env);
let recipient = Address::generate(env);

// 1. Add multiple admins with distinct roles
token_client.add_admin(
primary_admin,
&learn_token::AdminInfo {
address: admin_minter.clone(),
role: AdminRole::Minter,
},
);

token_client.add_admin(
primary_admin,
&learn_token::AdminInfo {
address: admin_pauser.clone(),
role: AdminRole::Pauser,
},
);

let admins = token_client.get_admins();
assert_eq!(admins.len(), 3);

// Verify role granted events were emitted
let events = env.events().all();
assert!(events.len() >= 2);

// 2. Enforce roles
assert!(token_client.has_role(&admin_minter, &AdminRole::Minter));
assert!(!token_client.has_role(&admin_minter, &AdminRole::Pauser));
assert!(token_client.has_role(&admin_pauser, &AdminRole::Pauser));
assert!(!token_client.has_role(&admin_pauser, &AdminRole::Minter));

// admin_minter can mint
token_client.mint(&admin_minter, &recipient, &500);
assert_eq!(token_client.balance(&recipient), 500);

// admin_minter cannot pause
assert!(token_client.try_pause(&admin_minter).is_err());

// admin_pauser can pause
token_client.pause(&admin_pauser);
assert!(token_client.is_paused());
token_client.unpause(&admin_pauser);

// admin_pauser cannot mint
assert!(token_client.try_mint(&admin_pauser, &recipient, &500).is_err());

// 3. Revocation
token_client.remove_admin(
primary_admin,
&learn_token::AdminInfo {
address: admin_minter.clone(),
role: AdminRole::Minter,
},
);

token_client.remove_admin(
primary_admin,
&learn_token::AdminInfo {
address: admin_pauser.clone(),
role: AdminRole::Pauser,
},
);

// Verify roles are revoked and actions fail
assert!(!token_client.has_role(&admin_minter, &AdminRole::Minter));
assert!(!token_client.has_role(&admin_pauser, &AdminRole::Pauser));
assert!(token_client.try_mint(&admin_minter, &recipient, &100).is_err());
assert!(token_client.try_pause(&admin_pauser).is_err());

let final_admins = token_client.get_admins();
assert_eq!(final_admins.len(), 1);
}

53 changes: 53 additions & 0 deletions tests/integration/security_reentrancy_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ impl MaliciousContract {
// Attempt an unauthorized call during contract execution
client.transfer(&Address::generate(&env), &Address::generate(&env), &1);
}

pub fn attack_mint(env: Env, token_id: Address, recipient: Address) {
let client = LearnTokenClient::new(&env, &token_id);
// Attempt reentrant mint call during state change execution
client.mint(&Address::generate(&env), &recipient, &5000);
}
}

#[test]
Expand Down Expand Up @@ -46,3 +52,50 @@ fn test_reentrancy_during_transfer() {
// The environment naturally protects against state corruption, often panicking if a re-entrant lock is triggered.
malicious_client.attack(&token_id);
}

#[test]
fn test_reentrancy_prevented_state_consistent_and_no_funds_lost() {
let env = Env::default();
let admin = Address::generate(&env);
let pt_contract_id = env.register_contract(None, ProgressTracker);
let token_id = env.register_contract(None, LearnToken);
let client = LearnTokenClient::new(&env, &token_id);

client.initialize(
&admin,
&SorobanString::from_str(&env, "ChainLearn"),
&SorobanString::from_str(&env, "CLRN"),
&7,
&pt_contract_id,
&1_000_000,
);

let victim = Address::generate(&env);
let malicious_id = env.register_contract(None, MaliciousContract);
let malicious_client = MaliciousContractClient::new(&env, &malicious_id);

env.mock_all_auths();

client.mint(&admin, &victim, &1_000);
client.mint(&admin, &malicious_id, &500);

let victim_balance_before = client.balance(&victim);
let malicious_balance_before = client.balance(&malicious_id);
let supply_before = client.total_supply();

assert_eq!(victim_balance_before, 1_000);
assert_eq!(malicious_balance_before, 500);
assert_eq!(supply_before, 1_500);

// Reentrant attack must fail / revert
let attack_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
malicious_client.attack_mint(&token_id, &malicious_id);
}));
assert!(attack_result.is_err(), "reentrant call must fail");

// State remains consistent after failed reentrancy attack (no funds lost)
assert_eq!(client.balance(&victim), victim_balance_before);
assert_eq!(client.balance(&malicious_id), malicious_balance_before);
assert_eq!(client.total_supply(), supply_before);
}

55 changes: 55 additions & 0 deletions tests/integration/upgrade_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,58 @@ fn test_contract_upgrade() {

assert_eq!(client.balance(&user), 100);
}

#[test]
fn test_contract_upgrade_preserves_state_and_updates_version() {
let env = Env::default();
let admin = Address::generate(&env);
let pt_contract_id = env.register_contract(None, ProgressTracker);
let token_id = env.register_contract(None, LearnToken);
let client = LearnTokenClient::new(&env, &token_id);

client.initialize(
&admin,
&SorobanString::from_str(&env, "ChainLearn"),
&SorobanString::from_str(&env, "CLRN"),
&7,
&pt_contract_id,
&1_000_000,
);

env.mock_all_auths();

let alice = Address::generate(&env);
let bob = Address::generate(&env);

client.mint(&admin, &alice, &500);
client.mint(&admin, &bob, &300);

assert_eq!(client.balance(&alice), 500);
assert_eq!(client.balance(&bob), 300);
assert_eq!(client.total_supply(), 800);
assert_eq!(client.upgrade_version(), 0);
assert_eq!(client.wasm_hash(), None);

let new_wasm_hash = BytesN::from_array(&env, &[2u8; 32]);
client.upgrade(&new_wasm_hash);

assert_eq!(client.upgrade_version(), 1);
assert_eq!(client.wasm_hash(), Some(new_wasm_hash));

// Verify state preserved after upgrade
assert_eq!(client.balance(&alice), 500);
assert_eq!(client.balance(&bob), 300);
assert_eq!(client.total_supply(), 800);
assert_eq!(client.admin(), admin);
assert_eq!(client.max_supply(), 1_000_000);

// Verify contract functions operate correctly post-upgrade
client.transfer(&alice, &bob, &200);
assert_eq!(client.balance(&alice), 300);
assert_eq!(client.balance(&bob), 500);

client.mint(&admin, &alice, &100);
assert_eq!(client.balance(&alice), 400);
assert_eq!(client.total_supply(), 900);
}

105 changes: 105 additions & 0 deletions tests/unit/token_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -877,4 +877,109 @@ mod token_unit_tests {
let executed = client.get_proposal(&proposal_id).unwrap();
assert!(executed.executed);
}

#[test]
fn test_vesting_schedule_cliff_linear_vesting_and_claiming() {
let env = Env::default();
let (admin, contract_id, _) = setup_token(&env);
let client = LearnTokenClient::new(&env, &contract_id);
let beneficiary = Address::generate(&env);

env.mock_all_auths();

let total_amount: i128 = 10_000;
let cliff_timestamp: u64 = 100;
let duration_seconds: u64 = 1_000;

env.ledger().with_mut(|l| {
l.timestamp = 0;
});

client.create_vesting(&beneficiary, &total_amount, &cliff_timestamp, &duration_seconds);

let schedule = client.get_vesting_schedule(&beneficiary).expect("schedule should exist");
assert_eq!(schedule.total_amount, 10_000);
assert_eq!(schedule.cliff_timestamp, 100);
assert_eq!(schedule.duration_seconds, 1_000);
assert!(!schedule.exhausted);
assert_eq!(client.get_vesting_claimed(&beneficiary), 0);

// Linear vesting halfway through cliff + 500s (50% vested)
env.ledger().with_mut(|l| {
l.timestamp = cliff_timestamp + 500;
});

client.claim_vested(&beneficiary);
assert_eq!(client.balance(&beneficiary), 5_000);
assert_eq!(client.get_vesting_claimed(&beneficiary), 5_000);
assert_eq!(client.total_supply(), 5_000);

let mid_schedule = client.get_vesting_schedule(&beneficiary).unwrap();
assert!(!mid_schedule.exhausted);

// Complete linear vesting cliff + 1000s (100% vested)
env.ledger().with_mut(|l| {
l.timestamp = cliff_timestamp + 1_000;
});

client.claim_vested(&beneficiary);
assert_eq!(client.balance(&beneficiary), 10_000);
assert_eq!(client.get_vesting_claimed(&beneficiary), 10_000);
assert_eq!(client.total_supply(), 10_000);

let final_schedule = client.get_vesting_schedule(&beneficiary).unwrap();
assert!(final_schedule.exhausted);
}

#[test]
#[should_panic(expected = "cliff not reached")]
fn test_vesting_cliff_enforced() {
let env = Env::default();
let (admin, contract_id, _) = setup_token(&env);
let client = LearnTokenClient::new(&env, &contract_id);
let beneficiary = Address::generate(&env);

env.mock_all_auths();

env.ledger().with_mut(|l| {
l.timestamp = 0;
});

client.create_vesting(&beneficiary, &10_000, &100, &1_000);

// Advance to timestamp 50 (cliff is 100)
env.ledger().with_mut(|l| {
l.timestamp = 50;
});

client.claim_vested(&beneficiary);
}

#[test]
#[should_panic(expected = "vesting schedule fully claimed")]
fn test_vesting_claiming_after_exhausted_panics() {
let env = Env::default();
let (admin, contract_id, _) = setup_token(&env);
let client = LearnTokenClient::new(&env, &contract_id);
let beneficiary = Address::generate(&env);

env.mock_all_auths();

env.ledger().with_mut(|l| {
l.timestamp = 0;
});

client.create_vesting(&beneficiary, &10_000, &100, &1_000);

env.ledger().with_mut(|l| {
l.timestamp = 1100;
});

client.claim_vested(&beneficiary);
assert_eq!(client.balance(&beneficiary), 10_000);

// Attempting to claim again after schedule is exhausted must panic
client.claim_vested(&beneficiary);
}
}