Skip to main content

Fee Management in Program Escrow

Last updated: 16 August 2026

Archived — describes a contract that does not exist

This page is in the archive: it documents a Soroban contract surface with no implementation in any Grainlify repository, verified 16 August 2026. Kept as a design record, not as a description of working software.

Overview​

The Program Escrow contract supports configurable fee deduction on both lock and payout operations. Fees are collected to sustain the platform and are sent to a designated fee recipient address.

Fee Configuration​

Fee parameters are managed through the FeeConfig struct stored under the FEE_CONFIG storage key.

FieldTypeDescription
lock_fee_ratei128Percentage fee on lock operations (basis points, max MAX_FEE_RATE)
payout_fee_ratei128Percentage fee on each payout (basis points, max MAX_FEE_RATE)
lock_fixed_feei128Flat fee on lock (token base units), capped to lock amount
payout_fixed_feei128Flat fee per payout (token base units), capped to gross payout
fee_recipientAddressAddress that receives collected fees
fee_enabledboolGlobal on/off switch for fee deduction
fee_waiversu32Bitmask for per-payout-type waivers (see fee-arithmetic.md)

Maximum Fee Rate Cap​

Constant​

MAX_FEE_RATE = 1000 (defined in lib.rs:197)

This corresponds to 10% in basis points (1000 bps). No lock_fee_rate or payout_fee_rate may exceed this value.

Derivation​

Soroban contracts operate within a per-transaction CPU instruction budget of 100 M instructions. Empirical benchmarking shows that fee-calculation overhead is negligible compared to token transfers and storage writes, so the cap is set conservatively at 10 % to prevent economic attacks while remaining flexible for most use cases.

Enforcement​

The cap is enforced in update_fee_config, the only public entry point that modifies FeeConfig after initialization:

if r > MAX_FEE_RATE {
panic_with_error!(&env, ContractError::InvalidFeeRate);
}

Audit note: Initialization paths (init_program, init_program_with_dependencies, batch_init_programs) set both rates to 0, which is below MAX_FEE_RATE by construction and requires no guard.

Updating Fees​

Call update_fee_config with Option<i128> parameters — a None leaves the current value unchanged:

client.update_fee_config(
&Some(500), // lock_fee_rate: 5%
&None, // payout_fee_rate: unchanged
&None, // lock_fixed_fee: unchanged
&None, // payout_fixed_fee: unchanged
&None, // fee_recipient: unchanged
&Some(true), // fee_enabled: true
);

Security Properties​

  1. Admin-only: Only the contract admin can call update_fee_config.
  2. Atomic update: All changes are written in a single storage set call — partial failure is impossible.
  3. Range validation: lock_fee_rate and payout_fee_rate are validated against MAX_FEE_RATE (panics with ContractError::InvalidFeeRate on violation).
  4. Non-negative fixed fees: lock_fixed_fee and payout_fixed_fee must be non-negative.
  5. Preservation: Fields set to None are preserved unchanged.

Fee Calculation​

See fee-arithmetic.md for the exact rounding policy and implementation details.

Testing​

Comprehensive boundary-value tests are located in test_payout_splits.rs::fee_enforcement. The test matrix covers:

ScenarioExpected Outcome
rate = MAX_FEE_RATE + 1Rejected with ContractError::InvalidFeeRate
rate = MAX_FEE_RATEAccepted
rate = 0Accepted
rate < 0Rejected
Both rates at MAX_FEE_RATEAccepted
One rate valid, other invalidRejected
Fixed fee negativeRejected
Partial update preserves other fieldsPreserved