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
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ jobs:
restore-keys: |
${{ runner.os }}-cargo-

- name: Build WASM fixtures for factory integration tests
- name: Check formatting
working-directory: soroban
run: cargo fmt --all -- --check
Expand All @@ -42,7 +43,7 @@ jobs:

- name: Build farming-pool WASM fixture for factory integration tests
working-directory: soroban
run: cargo build -p farming-pool --target wasm32v1-none --release
run: cargo build --workspace --target wasm32v1-none --release

- name: Run tests
working-directory: soroban
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build:
cd soroban && cargo build --target wasm32-unknown-unknown --release

test:
cd soroban && cargo build -p farming-pool --target wasm32v1-none --release
cd soroban && cargo build --workspace --target wasm32v1-none --release
cd soroban && cargo test --workspace

deploy-testnet:
Expand Down
33 changes: 32 additions & 1 deletion soroban/contracts/factory/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

mod types;

use soroban_sdk::{contract, contractimpl, symbol_short, vec, Address, BytesN, Env, IntoVal, Symbol, Val, Vec};
use soroban_sdk::{
contract, contractimpl, symbol_short, vec, Address, BytesN, Env, IntoVal, Symbol, Val, Vec,
};
use types::{DataKey, FactoryError, ListPoolsResponse, PoolRecord};

// ~30 days at ~5 s/ledger; extend to ~60 days when below threshold.
Expand Down Expand Up @@ -255,6 +257,35 @@ impl Factory {
);
}

/// Upgrade one registered farming pool in place. Admin-only.
///
/// This deliberately does not update the factory-level `WasmHash`; it is a
/// pool-by-pool hot swap for a pre-installed WASM hash.
pub fn upgrade_pool(
env: Env,
pool_id: u32,
new_wasm_hash: BytesN<32>,
) -> Result<(), FactoryError> {
let admin = load_admin(&env);
admin.require_auth();
bump_instance(&env);

let key = DataKey::Pool(pool_id);
let record = env
.storage()
.persistent()
.get::<DataKey, PoolRecord>(&key)
.ok_or(FactoryError::PoolNotFound)?;
bump_pool(&env, pool_id);

let upgrade_args: Vec<Val> = vec![&env, new_wasm_hash.clone().into_val(&env)];
let _: () =
env.invoke_contract(&record.address, &Symbol::new(&env, "upgrade"), upgrade_args);

#[allow(deprecated)]
env.events().publish(
(symbol_short!("factory"), symbol_short!("pool_upg")),
(pool_id, record.address, new_wasm_hash),
/// Update the WASM hash used for future `create_pool` deployments. Admin-only.
///
/// Allows the admin to point future pool deployments at a corrected or upgraded
Expand Down
104 changes: 103 additions & 1 deletion soroban/contracts/factory/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,21 @@ 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");
}

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)
}

/// Builds an initialised factory using the real farming-pool WASM.
fn setup() -> TestEnv {
let env = Env::default();
Expand Down Expand Up @@ -303,6 +314,95 @@ fn test_transfer_admin_emits_event_with_old_and_new_admin() {
);
}

// ── upgrade_pool ──────────────────────────────────────────────────────────────

#[test]
fn test_upgrade_pool_hot_swaps_registered_pool_without_changing_factory_hash() {
let t = setup();
let original_factory_hash = t.client.pool_wasm_hash();
let pool_id = t
.client
.create_pool(&Address::generate(&t.env), &1_728_000u128, &2u32, &10u64);
let pool_addr = t.client.get_pool(&pool_id).address;

let new_wasm_hash = upload_replacement_wasm(&t.env);
t.client.upgrade_pool(&pool_id, &new_wasm_hash);

assert_eq!(
t.env.events().all(),
vec![
&t.env,
(
pool_addr.clone(),
vec![
&t.env,
symbol_short!("pool").into_val(&t.env),
symbol_short!("upgraded").into_val(&t.env),
],
new_wasm_hash.clone().into_val(&t.env),
),
(
t.factory_addr.clone(),
vec![
&t.env,
symbol_short!("factory").into_val(&t.env),
symbol_short!("pool_upg").into_val(&t.env),
],
(pool_id, pool_addr.clone(), new_wasm_hash.clone()).into_val(&t.env),
)
]
);

assert_eq!(
t.client.pool_wasm_hash(),
original_factory_hash,
"pool-by-pool upgrades must not replace the factory default hash"
);
assert_eq!(
pool_record_ttl(&t.env, &t.factory_addr, pool_id),
TTL_EXTEND_TO
);
let record = t.client.get_pool(&pool_id);
assert_eq!(record.address, pool_addr);
}

