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
8 changes: 7 additions & 1 deletion governance_contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -344,10 +347,12 @@ impl GovernanceContract {
/// # Errors
///
/// Panics with `GovernanceError::AlreadyInitialized` if already initialised.
pub fn init(env: Env, 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);
}
// 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,
Expand All @@ -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()
Expand Down
10 changes: 10 additions & 0 deletions settlement_contract/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Address>,
threshold: u32,
governance: Address,
Expand All @@ -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(
Expand All @@ -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()
Expand Down Expand Up @@ -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),
Expand Down
6 changes: 6 additions & 0 deletions settlement_contract/src/merchant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
20 changes: 11 additions & 9 deletions settlement_contract/src/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -267,7 +269,7 @@ impl SettlementContract {
merchant.clone(),
reference.clone(),
),
(),
record,
);

split
Expand Down
3 changes: 3 additions & 0 deletions settlement_contract/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Loading