Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
56704cf
fix(#466): add fee-sum check to set_default_rule and _set_default_rule
Seunfunmi-319509 Aug 26, 2026
c46a3a8
Merge branch 'main' into fix/466-default-rule-fee-sum-check
Seunfunmi-319509 Aug 26, 2026
bd93ad3
fix: resolve clippy and fmt CI failures for #641
Seunfunmi-319509 Aug 26, 2026
37a9dae
fix: restore auth mocking and panic guards lost in merge
oluwaseunshowunmi97-byte Aug 27, 2026
a67e86f
Merge branch 'main' into fix/466-default-rule-fee-sum-check
oluwaseunshowunmi97-byte Aug 27, 2026
47c3845
chore: remove stray pr.md merged from main
oluwaseunshowunmi97-byte Aug 27, 2026
39f2932
style: format merged code
oluwaseunshowunmi97-byte Aug 27, 2026
f41a0b0
Merge branch 'main' into fix/466-default-rule-fee-sum-check
Seunfunmi-319509 Aug 29, 2026
41ee314
Merge branch 'main' into fix/466-default-rule-fee-sum-check
Seunfunmi-319509 Aug 30, 2026
5650eef
fix: resolve init deployer arg, duplicate imports, and fmt issues exp…
Seunfunmi-319509 Aug 30, 2026
1fbcea4
fix: add missing Symbol import and remove redundant clones flagged by…
Seunfunmi-319509 Aug 30, 2026
77604f0
fix: resolve clippy errors in governance_error_tests.rs
Seunfunmi-319509 Aug 30, 2026
d19a5a6
fix: use governance_contract::FeeConfig for gov_client.set_fee_config()
Seunfunmi-319509 Aug 30, 2026
84cc88e
fix: update tests to match contract behavior changes
Seunfunmi-319509 Aug 30, 2026
270fabb
fix: remove unused variables flagged by clippy -D warnings
Seunfunmi-319509 Aug 30, 2026
b82a4ab
fix: correct expected fee values in bootstrap_fallback_uses_canonical…
Seunfunmi-319509 Aug 30, 2026
a03adc8
Merge branch 'main' into fix/466-default-rule-fee-sum-check
Seunfunmi-319509 Aug 31, 2026
9483f11
fix: repair main-merge regressions for CI (clippy, fmt, tests)
Seunfunmi-319509 Aug 31, 2026
06468f1
fix: repair 6 failing settlement tests after main merge
Seunfunmi-319509 Aug 31, 2026
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
7 changes: 1 addition & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ clean:

fmt:
cargo fmt --all -- --check
.PHONY: fmt test check clippy all

fmt:
cargo fmt --all --check

test:
cargo test --workspace
Expand All @@ -47,5 +43,4 @@ wasm_size: optimize
check_codeowners:
bash scripts/check_codeowners.sh

all: fmt check clippy test test_scripts wasm_size check_codeowners
all: fmt check clippy test
all: fmt check clippy test test_scripts check_codeowners
5 changes: 4 additions & 1 deletion bettapay_common/src/error_codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,10 @@ pub const OPERATION_ALREADY_SCHEDULED: u32 = 12;
pub const INVALID_WASM_INTERFACE: u32 = 13;
/// The provided multisig threshold is invalid.
pub const INVALID_THRESHOLD: u32 = 14;
/// A recovery operation is already pending; a second one cannot be initiated.
pub const RECOVERY_ALREADY_PENDING: u32 = 15;
/// `pause` was called while the contract was already paused.
pub const ALREADY_PAUSED: u32 = 15;
pub const ALREADY_PAUSED: u32 = 17;
/// `unpause` was called while the contract was already unpaused.
pub const ALREADY_UNPAUSED: u32 = 16;

Expand All @@ -75,6 +77,7 @@ pub const SHARED_CODES: &[(&str, u32)] = &[
("OperationAlreadyScheduled", OPERATION_ALREADY_SCHEDULED),
("InvalidWasmInterface", INVALID_WASM_INTERFACE),
("InvalidThreshold", INVALID_THRESHOLD),
("RecoveryAlreadyPending", RECOVERY_ALREADY_PENDING),
("AlreadyPaused", ALREADY_PAUSED),
("AlreadyUnpaused", ALREADY_UNPAUSED),
];
Expand Down
28 changes: 22 additions & 6 deletions bettapay_common/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,12 +208,20 @@ mod compatibility_tests {
fn common_data_key_encoding_matches_legacy() {
let env = Env::default();
let mut map: soroban_sdk::Map<Val, u32> = soroban_sdk::Map::new(&env);

map.set(LegacyDataKey::RecoveryAddress.into_val(&env), 1u32);
assert_eq!(map.get(CommonDataKey::RecoveryAddress.into_val(&env)), Some(1u32), "RecoveryAddress encoding mismatch");
assert_eq!(
map.get(CommonDataKey::RecoveryAddress.into_val(&env)),
Some(1u32),
"RecoveryAddress encoding mismatch"
);

map.set(LegacyDataKey::PendingRecovery.into_val(&env), 2u32);
assert_eq!(map.get(CommonDataKey::PendingRecovery.into_val(&env)), Some(2u32), "PendingRecovery encoding mismatch");
assert_eq!(
map.get(CommonDataKey::PendingRecovery.into_val(&env)),
Some(2u32),
"PendingRecovery encoding mismatch"
);

// Note: Paused was also a unit variant in the legacy DataKey.
// We'll just define another legacy enum for it or reuse the same.
Expand All @@ -223,11 +231,19 @@ mod compatibility_tests {
Paused,
SystemParam(soroban_sdk::Symbol),
}

map.set(LegacyDataKey2::Paused.into_val(&env), 3u32);
assert_eq!(map.get(CommonDataKey::Paused.into_val(&env)), Some(3u32), "Paused encoding mismatch");
assert_eq!(
map.get(CommonDataKey::Paused.into_val(&env)),
Some(3u32),
"Paused encoding mismatch"
);

map.set(LegacyDataKey::Threshold.into_val(&env), 4u32);
assert_eq!(map.get(CommonDataKey::Threshold.into_val(&env)), Some(4u32), "Threshold encoding mismatch");
assert_eq!(
map.get(CommonDataKey::Threshold.into_val(&env)),
Some(4u32),
"Threshold encoding mismatch"
);
}
}
5 changes: 3 additions & 2 deletions governance_contract/src/anchor_no_event_error_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,8 @@ fn change_threshold_emits_no_event_when_insufficient_signatures() {
let recovery = Address::generate(&env);
let contract_id = env.register_contract(None, GovernanceContract);
let client = GovernanceContractClient::new(&env, &contract_id);
client.init(&admins, &2, &recovery);
let deployer = Address::generate(&env);
client.init(&deployer, &admins, &2, &recovery);

let single_signer = vec![&env, a1.clone()];
let prev = env.events().all().len();
Expand All @@ -241,7 +242,7 @@ fn change_threshold_emits_no_event_when_insufficient_signatures() {
// ---------------------------------------------------------------------------

#[test]
#[should_panic(expected = "Error(Contract, #15)")]
#[should_panic(expected = "Error(Contract, #17)")]
fn pause_emits_no_event_when_already_paused() {
let (env, client, admins) = setup();
client.pause(&admins);
Expand Down
2 changes: 1 addition & 1 deletion governance_contract/src/anchor_removal_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ mod tests {
let asset = Address::generate(&env);
let anchor = Address::generate(&env);
client.upsert_anchor(&admin, &asset, &anchor);
assert_eq!(client.get_anchor(&asset), Some(anchor.clone()));
assert_eq!(client.get_anchor(&asset), Some(anchor));
client.remove_anchor(&admin, &asset);
assert_eq!(client.get_anchor(&asset), None);
}
Expand Down
100 changes: 73 additions & 27 deletions governance_contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,11 +277,13 @@ pub enum GovernanceError {
InvalidWasmInterface = 13,
/// The provided multisig threshold is invalid.
InvalidThreshold = 14,
/// A recovery is already pending; initiate_recovery cannot overwrite it.
RecoveryAlreadyPending = 15,
/// The anchor for the specified asset was not found.
AnchorMissing = 200,
InvalidParamValue = 201,
/// `pause` was called while the contract was already paused.
AlreadyPaused = 15,
AlreadyPaused = 17,
/// `unpause` was called while the contract was already unpaused.
AlreadyUnpaused = 16,
/// The new admin set and threshold are identical to the current ones.
Expand All @@ -302,6 +304,9 @@ const _: () = {
assert!(GovernanceError::RecoveryDelayActive as u32 == error_codes::RECOVERY_DELAY_ACTIVE);
assert!(GovernanceError::InvalidWasmInterface as u32 == error_codes::INVALID_WASM_INTERFACE);
assert!(GovernanceError::InvalidThreshold as u32 == error_codes::INVALID_THRESHOLD);
assert!(
GovernanceError::RecoveryAlreadyPending as u32 == error_codes::RECOVERY_ALREADY_PENDING
);
assert!(GovernanceError::AnchorMissing as u32 >= error_codes::GOVERNANCE_RANGE_START);
assert!(GovernanceError::InvalidParamValue as u32 >= error_codes::GOVERNANCE_RANGE_START);
assert!(GovernanceError::AlreadyPaused as u32 == error_codes::ALREADY_PAUSED);
Expand Down Expand Up @@ -339,7 +344,13 @@ impl GovernanceContract {
/// # Errors
///
/// Panics with `GovernanceError::AlreadyInitialized` if already initialised.
pub fn init(env: Env, deployer: Address, admins: Vec<Address>, threshold: u32, recovery_address: Address) {
pub fn init(
env: Env,
deployer: Address,
admins: Vec<Address>,
threshold: u32,
recovery_address: Address,
) {
if env.storage().instance().has(&DataKey::Admin) {
panic_with_error!(&env, GovernanceError::AlreadyInitialized);
}
Expand Down Expand Up @@ -392,11 +403,7 @@ impl GovernanceContract {
pub fn update_recovery_address(env: Env, signers: Vec<Address>, new_recovery: Address) {
verify_admin_auth(&env, &signers, read_threshold(&env));
let admin = signers.get(0).unwrap();
assert_not_zero(
&env,
&new_recovery,
GovernanceError::InvalidRecoveryAddress,
);
assert_not_zero(&env, &new_recovery, GovernanceError::InvalidRecoveryAddress);
env.storage()
.instance()
.set(&CommonDataKey::RecoveryAddress, &new_recovery);
Expand Down Expand Up @@ -471,6 +478,14 @@ impl GovernanceContract {
recovery_address.require_auth();
assert_not_zero(&env, &new_admin, GovernanceError::InvalidAdmin);

if env
.storage()
.instance()
.has(&CommonDataKey::PendingRecovery)
{
panic_with_error!(&env, GovernanceError::RecoveryAlreadyPending);
}

let pending = PendingRecovery {
new_admin: new_admin.clone(),
execute_after: env.ledger().timestamp() + RECOVERY_DELAY_SECONDS,
Expand Down Expand Up @@ -512,7 +527,9 @@ impl GovernanceContract {

let new_admins = soroban_sdk::vec![&env, pending.new_admin.clone()];
env.storage().instance().set(&DataKey::Admin, &new_admins);
env.storage().instance().set(&CommonDataKey::Threshold, &1u32);
env.storage()
.instance()
.set(&CommonDataKey::Threshold, &1u32);
env.storage()
.instance()
.remove(&CommonDataKey::PendingRecovery);
Expand Down Expand Up @@ -706,7 +723,9 @@ impl GovernanceContract {
let key = DataKey::Anchor(asset.clone());
let old_anchor: Option<Address> = env.storage().persistent().get(&key);
env.storage().persistent().set(&key, &anchor.clone());
env.storage().persistent().extend_ttl(&key, ANCHOR_TTL_THRESHOLD, ANCHOR_TTL_BUMP);
env.storage()
.persistent()
.extend_ttl(&key, ANCHOR_TTL_THRESHOLD, ANCHOR_TTL_BUMP);
env.events().publish(
(Symbol::new(&env, events::ANCHOR_UPSERTED_EVENT), asset),
(old_anchor, anchor),
Expand Down Expand Up @@ -908,8 +927,8 @@ mod real_auth_tests;
mod tests {
use super::*;
use proptest::prelude::*;
use soroban_sdk::testutils::{Address as _, Events};
use soroban_sdk::testutils::storage::Persistent;
use soroban_sdk::testutils::{Address as _, Events};
use soroban_sdk::{vec, Bytes, FromVal, String};

fn setup() -> (
Expand All @@ -927,7 +946,7 @@ mod tests {
let recovery_address = Address::generate(&env);
let contract_id = env.register_contract(None, GovernanceContract);
let client = GovernanceContractClient::new(&env, &contract_id);
let deployer = Address::generate(&env);
let deployer = Address::generate(&env);
client.init(&deployer, &admins, &2, &recovery_address);
(env, client, admins, recovery_address)
}
Expand Down Expand Up @@ -1015,7 +1034,10 @@ mod tests {
let bad_hash = upload_test_wasm(&env); // empty wasm — no supports_interface

let result = client.try_upgrade(&admins, &bad_hash);
assert!(result.is_err(), "upgrade with non-conforming wasm must be rejected");
assert!(
result.is_err(),
"upgrade with non-conforming wasm must be rejected"
);

// Contract is intact after the failed upgrade.
let live_client = GovernanceContractClient::new(&env, &client.address);
Expand All @@ -1025,8 +1047,8 @@ mod tests {
#[test]
#[should_panic(expected = "Error(Contract, #1)")]
fn governance_rejects_double_initialization() {
let (_env, client, admins, recovery) = setup();
let deployer = Address::generate(&env);
let (env, client, admins, recovery) = setup();
let deployer = Address::generate(&env);
client.init(&deployer, &admins, &2, &recovery);
}

Expand All @@ -1039,8 +1061,8 @@ mod tests {
let recovery = Address::generate(&env);
let contract_id = env.register_contract(None, GovernanceContract);
let client = GovernanceContractClient::new(&env, &contract_id);
let deployer = Address::generate(&env);
client.init(&deployer, &vec![env, admin], &0, &recovery);
let deployer = Address::generate(&env);
client.init(&deployer, &vec![&env, admin], &0, &recovery);
}

#[test]
Expand Down Expand Up @@ -1175,7 +1197,7 @@ mod tests {
#[should_panic(expected = "Error(Contract, #4)")]
fn set_fee_config_rejects_fees_exceeding_ceiling() {
let (_env, client, admins, _recovery) = setup();

// Sum exceeds BPS_DENOMINATOR
let cfg = FeeConfig {
platform_fee_bps: 5_000,
Expand All @@ -1189,7 +1211,7 @@ mod tests {
#[should_panic(expected = "Error(Contract, #4)")]
fn set_fee_config_rejects_individual_fee_exceeding_max() {
let (_env, client, admins, _recovery) = setup();

// Individual fee exceeds MAX_FEE_BPS (governance trust root)
let cfg = FeeConfig {
platform_fee_bps: 5_001,
Expand Down Expand Up @@ -1436,7 +1458,8 @@ mod tests {
let contract_id = env.register_contract(None, GovernanceContract);
let client = GovernanceContractClient::new(&env, &contract_id);

let result = client.try_init(&admins, &threshold, &recovery);
let deployer = Address::generate(&env);
let result = client.try_init(&deployer, &admins, &threshold, &recovery);
if threshold == 0 || threshold > admin_count {
prop_assert!(result.is_err());
} else {
Expand Down Expand Up @@ -1464,8 +1487,8 @@ mod tests {
let client = GovernanceContractClient::new(&env, &contract_id);

assert!(!client.is_initialized());
let deployer = Address::generate(&env);
client.init(&deployer, &vec![env, admin.clone()], &1, &recovery_address);
let deployer = Address::generate(&env);
client.init(&deployer, &vec![&env, admin.clone()], &1, &recovery_address);
assert!(client.is_initialized());
}

Expand All @@ -1480,7 +1503,7 @@ mod tests {
let client = GovernanceContractClient::new(&env, &contract_id);

let admins = vec![&env, admin.clone()];
let deployer = Address::generate(&env);
let deployer = Address::generate(&env);
client.init(&deployer, &admins, &1, &recovery_address);
assert!(client.is_initialized());
assert_eq!(client.get_admin(), admins);
Expand Down Expand Up @@ -1545,7 +1568,7 @@ mod tests {

let contract_id = env.register_contract(None, GovernanceContract);
let client = GovernanceContractClient::new(&env, &contract_id);
let deployer = Address::generate(&env);
let deployer = Address::generate(&env);
client.init(&deployer, &admins, &1, &recovery);

assert_eq!(client.get_threshold(), 1);
Expand All @@ -1569,7 +1592,7 @@ mod tests {

let contract_id = env.register_contract(None, GovernanceContract);
let client = GovernanceContractClient::new(&env, &contract_id);
let deployer = Address::generate(&env);
let deployer = Address::generate(&env);
client.init(&deployer, &admins, &1, &recovery);

// Current threshold is 1, needs 2 signatures for change_threshold, but only 1 provided.
Expand All @@ -1592,7 +1615,7 @@ mod tests {

let contract_id = env.register_contract(None, GovernanceContract);
let client = GovernanceContractClient::new(&env, &contract_id);
let deployer = Address::generate(&env);
let deployer = Address::generate(&env);
client.init(&deployer, &admins, &1, &recovery);

// Threshold 3 > admins.len() 2 — must fail with InvalidThreshold, not auth.
Expand All @@ -1613,7 +1636,7 @@ mod tests {

let contract_id = env.register_contract(None, GovernanceContract);
let client = GovernanceContractClient::new(&env, &contract_id);
let deployer = Address::generate(&env);
let deployer = Address::generate(&env);
client.init(&deployer, &admins, &2, &recovery);

client.change_threshold(&admins, &0);
Expand Down Expand Up @@ -1668,6 +1691,29 @@ mod tests {
);
}

#[test]
#[should_panic(expected = "Error(Contract, #15)")]
fn initiate_recovery_rejects_overwrite_while_pending() {
let env = Env::default();
env.mock_all_auths();
let admin1 = Address::generate(&env);
let admin2 = Address::generate(&env);
let admins = vec![&env, admin1, admin2];
let recovery_address = Address::generate(&env);
let first_target = Address::generate(&env);
let second_target = Address::generate(&env);

let contract_id = env.register_contract(None, GovernanceContract);
let client = GovernanceContractClient::new(&env, &contract_id);
let deployer = Address::generate(&env);
client.init(&deployer, &admins, &2, &recovery_address);

client.initiate_recovery(&first_target);

// Second initiation must be rejected — a recovery is already pending.
client.initiate_recovery(&second_target);
}

// -----------------------------------------------------------------------
// InvalidWasmInterface: upgrade flow enforces supports_interface(1)
// -----------------------------------------------------------------------
Expand Down Expand Up @@ -1825,7 +1871,7 @@ mod tests {
let recovery_address = Address::generate(&env);
let contract_id = env.register_contract(None, GovernanceContract);
let client = GovernanceContractClient::new(&env, &contract_id);
let deployer = Address::generate(&env);
let deployer = Address::generate(&env);
client.init(&deployer, &admins, &1, &recovery_address);

client.pause(&admins);
Expand Down
Loading
Loading