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
28 changes: 28 additions & 0 deletions soroban/contracts/factory/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,20 @@ fn insert_sorted(records: &mut Vec<(u32, PoolRecord)>, record: (u32, PoolRecord)
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;

Expand Down Expand Up @@ -534,6 +548,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")),
Expand All @@ -542,6 +557,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<u32, FactoryError> {
require_initialized(&env)?;
bump_instance(&env);
Ok(read_admin_transfer_count(&env))
}

pub fn get_admin_transfer_count(env: Env) -> Result<u32, FactoryError> {
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
Expand Down
94 changes: 83 additions & 11 deletions soroban/contracts/factory/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -1368,15 +1401,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: &[],
},
}])
Expand All @@ -1387,3 +1422,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);
}
4 changes: 4 additions & 0 deletions soroban/contracts/factory/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
/// Running total of successful `upgrade_pool` calls, for version tracking (#258).
UpgradeCount,
}
Expand Down Expand Up @@ -118,6 +120,8 @@ 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,
/// `create_pool`'s minimum lock period is below the minimum allowed threshold.
MinLockPeriodTooShort = 14,
}
67 changes: 66 additions & 1 deletion soroban/contracts/farming-pool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,32 @@ 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 get_emergency_withdrawal_count(env: &Env) -> u32 {
env.storage()
.instance()
Expand Down Expand Up @@ -710,6 +736,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)?;
Expand Down Expand Up @@ -1093,7 +1120,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<Address> = Vec::new(&env);
for u in users.iter() {
if u != user {
Expand Down Expand Up @@ -1309,6 +1336,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)
}
Expand Down Expand Up @@ -1599,6 +1627,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<u32, PoolError> {
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<u32, PoolError> {
Self::staked_user_count(env)
}

/// Return the total number of lock operations performed on the pool.
pub fn lock_count(env: Env) -> Result<u32, PoolError> {
require_initialized(&env)?;
bump_instance(&env);
Ok(read_lock_count(&env))
}

pub fn get_lock_count(env: Env) -> Result<u32, PoolError> {
Self::lock_count(env)
}

/// Return the total number of unstake operations performed on the pool.
pub fn unstake_count(env: Env) -> Result<u32, PoolError> {
require_initialized(&env)?;
bump_instance(&env);
Ok(read_unstake_count(&env))
}

pub fn get_unstake_count(env: Env) -> Result<u32, PoolError> {
Self::unstake_count(env)
}
}

mod test;
83 changes: 75 additions & 8 deletions soroban/contracts/farming-pool/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2667,7 +2667,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);
Expand Down Expand Up @@ -2712,17 +2712,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);
}


Loading