diff --git a/governance_contract/src/lib.rs b/governance_contract/src/lib.rs index 82f89b76..d66f8e75 100644 --- a/governance_contract/src/lib.rs +++ b/governance_contract/src/lib.rs @@ -247,6 +247,9 @@ enum DataKey { /// Instance-storage schema version (u32) written at `init`. Baseline for /// the first storage migration (issue #507). SchemaVersion, + /// Instance — stored at `init` to gate initialization to the deployer + /// and prevent front-running (issue #684). + Deployer, } /// The schema version this build expects. `init` writes this value and @@ -344,10 +347,12 @@ impl GovernanceContract { /// # Errors /// /// Panics with `GovernanceError::AlreadyInitialized` if already initialised. - pub fn init(env: Env, admins: Vec
, threshold: u32, recovery_address: Address) { + pub fn init(env: Env, deployer: Address, admins: Vec, threshold: u32, recovery_address: Address) { if env.storage().instance().has(&DataKey::Admin) { panic_with_error!(&env, GovernanceError::AlreadyInitialized); } + // Gate initialization to the deployer to prevent front-running (issue #684). + deployer.require_auth(); validate_admins_and_threshold(&env, &admins, threshold); assert_not_zero( &env, @@ -357,6 +362,7 @@ impl GovernanceContract { for i in 0..threshold { admins.get(i).unwrap().require_auth(); } + env.storage().instance().set(&DataKey::Deployer, &deployer); env.storage().instance().set(&DataKey::Admin, &admins); env.storage() .instance() diff --git a/settlement_contract/src/admin.rs b/settlement_contract/src/admin.rs index ccaa8938..a5d92d2f 100644 --- a/settlement_contract/src/admin.rs +++ b/settlement_contract/src/admin.rs @@ -35,6 +35,7 @@ impl SettlementContract { /// * [`AlreadyInitialized`](SettlementError::AlreadyInitialized) — if the contract has already been initialized. pub fn init( env: Env, + deployer: Address, admins: Vec, threshold: u32, governance: Address, @@ -43,6 +44,8 @@ impl SettlementContract { if env.storage().instance().has(&DataKey::Admin) { panic_with_error!(&env, SettlementError::AlreadyInitialized); } + // Gate initialization to the deployer to prevent front-running (issue #684). + deployer.require_auth(); validate_admins_and_threshold(&env, &admins, threshold); validate_governance(&env, &governance); validate_nonzero_address( @@ -54,6 +57,7 @@ impl SettlementContract { for i in 0..threshold { admins.get(i).unwrap().require_auth(); } + env.storage().instance().set(&DataKey::Deployer, &deployer); write_admins(&env, &admins, threshold); env.storage() .instance() @@ -466,6 +470,12 @@ impl SettlementContract { env.storage() .persistent() .extend_ttl(&key, MERCHANT_TTL_THRESHOLD, MERCHANT_TTL_BUMP); + + // Remove any ArchivedMerchant tombstone from a prior registration so + // the re-registered merchant can read new payment records (issue #685). + let archived_key = DataKey::ArchivedMerchant(merchant.clone()); + env.storage().persistent().remove(&archived_key); + env.events().publish( ( Symbol::new(env, events::MERCHANT_REGISTERED_EVENT), diff --git a/settlement_contract/src/merchant.rs b/settlement_contract/src/merchant.rs index 7225936f..5e37fc44 100644 --- a/settlement_contract/src/merchant.rs +++ b/settlement_contract/src/merchant.rs @@ -55,6 +55,12 @@ impl SettlementContract { env.storage() .persistent() .extend_ttl(&key, MERCHANT_TTL_THRESHOLD, MERCHANT_TTL_BUMP); + + // Remove any ArchivedMerchant tombstone from a prior registration so + // the re-registered merchant can read new payment records (issue #685). + let archived_key = DataKey::ArchivedMerchant(merchant.clone()); + env.storage().persistent().remove(&archived_key); + env.events().publish( ( Symbol::new(&env, events::MERCHANT_REGISTERED_EVENT), diff --git a/settlement_contract/src/payments.rs b/settlement_contract/src/payments.rs index abb3c414..a0c2755a 100644 --- a/settlement_contract/src/payments.rs +++ b/settlement_contract/src/payments.rs @@ -46,14 +46,16 @@ fn calculate_split(env: &Env, amount: i128, rule: &SettlementRule) -> FeeSplit { // To prevent fee under-collection, ceiling division is simulated by adding `BPS_DENOMINATOR - 1` to the numerator. // Edge case: For small amounts, ceil rounding can force fees to 1 unit even when the basis points represent a tiny fraction. let platform_fee_amount = platform_bps.calculate_fee_ceil(amount); - let network_fee_amount = network_bps.calculate_fee_ceil(amount); - - // The merchant amount is calculated as the subtraction remainder of the gross amount minus all rounded-up fees. - // This ensures the sum of the split amounts (platform fee + network fee + merchant share) always equals the gross amount, - // except when the remainder is negative. For very small gross amounts with high/extreme fee percentages, - // the sum of rounded-up fees can exceed the gross amount. We explicitly clamp the merchant amount to 0 in this case. - let remainder = amount - platform_fee_amount - network_fee_amount; - let merchant_amount = remainder.max(0); + let mut network_fee_amount = network_bps.calculate_fee_ceil(amount); + + // Ceil-rounded fees can sum to more than the gross for tiny amounts with + // high fee configs. Clamp the network leg so total fees never exceed the + // gross, keeping the accounting equation balanced (issue #683). + if platform_fee_amount + network_fee_amount > amount { + network_fee_amount = amount - platform_fee_amount; + } + + let merchant_amount = (amount - platform_fee_amount - network_fee_amount).max(0); FeeSplit { gross_amount: amount, platform_fee_amount, @@ -267,7 +269,7 @@ impl SettlementContract { merchant.clone(), reference.clone(), ), - (), + record, ); split diff --git a/settlement_contract/src/types.rs b/settlement_contract/src/types.rs index 567df6ce..e4a2aef5 100644 --- a/settlement_contract/src/types.rs +++ b/settlement_contract/src/types.rs @@ -139,4 +139,7 @@ pub(crate) enum DataKey { Payment(Address, BytesN<32>), /// Storage key for a scheduled operation. ScheduledOperation(BytesN<32>), + /// Instance — stored at `init` to gate initialization to the deployer + /// and prevent front-running (issue #684). + Deployer, }