Skip to content

Commit 5e0e5ec

Browse files
authored
Merge pull request #41 from shamoo53/Security-Prevent-Admin-Rug-Pull
Security-Prevent-Admin-Rug-Pull
2 parents 60e1cd8 + d8ad684 commit 5e0e5ec

2 files changed

Lines changed: 165 additions & 0 deletions

File tree

contracts/vesting_contracts/src/lib.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ pub struct Vault {
2828
pub start_time: u64,
2929
pub end_time: u64,
3030
pub is_initialized: bool, // Lazy initialization flag
31+
pub is_irrevocable: bool, // Security flag to prevent admin withdrawal
3132
}
3233

3334
#[contracttype]
@@ -135,6 +136,7 @@ impl VestingContract {
135136
start_time,
136137
end_time,
137138
is_initialized: true, // Mark as fully initialized
139+
is_irrevocable: false, // Default to revocable
138140
};
139141

140142
// Store vault data immediately (expensive gas usage)
@@ -187,6 +189,7 @@ impl VestingContract {
187189
start_time,
188190
end_time,
189191
is_initialized: false, // Mark as lazy initialized
192+
is_irrevocable: false, // Default to revocable
190193
};
191194

192195
// Store only essential data initially (cheaper gas)
@@ -226,6 +229,7 @@ impl VestingContract {
226229
start_time: 0,
227230
end_time: 0,
228231
is_initialized: false,
232+
is_irrevocable: false,
229233
}
230234
});
231235

@@ -341,6 +345,7 @@ impl VestingContract {
341345
start_time: batch_data.start_times.get(i).unwrap(),
342346
end_time: batch_data.end_times.get(i).unwrap(),
343347
is_initialized: false, // Lazy initialization
348+
is_irrevocable: false, // Default to revocable
344349
};
345350

346351
// Store vault data (minimal writes)
@@ -392,6 +397,7 @@ impl VestingContract {
392397
start_time: batch_data.start_times.get(i).unwrap(),
393398
end_time: batch_data.end_times.get(i).unwrap(),
394399
is_initialized: true, // Full initialization
400+
is_irrevocable: false, // Default to revocable
395401
};
396402

397403
// Store vault data (expensive writes)
@@ -438,6 +444,7 @@ impl VestingContract {
438444
start_time: 0,
439445
end_time: 0,
440446
is_initialized: false,
447+
is_irrevocable: false,
441448
}
442449
});
443450

@@ -469,6 +476,7 @@ impl VestingContract {
469476
start_time: 0,
470477
end_time: 0,
471478
is_initialized: false,
479+
is_irrevocable: false,
472480
}
473481
});
474482

@@ -490,6 +498,9 @@ impl VestingContract {
490498
panic!("Vault not found");
491499
});
492500

501+
// Security check: Cannot revoke from irrevocable vaults
502+
require!(!vault.is_irrevocable, "Vault is irrevocable");
503+
493504
// Calculate amount to return (unreleased tokens)
494505
let unreleased_amount = vault.total_amount - vault.released_amount;
495506
require!(unreleased_amount > 0, "No tokens available to revoke");
@@ -525,6 +536,9 @@ impl VestingContract {
525536
panic!("Vault not found");
526537
});
527538

539+
// Security check: Cannot revoke from irrevocable vaults
540+
require!(!vault.is_irrevocable, "Vault is irrevocable");
541+
528542
// Calculate unvested balance (tokens not yet released)
529543
let unvested_balance = vault.total_amount - vault.released_amount;
530544
require!(amount > 0, "Amount to revoke must be positive");
@@ -551,6 +565,42 @@ impl VestingContract {
551565
amount
552566
}
553567

