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
64 changes: 64 additions & 0 deletions soroban/contracts/factory/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,70 @@ fn test_set_pool_wasm_hash_rejects_zero_hash() {
);
}

// #410 — the audit trail for a WASM-hash change is only useful if the event
// carries the *previous* hash as well as the new one: without the old hash an
// operator cannot tell which build was live before the change, so a rollback
// decision has nothing to verify against. This pins both hashes in the emitted
// event so a future edit to the payload cannot silently drop the old value.
#[test]
fn test_set_pool_wasm_hash_event_carries_old_and_new_hash() {
let t = setup();
let original_hash = t.wasm_hash.clone();
let new_hash = upload_replacement_wasm(&t.env);

t.client.set_pool_wasm_hash(&new_hash);

assert_eq!(
t.env.events().all(),
vec![
&t.env,
(
t.factory_addr.clone(),
vec![
&t.env,
symbol_short!("factory").into_val(&t.env),
symbol_short!("wasm_set").into_val(&t.env),
],
(original_hash, new_hash).into_val(&t.env),
)
]
);
}

#[test]
fn test_set_pool_wasm_hash_event_old_hash_matches_superseded_value() {
let t = setup();
let first_hash = upload_replacement_wasm(&t.env);
let second_hash = BytesN::from_array(&t.env, &[7u8; 32]);

t.client.set_pool_wasm_hash(&first_hash);
t.client.set_pool_wasm_hash(&second_hash);

// The second change must report the first change's value as the old hash,
// which is only checkable by reading the events in order.
let events = t.env.events().all();
let wasm_set: Vec<_> = events
.events()
.iter()
.filter(|(_, topics, _)| {
topics
== &vec![
&t.env,
symbol_short!("factory").into_val(&t.env),
symbol_short!("wasm_set").into_val(&t.env),
]
})
.collect();

assert_eq!(wasm_set.len(), 2);
let (_, _, second_payload) = wasm_set[1];
assert_eq!(
second_payload,
(first_hash, second_hash).into_val(&t.env),
"the second event must pair the superseded hash with the new one"
);
}

// ── pool_count ────────────────────────────────────────────────────────────────

#[test]
Expand Down
59 changes: 58 additions & 1 deletion soroban/contracts/vesting-wallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ mod types;

use soroban_sdk::{contract, contractimpl, symbol_short, token, Address, Env};
use types::DataKey;
pub use types::{AdminTransferred, VestingError, VestingSchedule};
pub use types::{AdminTransferred, VestingError, VestingOverview, VestingSchedule};

// Persistent-storage TTL: extend to ~60 days if below ~30 days (at ~5 s/ledger).
const TTL_THRESHOLD: u32 = 518_400;
Expand Down Expand Up @@ -322,6 +322,25 @@ impl VestingWallet {
Ok(())
}

/// Claim every currently-vested token in a single call.
///
/// Convenience wrapper over {@link VestingWallet::release}, which already
/// transfers the whole vested-but-unclaimed balance in one transfer, so
/// this adds a self-documenting entry point rather than a second code path
/// — the release logic, the beneficiary authorisation and the
/// `vest/released` event are identical because it delegates.
///
/// Note the signature deliberately takes no `beneficiary` argument: the
/// beneficiary is read from storage and is the account that must authorise
/// the call, so an argument would either be ignored or let a third party
/// force a release at a time the beneficiary did not choose (#407).
///
/// Returns the total amount transferred, which is 0 when nothing has vested
/// yet, and `NotInitialized` if the wallet was never initialized.
pub fn release_all(env: Env) -> Result<i128, VestingError> {
Self::release(env)
}

/// Return the total amount vested as of the current ledger.
pub fn vested_amount(env: Env) -> Result<i128, VestingError> {
require_initialized(&env)?;
Expand Down Expand Up @@ -389,6 +408,44 @@ impl VestingWallet {
})
}

/// Return the whole schedule *and* its live progress in a single call.
///
/// `get_vesting_schedule` returns the configured parameters only, so a
/// frontend still had to follow it with `vested_amount`,
/// `released_amount` and `releasable` — four round trips for one vesting
/// overview, and a real risk of the components disagreeing because they
/// were read at different ledgers. This returns all of it atomically.
///
/// Kept as a separate type from `VestingSchedule` so that struct's layout —
/// and the XDR clients already decode — is untouched (#409).
///
/// `releasable_amount` is what `release_all` would transfer right now.
/// Returns `NotInitialized` if the wallet has not been initialized.
pub fn get_vesting_overview(env: Env) -> Result<VestingOverview, VestingError> {
require_initialized(&env)?;
bump_instance(&env);

let vested = compute_vested(&env)?;
let released = get_released(&env);

Ok(VestingOverview {
beneficiary: get_beneficiary(&env),
token: get_token(&env),
total_amount: get_total_amount(&env),
start_ledger: get_start_ledger(&env),
cliff_ledger: get_cliff_ledger(&env),
end_ledger: get_end_ledger(&env),
revocable: is_revocable(&env),
revoked: is_revoked(&env),
vested_amount: vested,
released_amount: released,
// Saturating, not plain subtraction: after revocation the frozen
// vested amount can be lower than what was already released, and a
// negative "releasable" would be nonsense to a caller.
releasable_amount: vested.saturating_sub(released),
})
}

/// Returns `(start_ledger, cliff_ledger, end_ledger)` in a single read for
/// frontends that render the vesting schedule (#256). Returns
/// `NotInitialized` if the wallet has not been initialized.
Expand Down
Loading