@@ -28,23 +28,49 @@ pub enum ReleaseCondition {
2828 Timelock ( u64 ) ,
2929}
3030
31- // ── Fee constants (#344) ─────────────────────────────────────────────────────
31+ // ---- Fee constants (#344/#722) --------------------------------------------------
3232
33- /// Platform fee in basis points (2.5 %).
34- pub const PLATFORM_FEE_BPS : i128 = 250 ;
33+ /// Default platform fee in basis points (2.5 %).
34+ pub const DEFAULT_PLATFORM_FEE_BPS : i128 = 250 ;
3535
36- /// Maximum platform fee in token units (500 USDC-equivalent).
37- pub const PLATFORM_FEE_CAP : i128 = 500 ;
36+ /// Default maximum platform fee in token units (500 USDC-equivalent).
37+ pub const DEFAULT_PLATFORM_FEE_CAP : i128 = 500 ;
3838
3939/// Dispute timeout: 30 days in ledger seconds (#731).
4040/// After this window anyone can call `resolve_expired_dispute` for a 50/50 split.
4141pub const DISPUTE_TIMEOUT_SECS : u64 = 30 * 24 * 3600 ;
4242
43- /// Compute the platform fee for a given gross amount.
44- /// Fee = min(amount * 250 / 10_000, 500)
43+ /// Governance-configurable fee configuration stored on-chain (#722).
44+ #[ contracttype]
45+ #[ derive( Clone , Debug ) ]
46+ pub struct FeeConfig {
47+ pub fee_bps : i128 ,
48+ pub fee_cap : i128 ,
49+ }
50+
51+ /// Read the current fee config from storage, falling back to defaults.
52+ pub fn get_fee_config ( env : & Env ) -> FeeConfig {
53+ env. storage ( )
54+ . persistent ( )
55+ . get :: < DataKey , FeeConfig > ( & DataKey :: FeeConfig )
56+ . unwrap_or ( FeeConfig {
57+ fee_bps : DEFAULT_PLATFORM_FEE_BPS ,
58+ fee_cap : DEFAULT_PLATFORM_FEE_CAP ,
59+ } )
60+ }
61+
62+ /// Compute the platform fee for a given gross amount using the current config.
63+ /// Fee = min(amount * fee_bps / 10_000, fee_cap)
4564pub fn platform_fee ( amount : i128 ) -> i128 {
46- let raw = amount * PLATFORM_FEE_BPS / 10_000 ;
47- if raw > PLATFORM_FEE_CAP { PLATFORM_FEE_CAP } else { raw }
65+ // When called from storage-aware code use platform_fee_with_config instead.
66+ let raw = amount * DEFAULT_PLATFORM_FEE_BPS / 10_000 ;
67+ if raw > DEFAULT_PLATFORM_FEE_CAP { DEFAULT_PLATFORM_FEE_CAP } else { raw }
68+ }
69+
70+ /// Compute platform fee using a specific FeeConfig (snapshot on deposit).
71+ pub fn platform_fee_with_config ( amount : i128 , config : & FeeConfig ) -> i128 {
72+ let raw = amount * config. fee_bps / 10_000 ;
73+ if raw > config. fee_cap { config. fee_cap } else { raw }
4874}
4975
5076/// Escrow Account
@@ -87,6 +113,9 @@ pub enum DataKey {
87113 MilestoneReleasedAmount ( u64 ) ,
88114 DisputeExpiry ( u64 ) ,
89115 DisputeSplit ,
116+ FeeConfig ,
117+ OracleStalenessSecs ,
118+ OracleAddress ,
90119}
91120
92121// ── Issue #631: Yield Farming for Unwithdrawn Escrow Funds ───────────────────
@@ -146,7 +175,8 @@ impl EscrowContract {
146175 let mut counter: u64 = env. storage ( ) . persistent ( ) . get :: < Symbol , u64 > ( & counter_key) . unwrap_or ( 0 ) ;
147176 counter += 1 ;
148177
149- let fee = platform_fee ( amount) ;
178+ let fee_cfg = get_fee_config ( & env) ;
179+ let fee = platform_fee_with_config ( amount, & fee_cfg) ;
150180 let net_amount = amount - fee;
151181
152182 // Collect platform fee to admin if set, otherwise hold in contract
@@ -229,6 +259,26 @@ impl EscrowContract {
229259 "Release condition not met"
230260 ) ;
231261
262+
263+ // Issue #725: Oracle price freshness check before release
264+ if let Some ( oracle_addr) = env. storage ( ) . persistent ( ) . get :: < DataKey , Address > ( & DataKey :: OracleAddress ) {
265+ let max_staleness = Self :: get_oracle_staleness_secs ( & env) ;
266+ let price_data: soroban_sdk:: Vec < soroban_sdk:: Val > = env
267+ . invoke_contract (
268+ & oracle_addr,
269+ & Symbol :: new ( & env, "get_price" ) ,
270+ soroban_sdk:: Vec :: new ( & env) ,
271+ ) ;
272+ let timestamp: u64 = price_data
273+ . get ( 1 )
274+ . expect ( "Oracle returned invalid price data" )
275+ . try_into ( )
276+ . unwrap_or ( 0 ) ;
277+ let age_secs = env. ledger ( ) . timestamp ( ) . saturating_sub ( timestamp) ;
278+ if age_secs > max_staleness {
279+ panic ! ( "Oracle price feed is stale" ) ;
280+ }
281+ }
232282 TokenClient :: new ( & env, & escrow. token )
233283 . transfer ( & env. current_contract_address ( ) , & escrow. payee , & escrow. amount ) ;
234284
@@ -962,6 +1012,67 @@ impl EscrowContract {
9621012 }
9631013}
9641014
1015+
1016+ // ---- Issue #722: Governance-controlled platform fee update ----
1017+
1018+ /// Update the platform fee configuration. Only the governance multisig
1019+ /// (platform admin) may call this.
1020+ /// Emits a fee_updated event on successful change.
1021+ pub fn update_fee ( env : Env , admin : Address , new_bps : i128 , new_cap : i128 ) {
1022+ admin. require_auth ( ) ;
1023+ let stored_admin: Address = env
1024+ . storage ( )
1025+ . persistent ( )
1026+ . get :: < Symbol , Address > ( & Symbol :: new ( & env, "platform_admin" ) )
1027+ . expect ( "Platform admin not set" ) ;
1028+ assert_eq ! ( admin, stored_admin, "Only governance multisig can update fee" ) ;
1029+ assert ! ( new_bps >= 0 , "Fee BPS must be non-negative" ) ;
1030+ assert ! ( new_bps <= 10_000 , "Fee BPS must not exceed 10_000" ) ;
1031+ assert ! ( new_cap >= 0 , "Fee cap must be non-negative" ) ;
1032+ env. storage ( ) . persistent ( ) . set ( & DataKey :: FeeConfig , & FeeConfig {
1033+ fee_bps : new_bps,
1034+ fee_cap : new_cap,
1035+ } ) ;
1036+ env. events ( ) . publish (
1037+ ( symbol_short ! ( "escrow" ) , symbol_short ! ( "fee_upd" ) ) ,
1038+ ( new_bps, new_cap) ,
1039+ ) ;
1040+ }
1041+
1042+ // ---- Issue #725: Oracle price freshness check ----
1043+
1044+ /// Return the oracle staleness threshold in seconds (governance-configurable).
1045+ /// Defaults to 3600 (1 hour).
1046+ pub fn get_oracle_staleness_secs ( env : & Env ) -> u64 {
1047+ env. storage ( )
1048+ . persistent ( )
1049+ . get :: < DataKey , u64 > ( & DataKey :: OracleStalenessSecs )
1050+ . unwrap_or ( 3600 )
1051+ }
1052+
1053+ /// Set the oracle staleness threshold. Only governance multisig may call this.
1054+ pub fn set_oracle_staleness_secs ( env : Env , admin : Address , staleness_secs : u64 ) {
1055+ admin. require_auth ( ) ;
1056+ let stored_admin: Address = env
1057+ . storage ( )
1058+ . persistent ( )
1059+ . get :: < Symbol , Address > ( & Symbol :: new ( & env, "platform_admin" ) )
1060+ . expect ( "Platform admin not set" ) ;
1061+ assert_eq ! ( admin, stored_admin, "Only governance multisig can set staleness" ) ;
1062+ env. storage ( ) . persistent ( ) . set ( & DataKey :: OracleStalenessSecs , & staleness_secs) ;
1063+ }
1064+
1065+ /// Set the oracle contract address. Only governance multisig may call this.
1066+ pub fn set_oracle_address ( env : Env , admin : Address , oracle : Address ) {
1067+ admin. require_auth ( ) ;
1068+ let stored_admin: Address = env
1069+ . storage ( )
1070+ . persistent ( )
1071+ . get :: < Symbol , Address > ( & Symbol :: new ( & env, "platform_admin" ) )
1072+ . expect ( "Platform admin not set" ) ;
1073+ assert_eq ! ( admin, stored_admin, "Only governance multisig can set oracle" ) ;
1074+ env. storage ( ) . persistent ( ) . set ( & DataKey :: OracleAddress , & oracle) ;
1075+ }
9651076#[ cfg( test) ]
9661077mod tests {
9671078 use super :: * ;
0 commit comments