diff --git a/src/lib.rs b/src/lib.rs index 2bad7ad..d5e52d6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,11 +9,15 @@ pub mod events; pub mod fees; pub mod mint; pub mod multisig; +pub mod nonce; +pub mod rate_lock; pub mod schema; pub mod token; pub mod utils; pub use crate::email_to_wallet::EmailToWalletContract; +pub use crate::nonce::ContractError; +pub use crate::nonce::NonceTracker; pub use conversion::ConversionContract; pub use conversion::Currency; pub use escrow::EscrowContract; diff --git a/src/nonce.rs b/src/nonce.rs new file mode 100644 index 0000000..f6b52aa --- /dev/null +++ b/src/nonce.rs @@ -0,0 +1,36 @@ +#![no_std] + +use soroban_sdk::{contract, contracterror, contractimpl, symbol_short, Address, Env, Symbol}; + +#[contracterror] +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum ContractError { + InvalidNonce = 1, +} + +#[contract] +pub struct NonceTracker; + +#[contractimpl] +impl NonceTracker { + pub fn get_nonce(env: Env, user: Address) -> u64 { + let key = (user.clone(), symbol_short!("NONCE")); + env.storage().persistent().get(&key).unwrap_or(0) + } + + pub fn check_and_update_nonce( + env: Env, + user: Address, + incoming: u64, + ) -> Result { + let key = (user.clone(), symbol_short!("NONCE")); + let stored: u64 = env.storage().persistent().get(&key).unwrap_or(0); + + if incoming <= stored { + return Err(ContractError::InvalidNonce); + } + + env.storage().persistent().set(&key, &incoming); + Ok(incoming) + } +} diff --git a/src/rate_lock.rs b/src/rate_lock.rs new file mode 100644 index 0000000..a7c2fca --- /dev/null +++ b/src/rate_lock.rs @@ -0,0 +1,35 @@ +#![no_std] + +use soroban_sdk::{contract, contracterror, contractimpl, symbol_short, Address, Env, Symbol}; + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum RateLockError { + NoRateLocked = 1, + RateExpired = 2, +} + +#[contract] +pub struct RateLockContract; + +#[contractimpl] +impl RateLockContract { + pub fn lock_rate(env: Env, user: Address, rate: i128, duration_seconds: u64) { + let expiry = env.ledger().timestamp() + duration_seconds; + let key = (user.clone(), symbol_short!("RATELOCK")); + env.storage().persistent().set(&key, &(rate, expiry)); + } + + pub fn validate_conversion(env: Env, user: Address) -> Result { + let key = (user.clone(), symbol_short!("RATELOCK")); + let stored: Option<(i128, u64)> = env.storage().persistent().get(&key); + + let (rate, expiry) = stored.ok_or(RateLockError::NoRateLocked)?; + + if env.ledger().timestamp() > expiry { + return Err(RateLockError::RateExpired); + } + + Ok(rate) + } +} diff --git a/test_snapshots/test_lock_and_validate_rate.1.json b/test_snapshots/test_lock_and_validate_rate.1.json new file mode 100644 index 0000000..489e0dd --- /dev/null +++ b/test_snapshots/test_lock_and_validate_rate.1.json @@ -0,0 +1,133 @@ +{ + "generators": { + "address": 2, + "nonce": 0 + }, + "auth": [ + [], + [], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 61, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "symbol": "RATELOCK" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "symbol": "RATELOCK" + } + ] + }, + "durability": "persistent", + "val": { + "vec": [ + { + "i128": { + "hi": 0, + "lo": 100 + } + }, + { + "u64": 60 + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/test_snapshots/test_nonce_tracker.1.json b/test_snapshots/test_nonce_tracker.1.json new file mode 100644 index 0000000..84e2e0b --- /dev/null +++ b/test_snapshots/test_nonce_tracker.1.json @@ -0,0 +1,122 @@ +{ + "generators": { + "address": 2, + "nonce": 0 + }, + "auth": [ + [], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "symbol": "NONCE" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "symbol": "NONCE" + } + ] + }, + "durability": "persistent", + "val": { + "u64": 2 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/tests/nonce_test.rs b/tests/nonce_test.rs new file mode 100644 index 0000000..331a3cb --- /dev/null +++ b/tests/nonce_test.rs @@ -0,0 +1,26 @@ +#![cfg(test)] + +use soroban_sdk::testutils::Address as _; +use soroban_sdk::{Address, Env}; +use stellar_multisig_contract::nonce::ContractError; +use stellar_multisig_contract::nonce::NonceTracker; + +#[test] +fn test_nonce_tracker() { + let env = Env::default(); + let contract_id = env.register(NonceTracker, ()); + let user = Address::generate(&env); + + env.as_contract(&contract_id, || { + assert_eq!(NonceTracker::get_nonce(env.clone(), user.clone()), 0); + NonceTracker::check_and_update_nonce(env.clone(), user.clone(), 1).unwrap(); + assert_eq!(NonceTracker::get_nonce(env.clone(), user.clone()), 1); + }); + + env.as_contract(&contract_id, || { + let err = NonceTracker::check_and_update_nonce(env.clone(), user.clone(), 1).unwrap_err(); + assert_eq!(err, ContractError::InvalidNonce); + NonceTracker::check_and_update_nonce(env.clone(), user.clone(), 2).unwrap(); + assert_eq!(NonceTracker::get_nonce(env.clone(), user.clone()), 2); + }) +} diff --git a/tests/rate_lock_test.rs b/tests/rate_lock_test.rs new file mode 100644 index 0000000..eee379e --- /dev/null +++ b/tests/rate_lock_test.rs @@ -0,0 +1,33 @@ +#![cfg(test)] + +use soroban_sdk::testutils::Address as _; +use soroban_sdk::testutils::Ledger; +use soroban_sdk::{Address, Env}; +use stellar_multisig_contract::rate_lock::RateLockContractClient as RateLockClient; +use stellar_multisig_contract::rate_lock::{RateLockContract, RateLockError}; + +#[test] +fn test_lock_and_validate_rate() { + let env = Env::default(); + let user = Address::generate(&env); + let contract_id = env.register(RateLockContract, ()); + + // Lock rate + env.as_contract(&contract_id, || { + RateLockContract::lock_rate(env.clone(), user.clone(), 100, 60); + }); + + // Validate inside contract context + let rate = env.as_contract(&contract_id, || { + RateLockContract::validate_conversion(env.clone(), user.clone()).unwrap() + }); + assert_eq!(rate, 100); + + // Advance time + env.ledger().set_timestamp(env.ledger().timestamp() + 61); + + let err = env.as_contract(&contract_id, || { + RateLockContract::validate_conversion(env.clone(), user.clone()).unwrap_err() + }); + assert_eq!(err, RateLockError::RateExpired); +}