From 8faf864fddfb1c34715f5d438c2e356fc9ea15a9 Mon Sep 17 00:00:00 2001 From: Joeloo1 Date: Fri, 28 Aug 2026 23:02:50 +0100 Subject: [PATCH] feat: add aggregate operation count getters for farming-pool, factory, and vesting-wallet (#260, #261, #262, #263) --- soroban/contracts/factory/src/lib.rs | 28 ++++++ soroban/contracts/factory/src/test.rs | 94 ++++++++++++++++--- soroban/contracts/factory/src/types.rs | 4 + soroban/contracts/farming-pool/src/lib.rs | 69 +++++++++++++- soroban/contracts/farming-pool/src/test.rs | 83 ++++++++++++++-- soroban/contracts/farming-pool/src/types.rs | 5 + soroban/contracts/vesting-wallet/src/lib.rs | 27 ++++++ soroban/contracts/vesting-wallet/src/test.rs | 34 ++++++- soroban/contracts/vesting-wallet/src/types.rs | 2 + 9 files changed, 324 insertions(+), 22 deletions(-) diff --git a/soroban/contracts/factory/src/lib.rs b/soroban/contracts/factory/src/lib.rs index 978a9d7..3627eef 100644 --- a/soroban/contracts/factory/src/lib.rs +++ b/soroban/contracts/factory/src/lib.rs @@ -135,6 +135,20 @@ fn insert_sorted( records.insert(insert_at, record); } +fn read_admin_transfer_count(env: &Env) -> u32 { + env.storage() + .instance() + .get(&DataKey::AdminTransferCount) + .unwrap_or(0) +} + +fn increment_admin_transfer_count(env: &Env) { + let count = read_admin_transfer_count(env); + env.storage() + .instance() + .set(&DataKey::AdminTransferCount, &(count + 1)); +} + #[contract] pub struct Factory; @@ -491,6 +505,7 @@ impl Factory { current.require_auth(); bump_instance(&env); env.storage().instance().set(&DataKey::Admin, &new_admin); + increment_admin_transfer_count(&env); #[allow(deprecated)] env.events().publish( (symbol_short!("factory"), symbol_short!("adm_xfr")), @@ -499,6 +514,19 @@ impl Factory { Ok(()) } + /// Return the total number of admin transfers performed. + /// + /// Returns `NotInitialized` if the factory has not been initialized. + pub fn admin_transfer_count(env: Env) -> Result { + require_initialized(&env)?; + bump_instance(&env); + Ok(read_admin_transfer_count(&env)) + } + + pub fn get_admin_transfer_count(env: Env) -> Result { + Self::admin_transfer_count(env) + } + /// Upgrade one registered farming pool in place. Admin-only. /// /// This deliberately does not update the factory-level `WasmHash`; it is a diff --git a/soroban/contracts/factory/src/test.rs b/soroban/contracts/factory/src/test.rs index e0d9c02..e47821b 100644 --- a/soroban/contracts/factory/src/test.rs +++ b/soroban/contracts/factory/src/test.rs @@ -30,19 +30,52 @@ mod farming_pool_wasm { soroban_sdk::contractimport!(file = "../../target/wasm32v1-none/release/farming_pool.wasm"); } -/// A distinct, valid contract WASM used to prove that `upgrade_pool` changes -/// the registered pool's executable hash rather than merely re-installing its -/// current farming-pool WASM. -mod replacement_wasm { - soroban_sdk::contractimport!(file = "../../target/wasm32v1-none/release/factory.wasm"); -} +const ADD_I32_WASM: &[u8] = &[ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x10, 0x03, 0x60, 0x02, 0x7e, 0x7e, 0x01, + 0x7e, 0x60, 0x02, 0x7f, 0x7f, 0x01, 0x7e, 0x60, 0x00, 0x00, 0x02, 0x0d, 0x02, 0x01, 0x78, 0x01, + 0x31, 0x00, 0x00, 0x01, 0x76, 0x01, 0x67, 0x00, 0x00, 0x03, 0x04, 0x03, 0x00, 0x01, 0x02, 0x05, + 0x03, 0x01, 0x00, 0x10, 0x06, 0x19, 0x03, 0x7f, 0x01, 0x41, 0x80, 0x80, 0xc0, 0x00, 0x0b, 0x7f, + 0x00, 0x41, 0x80, 0x80, 0xc0, 0x00, 0x0b, 0x7f, 0x00, 0x41, 0x80, 0x80, 0xc0, 0x00, 0x0b, 0x07, + 0x2f, 0x05, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x03, 0x61, 0x64, 0x64, 0x00, + 0x02, 0x01, 0x5f, 0x00, 0x04, 0x0a, 0x5f, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x65, 0x6e, 0x64, + 0x03, 0x01, 0x0b, 0x5f, 0x5f, 0x68, 0x65, 0x61, 0x70, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x03, 0x02, + 0x0a, 0xe3, 0x01, 0x03, 0xc5, 0x01, 0x02, 0x04, 0x7f, 0x01, 0x7e, 0x23, 0x00, 0x41, 0x20, 0x6b, + 0x22, 0x03, 0x24, 0x00, 0x02, 0x40, 0x20, 0x00, 0x42, 0xff, 0x01, 0x83, 0x42, 0x05, 0x52, 0x20, + 0x01, 0x42, 0xff, 0x01, 0x83, 0x42, 0x05, 0x52, 0x72, 0x45, 0x04, 0x40, 0x20, 0x00, 0x42, 0x20, + 0x88, 0xa7, 0x21, 0x04, 0x20, 0x01, 0x42, 0x20, 0x88, 0xa7, 0x21, 0x05, 0x20, 0x03, 0x42, 0x8e, + 0xd2, 0xa9, 0x13, 0x37, 0x03, 0x08, 0x42, 0x02, 0x21, 0x06, 0x41, 0x01, 0x21, 0x02, 0x03, 0x40, + 0x20, 0x02, 0x04, 0x40, 0x20, 0x02, 0x41, 0x01, 0x6b, 0x21, 0x02, 0x42, 0x8e, 0xd2, 0xa9, 0x13, + 0x21, 0x06, 0x0c, 0x01, 0x0b, 0x0b, 0x20, 0x03, 0x20, 0x06, 0x37, 0x03, 0x10, 0x20, 0x03, 0x41, + 0x10, 0x6a, 0x22, 0x02, 0x41, 0x01, 0x10, 0x03, 0x20, 0x03, 0x20, 0x01, 0x42, 0x80, 0x80, 0x80, + 0x80, 0x70, 0x83, 0x42, 0x05, 0x84, 0x37, 0x03, 0x18, 0x20, 0x03, 0x20, 0x00, 0x42, 0x80, 0x80, + 0x80, 0x80, 0x70, 0x83, 0x42, 0x05, 0x84, 0x37, 0x03, 0x10, 0x20, 0x02, 0x41, 0x02, 0x10, 0x03, + 0x10, 0x00, 0x1a, 0x20, 0x05, 0x41, 0x00, 0x48, 0x20, 0x04, 0x20, 0x05, 0x6a, 0x22, 0x02, 0x20, + 0x04, 0x48, 0x47, 0x0d, 0x01, 0x20, 0x03, 0x41, 0x20, 0x6a, 0x24, 0x00, 0x20, 0x02, 0xad, 0x42, + 0x20, 0x86, 0x42, 0x05, 0x84, 0x0f, 0x0b, 0x00, 0x0b, 0x00, 0x0b, 0x16, 0x00, 0x20, 0x00, 0xad, + 0x42, 0x20, 0x86, 0x42, 0x04, 0x84, 0x20, 0x01, 0xad, 0x42, 0x20, 0x86, 0x42, 0x04, 0x84, 0x10, + 0x01, 0x0b, 0x03, 0x00, 0x01, 0x0b, 0x00, 0x4b, 0x0e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, + 0x74, 0x73, 0x70, 0x65, 0x63, 0x76, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x61, 0x64, 0x64, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x05, 0x00, 0x1e, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x65, 0x6e, + 0x76, 0x6d, 0x65, 0x74, 0x61, 0x76, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x73, 0x0e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x6d, 0x65, + 0x74, 0x61, 0x76, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x72, 0x73, 0x76, 0x65, + 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x31, 0x2e, 0x37, 0x34, 0x2e, 0x30, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x72, 0x73, 0x73, 0x64, 0x6b, 0x76, 0x65, 0x72, + 0x00, 0x00, 0x00, 0x33, 0x32, 0x30, 0x2e, 0x30, 0x2e, 0x30, 0x2d, 0x72, 0x63, 0x32, 0x23, 0x37, + 0x63, 0x31, 0x35, 0x34, 0x62, 0x34, 0x66, 0x65, 0x36, 0x61, 0x33, 0x37, 0x64, 0x37, 0x63, 0x61, + 0x37, 0x31, 0x37, 0x37, 0x33, 0x34, 0x32, 0x64, 0x65, 0x64, 0x62, 0x36, 0x39, 0x66, 0x33, 0x31, + 0x30, 0x38, 0x30, 0x39, 0x35, 0x65, 0x66, 0x00, +]; fn upload_farming_pool_wasm(env: &Env) -> BytesN<32> { env.deployer().upload_contract_wasm(farming_pool_wasm::WASM) } fn upload_replacement_wasm(env: &Env) -> BytesN<32> { - env.deployer().upload_contract_wasm(replacement_wasm::WASM) + env.deployer().upload_contract_wasm(ADD_I32_WASM) } /// Builds an initialised factory using the real farming-pool WASM. @@ -1351,15 +1384,17 @@ fn test_unpause_pool_creation_allows_create_pool() { #[test] fn test_pause_pool_creation_requires_admin_auth() { - let (env, factory_addr, client, _admin, user) = setup_without_mocked_auth(); + let t = setup(); + let user = Address::generate(&t.env); - let result = client + let result = t + .client .mock_auths(&[MockAuth { address: &user, invoke: &MockAuthInvoke { - contract: &factory_addr, + contract: &t.factory_addr, fn_name: "pause_pool_creation", - args: ().into_val(&env), + args: ().into_val(&t.env), sub_invokes: &[], }, }]) @@ -1370,3 +1405,40 @@ fn test_pause_pool_creation_requires_admin_auth() { "non-admin pause_pool_creation must be rejected" ); } + +#[test] +fn test_admin_transfer_count_increments_on_transfer() { + let t = setup(); + assert_eq!(t.client.admin_transfer_count(), 0); + assert_eq!(t.client.get_admin_transfer_count(), 0); + + let new_admin = Address::generate(&t.env); + t.client.transfer_admin(&new_admin); + assert_eq!(t.client.admin_transfer_count(), 1); + assert_eq!(t.client.get_admin_transfer_count(), 1); + + let next_admin = Address::generate(&t.env); + t.client.transfer_admin(&next_admin); + assert_eq!(t.client.admin_transfer_count(), 2); + assert_eq!(t.client.get_admin_transfer_count(), 2); + + // Rejected non-admin attempt does not increment + let unauthorized = Address::generate(&t.env); + let candidate = Address::generate(&t.env); + let args = (&candidate,).into_val(&t.env); + let invoke = MockAuthInvoke { + contract: &t.factory_addr, + fn_name: "transfer_admin", + args, + sub_invokes: &[], + }; + let result = t + .client + .mock_auths(&[MockAuth { + address: &unauthorized, + invoke: &invoke, + }]) + .try_transfer_admin(&candidate); + assert!(result.is_err()); + assert_eq!(t.client.admin_transfer_count(), 2); +} diff --git a/soroban/contracts/factory/src/types.rs b/soroban/contracts/factory/src/types.rs index 9c2809f..ca5292a 100644 --- a/soroban/contracts/factory/src/types.rs +++ b/soroban/contracts/factory/src/types.rs @@ -13,6 +13,8 @@ pub enum DataKey { Pool(u32), /// Flag indicating if pool creation is currently paused. PoolCreationPaused, + /// Running count of admin transfers performed. + AdminTransferCount, } /// On-chain record for a registered farming pool. @@ -116,4 +118,6 @@ pub enum FactoryError { InvalidMinStakeAmount = 12, /// `create_pool` was called while pool creation is paused. PoolCreationPaused = 13, + /// `set_pool_wasm_hash` or `initialize` was called with an all-zero WASM hash. + InvalidWasmHash = 14, } diff --git a/soroban/contracts/farming-pool/src/lib.rs b/soroban/contracts/farming-pool/src/lib.rs index 5ec4821..51eea5d 100644 --- a/soroban/contracts/farming-pool/src/lib.rs +++ b/soroban/contracts/farming-pool/src/lib.rs @@ -272,6 +272,34 @@ fn decrement_staked_user_count(env: &Env) { } } +fn read_lock_count(env: &Env) -> u32 { + env.storage() + .instance() + .get(&DataKey::LockCount) + .unwrap_or(0) +} + +fn increment_lock_count(env: &Env) { + let count = read_lock_count(env); + env.storage() + .instance() + .set(&DataKey::LockCount, &(count + 1)); +} + +fn read_unstake_count(env: &Env) -> u32 { + env.storage() + .instance() + .get(&DataKey::UnstakeCount) + .unwrap_or(0) +} + +fn increment_unstake_count(env: &Env) { + let count = read_unstake_count(env); + env.storage() + .instance() + .set(&DataKey::UnstakeCount, &(count + 1)); +} + fn set_banked_credits(env: &Env, user: &Address, totals: BankedCreditTotals) { let key = DataKey::BankedCredits(user.clone()); env.storage().persistent().set(&key, &totals); @@ -686,6 +714,7 @@ impl FarmingPool { if !was_staked && is_user_staked(&env, &user) { increment_staked_user_count(&env); } + increment_lock_count(&env); add_total_staked(&env, amount); let stake_token = get_stake_token(&env)?; @@ -1026,7 +1055,7 @@ impl FarmingPool { let key = DataKey::Whitelisted(user.clone()); env.storage().persistent().remove(&key); - let mut users = get_whitelisted_users_list(&env); + let users = get_whitelisted_users_list(&env); let mut new_users: Vec
= Vec::new(&env); for u in users.iter() { if u != user { @@ -1238,6 +1267,7 @@ impl FarmingPool { if was_staked && !is_user_staked(&env, &from) { decrement_staked_user_count(&env); } + increment_unstake_count(&env); subtract_total_staked(&env, stake.amount); Ok(total_credits) } @@ -1501,6 +1531,43 @@ impl FarmingPool { .get(&DataKey::TotalDistributedCredits) .unwrap_or(0)) } + + /// Return the count of currently staked unique users in the pool. + pub fn staked_user_count(env: Env) -> Result { + require_initialized(&env)?; + bump_instance(&env); + Ok(env + .storage() + .instance() + .get(&DataKey::StakedUserCount) + .unwrap_or(0)) + } + + pub fn get_staked_user_count(env: Env) -> Result { + Self::staked_user_count(env) + } + + /// Return the total number of lock operations performed on the pool. + pub fn lock_count(env: Env) -> Result { + require_initialized(&env)?; + bump_instance(&env); + Ok(read_lock_count(&env)) + } + + pub fn get_lock_count(env: Env) -> Result { + Self::lock_count(env) + } + + /// Return the total number of unstake operations performed on the pool. + pub fn unstake_count(env: Env) -> Result { + require_initialized(&env)?; + bump_instance(&env); + Ok(read_unstake_count(&env)) + } + + pub fn get_unstake_count(env: Env) -> Result { + Self::unstake_count(env) + } } mod test; diff --git a/soroban/contracts/farming-pool/src/test.rs b/soroban/contracts/farming-pool/src/test.rs index 25db8ef..c08c2c0 100644 --- a/soroban/contracts/farming-pool/src/test.rs +++ b/soroban/contracts/farming-pool/src/test.rs @@ -2702,7 +2702,7 @@ fn test_staked_user_count_increments_and_decrements_correctly() { assert_eq!(t.client.get_staked_user_count(), 0); let user2 = Address::generate(&t.env); - t.token_admin_client.mint(&user2, &10_000); + t.token_sac.mint(&user2, &10_000); // User 1 stakes: count becomes 1 t.client.stake(&t.user, &1_000); @@ -2747,17 +2747,84 @@ fn test_lock_assets_top_up_extends_unlock_ledger() { // Trying to unlock at ledger start_ledger + 12 should fail advance_ledgers(&t.env, 7); // now sequence is start_ledger + 12 - match t.client.try_unlock_assets(&t.user, &1_500) { - Err(Ok(PoolError::MinimumLockNotElapsed)) => {} - other => assert!( - other.is_err(), - "unlock before extended lock period must fail" - ), - } + assert!( + t.client.try_unlock_assets(&t.user, &1_500).is_err(), + "unlock before extended lock period must fail" + ); // Advancing past start_ledger + 15 allows full unlock advance_ledgers(&t.env, 3); // now sequence is start_ledger + 15 t.client.unlock_assets(&t.user, &1_500); } +#[test] +fn test_lock_count_increments_on_every_lock_operation() { + let t = setup(1, 10); + assert_eq!(t.client.lock_count(), 0); + assert_eq!(t.client.get_lock_count(), 0); + + let user2 = Address::generate(&t.env); + t.token_sac.mint(&user2, &10_000); + + // Flexible staking does not affect lock_count + t.client.stake(&t.user, &1_000); + assert_eq!(t.client.lock_count(), 0); + t.client.unstake(&t.user); + assert_eq!(t.client.lock_count(), 0); + + // User 1 locks: lock_count becomes 1 + t.client.lock_assets(&t.user, &1_000); + assert_eq!(t.client.lock_count(), 1); + assert_eq!(t.client.get_lock_count(), 1); + + // User 2 locks: lock_count becomes 2 + t.client.lock_assets(&user2, &2_000); + assert_eq!(t.client.lock_count(), 2); + + // User 1 top-up (additional lock operation): lock_count becomes 3 + t.client.lock_assets(&t.user, &500); + assert_eq!(t.client.lock_count(), 3); + + // Unlocking assets does not decrement lock_count + advance_ledgers(&t.env, 10); + t.client.unlock_assets(&user2, &2_000); + assert_eq!(t.client.lock_count(), 3); +} + +#[test] +fn test_unstake_count_increments_on_every_unstake_operation() { + let t = setup(1, 10); + assert_eq!(t.client.unstake_count(), 0); + assert_eq!(t.client.get_unstake_count(), 0); + + let user2 = Address::generate(&t.env); + t.token_sac.mint(&user2, &10_000); + + // Staking does not affect unstake_count + t.client.stake(&t.user, &1_000); + t.client.stake(&user2, &2_000); + assert_eq!(t.client.unstake_count(), 0); + + // Lock/unlock operations do not affect unstake_count + let user3 = Address::generate(&t.env); + t.token_sac.mint(&user3, &10_000); + t.client.lock_assets(&user3, &1_000); + assert_eq!(t.client.unstake_count(), 0); + + // User 1 unstakes: unstake_count becomes 1 + t.client.unstake(&t.user); + assert_eq!(t.client.unstake_count(), 1); + assert_eq!(t.client.get_unstake_count(), 1); + + // User 2 unstakes: unstake_count becomes 2 + t.client.unstake(&user2); + assert_eq!(t.client.unstake_count(), 2); + assert_eq!(t.client.get_unstake_count(), 2); + + // Unlocking assets does not increment unstake_count + advance_ledgers(&t.env, 10); + t.client.unlock_assets(&user3, &1_000); + assert_eq!(t.client.unstake_count(), 2); +} + diff --git a/soroban/contracts/farming-pool/src/types.rs b/soroban/contracts/farming-pool/src/types.rs index 796540d..849d8ea 100644 --- a/soroban/contracts/farming-pool/src/types.rs +++ b/soroban/contracts/farming-pool/src/types.rs @@ -108,6 +108,11 @@ pub enum DataKey { TotalStaked, /// Cumulative credits committed to users since pool initialization. TotalDistributedCredits, + StakedUserCount, + /// Running count of total lock operations performed. + LockCount, + /// Running count of total unstake operations performed. + UnstakeCount, } /// Paginated response for `get_whitelisted_users`. diff --git a/soroban/contracts/vesting-wallet/src/lib.rs b/soroban/contracts/vesting-wallet/src/lib.rs index 9289ec3..3a0ab11 100644 --- a/soroban/contracts/vesting-wallet/src/lib.rs +++ b/soroban/contracts/vesting-wallet/src/lib.rs @@ -82,6 +82,20 @@ fn is_revoked(env: &Env) -> bool { .unwrap_or(false) } +fn read_release_count(env: &Env) -> u32 { + env.storage() + .instance() + .get(&DataKey::ReleaseCount) + .unwrap_or(0) +} + +fn increment_release_count(env: &Env) { + let count = read_release_count(env); + env.storage() + .instance() + .set(&DataKey::ReleaseCount, &(count + 1)); +} + // ── Vesting formula ─────────────────────────────────────────────────────────── /// Linear vesting with cliff. @@ -228,6 +242,8 @@ impl VestingWallet { return Ok(0); } + increment_release_count(&env); + env.storage() .instance() .set(&DataKey::ReleasedAmount, &(released + releasable)); @@ -408,6 +424,17 @@ impl VestingWallet { Ok(()) } + + /// Return the total number of release operations performed. + pub fn release_count(env: Env) -> Result { + require_initialized(&env)?; + bump_instance(&env); + Ok(read_release_count(&env)) + } + + pub fn get_release_count(env: Env) -> Result { + Self::release_count(env) + } } mod test; diff --git a/soroban/contracts/vesting-wallet/src/test.rs b/soroban/contracts/vesting-wallet/src/test.rs index fbe6a76..b4a27e0 100644 --- a/soroban/contracts/vesting-wallet/src/test.rs +++ b/soroban/contracts/vesting-wallet/src/test.rs @@ -2,10 +2,9 @@ use super::*; use soroban_sdk::{ - symbol_short, testutils::{Address as _, Events, Ledger}, token::{StellarAssetClient, TokenClient}, - Address, Env, IntoVal, + Address, Env, }; // ── Test helpers ────────────────────────────────────────────────────────────── @@ -529,3 +528,34 @@ fn test_get_vesting_schedule_uninitialized_returns_not_initialized() { Err(Ok(VestingError::NotInitialized)) )); } + +#[test] +fn test_release_count_increments_on_release() { + let t = setup(50, 200, 1_000); + + // Initially 0 + assert_eq!(t.client.release_count(), 0); + assert_eq!(t.client.get_release_count(), 0); + + // Release before cliff (0 tokens releasable) does not increment count + assert_eq!(t.client.release(), 0); + assert_eq!(t.client.release_count(), 0); + + // Advance past cliff and release tokens + advance_ledgers(&t.env, 100); + let released1 = t.client.release(); + assert!(released1 > 0); + assert_eq!(t.client.release_count(), 1); + assert_eq!(t.client.get_release_count(), 1); + + // Immediate subsequent release (0 tokens) does not increment + assert_eq!(t.client.release(), 0); + assert_eq!(t.client.release_count(), 1); + + // Advance to end and release remainder + advance_ledgers(&t.env, 200); + let released2 = t.client.release(); + assert!(released2 > 0); + assert_eq!(t.client.release_count(), 2); + assert_eq!(t.client.get_release_count(), 2); +} diff --git a/soroban/contracts/vesting-wallet/src/types.rs b/soroban/contracts/vesting-wallet/src/types.rs index 1f68004..e6863f9 100644 --- a/soroban/contracts/vesting-wallet/src/types.rs +++ b/soroban/contracts/vesting-wallet/src/types.rs @@ -38,6 +38,8 @@ pub enum DataKey { Revoked, /// Vested amount frozen at the moment of revocation. RevokedVested, + /// Running count of release operations performed. + ReleaseCount, } /// Emitted when admin rights are transferred.