#[test]
fn test_upgrade_pool_missing_pool_returns_not_found() {
let t = setup();
let new_wasm_hash = t.wasm_hash.clone();
assert_eq!(
t.client.try_upgrade_pool(&0u32, &new_wasm_hash),
Err(Ok(FactoryError::PoolNotFound))
);
}

#[test]
fn test_upgrade_pool_requires_factory_admin_auth() {
let t = setup();
let pool_id = t
.client
.create_pool(&Address::generate(&t.env), &1_728_000u128, &2u32, &10u64);

let not_admin = Address::generate(&t.env);
let new_wasm_hash = t.wasm_hash.clone();
let args = (&pool_id, &new_wasm_hash).into_val(&t.env);
let invoke = MockAuthInvoke {
contract: &t.factory_addr,
fn_name: "upgrade_pool",
args,
sub_invokes: &[],
};
let result = t
.client
.mock_auths(&[MockAuth {
address: &not_admin,
invoke: &invoke,
}])
.try_upgrade_pool(&pool_id, &new_wasm_hash);

assert!(result.is_err(), "only the factory admin may upgrade pools");
}

// ── create_pool auth gate ─────────────────────────────────────────────────────

#[test]
Expand Down Expand Up @@ -714,7 +814,9 @@ fn test_create_pool_configures_deployed_pool_matching_factory_record() {
let t = setup();
let asset = Address::generate(&t.env);

let id = t.client.create_pool(&asset, &17_280_000u128, &3u32, &86_400u64);
let id = t
.client
.create_pool(&asset, &17_280_000u128, &3u32, &86_400u64);
let record = t.client.get_pool(&id);

assert_eq!(record.credit_rate, 1_000);
Expand Down
45 changes: 44 additions & 1 deletion soroban/contracts/farming-pool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ mod types;
#[cfg(test)]
mod mock_reentrant_token;

use soroban_sdk::{contract, contractimpl, symbol_short, token, Address, Env};
use soroban_sdk::{contract, contractimpl, symbol_short, token, Address, BytesN, Env};
pub use types::PoolError;
use types::{BoostConfig, DataKey, Position, UserStake};

pub const SCHEMA_VERSION: u32 = 1;

// Persistent-storage TTL: extend to ~60 days if below ~30 days (at ~5s/ledger).
const USER_TTL_THRESHOLD: u32 = 518_400;
const USER_TTL_EXTEND_TO: u32 = 1_036_800;
Expand Down Expand Up @@ -81,6 +83,13 @@ fn pool_is_paused(env: &Env) -> bool {
.unwrap_or(false)
}

fn read_schema_version(env: &Env) -> u32 {
env.storage()
.instance()
.get(&DataKey::SchemaVersion)
.unwrap_or(SCHEMA_VERSION)
}

fn get_user_boost(env: &Env, user: &Address) -> Option<u32> {
let key = DataKey::UserBoost(user.clone());
let value: Option<u32> = env.storage().persistent().get(&key);
Expand Down Expand Up @@ -211,6 +220,9 @@ impl FarmingPool {
env.storage()
.instance()
.set(&DataKey::MinLockPeriod, &min_lock_period);
env.storage()
.instance()
.set(&DataKey::SchemaVersion, &SCHEMA_VERSION);
bump_instance(&env);
Ok(())
}
Expand All @@ -233,6 +245,37 @@ impl FarmingPool {
Ok(())
}

pub fn schema_version(env: Env) -> u32 {
bump_instance(&env);
read_schema_version(&env)
}

pub fn migrate(env: Env) -> Result<u32, PoolError> {
require_initialized(&env)?;
get_admin(&env)?.require_auth();
bump_instance(&env);

let current = read_schema_version(&env);
env.storage()
.instance()
.set(&DataKey::SchemaVersion, &SCHEMA_VERSION);
Ok(current)
}

pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) -> Result<(), PoolError> {
require_initialized(&env)?;
get_admin(&env)?.require_auth();
bump_instance(&env);

#[allow(deprecated)]
env.events().publish(
(symbol_short!("pool"), symbol_short!("upgraded")),
new_wasm_hash.clone(),
);
env.deployer().update_current_contract_wasm(new_wasm_hash);
Ok(())
}

pub fn lock_assets(env: Env, user: Address, amount: i128) -> Result<(), PoolError> {
user.require_auth();
require_initialized(&env)?;
Expand Down
Loading