Fee Management in Program Escrow
Last updated: 16 August 2026
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.
| Field | Type | Description |
|---|---|---|
lock_fee_rate | i128 | Percentage fee on lock operations (basis points, max MAX_FEE_RATE) |
payout_fee_rate | i128 | Percentage fee on each payout (basis points, max MAX_FEE_RATE) |
lock_fixed_fee | i128 | Flat fee on lock (token base units), capped to lock amount |
payout_fixed_fee | i128 | Flat fee per payout (token base units), capped to gross payout |
fee_recipient | Address | Address that receives collected fees |
fee_enabled | bool | Global on/off switch for fee deduction |
fee_waivers | u32 | Bitmask 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 to0, which is belowMAX_FEE_RATEby 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
- Admin-only: Only the contract admin can call
update_fee_config. - Atomic update: All changes are written in a single storage
setcall — partial failure is impossible. - Range validation:
lock_fee_rateandpayout_fee_rateare validated againstMAX_FEE_RATE(panics withContractError::InvalidFeeRateon violation). - Non-negative fixed fees:
lock_fixed_feeandpayout_fixed_feemust be non-negative. - Preservation: Fields set to
Noneare 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:
| Scenario | Expected Outcome |
|---|---|
rate = MAX_FEE_RATE + 1 | Rejected with ContractError::InvalidFeeRate |
rate = MAX_FEE_RATE | Accepted |
rate = 0 | Accepted |
rate < 0 | Rejected |
Both rates at MAX_FEE_RATE | Accepted |
| One rate valid, other invalid | Rejected |
| Fixed fee negative | Rejected |
| Partial update preserves other fields | Preserved |