568+
// Mark a vault as irrevocable to prevent admin withdrawal
569+
pub fn mark_irrevocable(env: Env, vault_id: u64) {
570+
Self::require_admin(&env);
571+
572+
let mut vault: Vault = env.storage().instance()
573+
.get(&VAULT_DATA, &vault_id)
574+
.unwrap_or_else(|| {
575+
panic!("Vault not found");
576+
});
577+
578+
// Cannot mark already irrevocable vaults
579+
require!(!vault.is_irrevocable, "Vault is already irrevocable");
580+
581+
// Mark vault as irrevocable
582+
vault.is_irrevocable = true;
583+
env.storage().instance().set(&VAULT_DATA, &vault_id, &vault);
584+
585+
// Emit IrrevocableMarked event
586+
let timestamp = env.ledger().timestamp();
587+
env.events().publish(
588+
(Symbol::new(&env, "IrrevocableMarked"), vault_id),
589+
(timestamp),
590+
);
591+
}
592+
593+
// Check if a vault is irrevocable
594+
pub fn is_vault_irrevocable(env: Env, vault_id: u64) -> bool {
595+
let vault: Vault = env.storage().instance()
596+
.get(&VAULT_DATA, &vault_id)
597+
.unwrap_or_else(|| {
598+
panic!("Vault not found");
599+
});
600+
601+
vault.is_irrevocable
602+
}
603+
554604
// Get contract state for invariant checking
555605
pub fn get_contract_state(env: Env) -> (i128, i128, i128) {
556606
let initial_supply: i128 = env.storage().instance().get(&INITIAL_SUPPLY).unwrap_or(0);

contracts/vesting_contracts/src/test.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,121 @@ fn test_usdc_integration_mock_token() {
549549
assert_eq!(updated_vault.released_amount, claimable);
550550
}
551551

552+
#[test]
553+
fn test_irrevocable_vault_security() {
554+
let env = Env::default();
555+
let contract_id = env.register(VestingContract, ());
556+
let client = VestingContractClient::new(&env, &contract_id);
557+
558+
// Create addresses for testing
559+
let admin = Address::generate(&env);
560+
let vault_owner = Address::generate(&env);
561+
let unauthorized_user = Address::generate(&env);
562+
563+
// Initialize contract with admin
564+
let initial_supply = 1000000i128;
565+
client.initialize(&admin, &initial_supply);
566+
567+
// Create a vault
568+
env.as_contract(&contract_id, || {
569+
env.current_contract_address().set(&admin);
570+
});
571+
572+
let vault_amount = 1000i128;
573+
let vault_id = client.create_vault_full(&vault_owner, &vault_amount, &100u64, &200u64);
574+
575+
// Verify vault is initially revocable
576+
assert_eq!(client.is_vault_irrevocable(&vault_id), false);
577+
578+
// Test: Unauthorized user cannot mark vault as irrevocable
579+
env.as_contract(&contract_id, || {
580+
env.current_contract_address().set(&unauthorized_user);
581+
});
582+
583+
let result = std::panic::catch_unwind(|| {
584+
client.mark_irrevocable(&vault_id);
585+
});
586+
assert!(result.is_err());
587+
588+
// Test: Admin can mark vault as irrevocable
589+
env.as_contract(&contract_id, || {
590+
env.current_contract_address().set(&admin);
591+
});
592+
593+
client.mark_irrevocable(&vault_id);
594+
595+
// Verify vault is now irrevocable
596+
assert_eq!(client.is_vault_irrevocable(&vault_id), true);
597+
598+
// Test: Cannot mark already irrevocable vault
599+
let result = std::panic::catch_unwind(|| {
600+
client.mark_irrevocable(&vault_id);
601+
});
602+
assert!(result.is_err());
603+
604+
// Test: Admin cannot revoke tokens from irrevocable vault (full revocation)
605+
let result = std::panic::catch_unwind(|| {
606+
client.revoke_tokens(&vault_id);
607+
});
608+
assert!(result.is_err());
609+
610+
// Test: Admin cannot revoke partial tokens from irrevocable vault
611+
let result = std::panic::catch_unwind(|| {
612+
client.revoke_partial(&vault_id, &100i128);
613+
});
614+
assert!(result.is_err());
615+
616+
// Verify the vault state remains unchanged
617+
let vault = client.get_vault(&vault_id);
618+
assert_eq!(vault.released_amount, 0);
619+
assert_eq!(vault.total_amount, vault_amount);
620+
assert_eq!(vault.is_irrevocable, true);
621+
}
622+
623+
#[test]
624+
fn test_irrevocable_vault_with_claims() {
625+
let env = Env::default();
626+
let contract_id = env.register(VestingContract, ());
627+
let client = VestingContractClient::new(&env, &contract_id);
628+
629+
// Create addresses for testing
630+
let admin = Address::generate(&env);
631+
let vault_owner = Address::generate(&env);
632+
633+
// Initialize contract with admin
634+
let initial_supply = 1000000i128;
635+
client.initialize(&admin, &initial_supply);
636+
637+
// Create a vault
638+
env.as_contract(&contract_id, || {
639+
env.current_contract_address().set(&admin);
640+
});
641+
642+
let vault_amount = 1000i128;
643+
let vault_id = client.create_vault_full(&vault_owner, &vault_amount, &100u64, &200u64);
644+
645+
// Mark vault as irrevocable
646+
client.mark_irrevocable(&vault_id);
647+
648+
// Beneficiary can still claim tokens from irrevocable vault
649+
env.ledger().set_timestamp(150); // Halfway through vesting period
650+
651+
let claimable_amount = vault_amount / 2; // 500 tokens should be claimable
652+
let claimed = client.claim_tokens(&vault_id, &claimable_amount);
653+
assert_eq!(claimed, claimable_amount);
654+
655+
// Verify vault state after claim
656+
let vault = client.get_vault(&vault_id);
657+
assert_eq!(vault.released_amount, claimable_amount);
658+
assert_eq!(vault.is_irrevocable, true);
659+
660+
// Admin still cannot revoke even after claims
661+
let result = std::panic::catch_unwind(|| {
662+
client.revoke_partial(&vault_id, &100i128);
663+
});
664+
assert!(result.is_err());
665+
}
666+
552667
// -------------------------------------------------------------------------
553668
// Additional beneficiary-transfer tests added for coverage
554669
// -------------------------------------------------------------------------

0 commit comments

Comments
 (0)