Skip to content
Open
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: 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"
);
}
}
3 changes: 2 additions & 1 deletion 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 Down
67 changes: 41 additions & 26 deletions governance_contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,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 +398,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 @@ -512,7 +514,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 +710,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 +914,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 +933,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 +1021,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 +1034,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 +1048,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, &soroban_sdk::vec![&env, admin], &0, &recovery);
}

#[test]
Expand Down Expand Up @@ -1175,7 +1184,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 +1198,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 +1445,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 +1474,13 @@ 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,
&soroban_sdk::vec![&env, admin.clone()],
&1,
&recovery_address,
);
assert!(client.is_initialized());
}

Expand All @@ -1480,7 +1495,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 +1560,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 +1584,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 +1607,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 +1628,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 @@ -1825,7 +1840,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
16 changes: 8 additions & 8 deletions governance_contract/src/real_auth_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ fn fee_anchor_and_system_param_writes_require_real_authorization() {
let key = Symbol::new(&env, "real_auth");
env.mock_auths(&[]);

assert!(client.try_set_fee_config(&admins, &valid_fee_config()).is_err());
assert!(client
.try_set_fee_config(&admins, &valid_fee_config())
.is_err());
assert!(client.try_upsert_anchor(&admins, &asset, &anchor).is_err());
assert!(client.try_update_system_param(&admins, &key, &1).is_err());
}
Expand All @@ -40,11 +42,7 @@ fn admin_transfer_and_threshold_change_require_real_authorization() {
env.mock_auths(&[]);

assert!(client
.try_transfer_admin(
&admins,
&soroban_sdk::vec![&env, replacement_admin],
&1,
)
.try_transfer_admin(&admins, &soroban_sdk::vec![&env, replacement_admin], &1,)
.is_err());

let env = Env::default();
Expand All @@ -56,7 +54,8 @@ fn admin_transfer_and_threshold_change_require_real_authorization() {
let recovery = Address::generate(&env);
let contract_id = env.register_contract(None, GovernanceContract);
let client = GovernanceContractClient::new(&env, &contract_id);
client.init(&admins, &1, &recovery);
let deployer = Address::generate(&env);
client.init(&deployer, &admins, &1, &recovery);
env.mock_auths(&[]);

assert!(client.try_change_threshold(&admins, &2).is_err());
Expand Down Expand Up @@ -98,7 +97,8 @@ fn initialization_and_upgrade_require_real_authorization() {
let client = GovernanceContractClient::new(&env, &contract_id);
let admins = soroban_sdk::vec![&env, admin];
env.mock_auths(&[]);
assert!(client.try_init(&admins, &1, &recovery).is_err());
let deployer = Address::generate(&env);
assert!(client.try_init(&deployer, &admins, &1, &recovery).is_err());

let (env, client, admins) = super::setup();
let wasm_hash = env
Expand Down
33 changes: 24 additions & 9 deletions settlement_contract/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ use crate::errors::SettlementError;
use crate::storage::{
assert_not_paused, is_merchant_registered_and_bump_ttl, read_admin, read_admins,
read_fallback_rule, read_governance, read_optional_primary_admin, read_pending_recovery,
read_recovery_address, read_rule_or_default, read_threshold,
validate_admins_and_threshold, validate_governance, validate_nonzero_address,
verify_admin_auth, write_admins,
read_recovery_address, read_rule_or_default, read_threshold, validate_admins_and_threshold,
validate_governance, validate_nonzero_address, verify_admin_auth, write_admins,
};
use crate::types::{DataKey, Operation, ScheduledOp, SettlementRule};
use crate::{
Expand Down Expand Up @@ -389,14 +388,20 @@ impl SettlementContract {
env.storage().persistent().remove(&key);

match operation {
Operation::UpdateGovernance(new_gov) => Self::_update_governance(&env, &executor, new_gov),
Operation::UpdateGovernance(new_gov) => {
Self::_update_governance(&env, &executor, new_gov)
}
Operation::CancelRecovery => Self::_cancel_recovery(&env, &executor),
Operation::TransferAdmin(new_admins, new_threshold) => {
Self::_transfer_admin(&env, &executor, new_admins, new_threshold)
}
Operation::Upgrade(wasm_hash) => Self::_upgrade(&env, &executor, wasm_hash),
Operation::RegisterMerchant(merchant) => Self::_register_merchant(&env, &executor, merchant),
Operation::UnregisterMerchant(merchant) => Self::_unregister_merchant(&env, &executor, merchant),
Operation::RegisterMerchant(merchant) => {
Self::_register_merchant(&env, &executor, merchant)
}
Operation::UnregisterMerchant(merchant) => {
Self::_unregister_merchant(&env, &executor, merchant)
}
Operation::SetSettlementRule(merchant, rule) => {
Self::_set_settlement_rule(&env, &executor, merchant, rule)
}
Expand Down Expand Up @@ -474,7 +479,12 @@ impl SettlementContract {
events::emit_recovery_cancelled(env, executor);
}

fn _transfer_admin(env: &Env, executor: &Address, new_admins: Vec<Address>, new_threshold: u32) {
fn _transfer_admin(
env: &Env,
_executor: &Address,
new_admins: Vec<Address>,
new_threshold: u32,
) {
let old_admin = read_admin(env);
validate_admins_and_threshold(env, &new_admins, new_threshold);
// Enforce admin/merchant exclusivity in both directions (issue #692).
Expand Down Expand Up @@ -522,7 +532,7 @@ impl SettlementContract {
SettlementError::EmptyAddress,
SettlementError::ZeroAddress,
);

// Prevent an admin from being registered as a merchant
let admins = read_admins(env);
for i in 0..admins.len() {
Expand Down Expand Up @@ -602,7 +612,12 @@ impl SettlementContract {
);
}

fn _set_settlement_rule(env: &Env, executor: &Address, merchant: Address, rule: SettlementRule) {
fn _set_settlement_rule(
env: &Env,
executor: &Address,
merchant: Address,
rule: SettlementRule,
) {
assert_not_paused(env);

if !is_merchant_registered_and_bump_ttl(env, merchant.clone()) {
Expand Down
Loading