@@ -85,7 +85,9 @@ pub enum Error {
8585 DecryptionFailed = 55 ,
8686 InsufficientDelegation = 56 ,
8787 InvalidCommissionRate = 57 ,
88- InvalidVdfProof = 58 ,
88+ VolatilityExceeded = 62 ,
89+ VolatilityCircuitBreakerTripped = 63 ,
90+ VolatilityTimelockActive = 64 ,
8991}
9092
9193#[ contracttype]
@@ -800,23 +802,6 @@ pub struct ZkRangeProof {
800802 pub created_at : u64 ,
801803}
802804
803- #[ contracttype]
804- #[ derive( Clone , Debug ) ]
805- /// A VDF proof for a Wesolowski-style time-lock delay (Issue #837).
806- /// SCAFFOLD: verify_vdf_proof below is a stub (always returns false) until
807- /// the group arithmetic (RSA/class-group modexp + Fiat-Shamir challenge,
808- /// wasm32-compatible bignum) is implemented. Do not treat as a working
809- /// security gate yet.
810- pub struct VdfProof {
811- pub task_id : u64 ,
812- pub input : Bytes ,
813- pub output : Bytes ,
814- pub proof : Bytes ,
815- pub difficulty : u64 ,
816- pub is_verified : bool ,
817- pub created_at : u64 ,
818- }
819-
820805#[ contracttype]
821806#[ derive( Clone , Debug ) ]
822807pub struct DynamicBountyConfig {
@@ -965,6 +950,10 @@ pub enum DataKey {
965950 ZkRangeProofCounter ,
966951 TaskDynamicBounty ( u64 ) ,
967952 FlashSwapRecord ( u64 ) ,
953+ MaxVolatilityBps ,
954+ LastOraclePrice ,
955+ VolatilityCircuitBreakerTripped ,
956+ VolatilityUnpauseTimelock ,
968957 FlashSwapCounter ,
969958 KeeperRandomSeed ,
970959 InsuranceVaultBalance ,
@@ -2131,6 +2120,75 @@ impl SoroTaskContract {
21312120 exit_security_guard ( & env) ;
21322121 }
21332122
2123+ /// Sets the maximum allowable single-update oracle price volatility threshold in basis points (bps).
2124+ pub fn set_max_volatility_bps ( env : Env , admin : Address , max_bps : u32 ) {
2125+ admin. require_auth ( ) ;
2126+ env. storage ( ) . instance ( ) . set ( & DataKey :: MaxVolatilityBps , & max_bps) ;
2127+ }
2128+
2129+ /// Returns the maximum volatility threshold in bps (default: 500 = 5%).
2130+ pub fn get_max_volatility_bps ( env : & Env ) -> u32 {
2131+ env. storage ( ) . instance ( ) . get ( & DataKey :: MaxVolatilityBps ) . unwrap_or ( 500 )
2132+ }
2133+
2134+ /// Checks if the volatility circuit breaker is currently tripped.
2135+ pub fn is_volatility_circuit_tripped ( env : & Env ) -> bool {
2136+ env. storage ( ) . instance ( ) . get ( & DataKey :: VolatilityCircuitBreakerTripped ) . unwrap_or ( false )
2137+ }
2138+
2139+ /// Updates oracle price, checking single-update price delta against max_volatility_bps.
2140+ /// Trips circuit breaker and returns Ok(true) if volatility exceeds threshold, Ok(false) if updated normally.
2141+ pub fn check_oracle_volatility ( env : Env , new_price : i128 ) -> Result < bool , Error > {
2142+ enter_security_guard ( & env) ;
2143+ if Self :: is_volatility_circuit_tripped ( & env) {
2144+ exit_security_guard ( & env) ;
2145+ return Err ( Error :: VolatilityCircuitBreakerTripped ) ;
2146+ }
2147+
2148+ let max_volatility = Self :: get_max_volatility_bps ( & env) ;
2149+ if let Some ( last_price) = env. storage ( ) . instance ( ) . get :: < DataKey , i128 > ( & DataKey :: LastOraclePrice ) {
2150+ if last_price > 0 {
2151+ let diff = if new_price > last_price {
2152+ new_price - last_price
2153+ } else {
2154+ last_price - new_price
2155+ } ;
2156+ let volatility_bps = ( ( diff as u128 * 10_000 ) / last_price as u128 ) as u32 ;
2157+ if volatility_bps > max_volatility {
2158+ env. storage ( ) . instance ( ) . set ( & DataKey :: VolatilityCircuitBreakerTripped , & true ) ;
2159+ let current_time = env. ledger ( ) . timestamp ( ) ;
2160+ env. storage ( ) . instance ( ) . set ( & DataKey :: VolatilityUnpauseTimelock , & ( current_time + 3_600 ) ) ;
2161+ crate :: events:: EventLogger :: log_oracle_volatility_breach (
2162+ & env,
2163+ last_price,
2164+ new_price,
2165+ volatility_bps,
2166+ max_volatility,
2167+ ) ;
2168+ exit_security_guard ( & env) ;
2169+ return Ok ( true ) ;
2170+ }
2171+ }
2172+ }
2173+
2174+ env. storage ( ) . instance ( ) . set ( & DataKey :: LastOraclePrice , & new_price) ;
2175+ exit_security_guard ( & env) ;
2176+ Ok ( false )
2177+ }
2178+
2179+ /// Unpauses the volatility circuit breaker after timelock expiration.
2180+ pub fn unpause_volatility_breaker ( env : Env , admin : Address ) -> Result < ( ) , Error > {
2181+ admin. require_auth ( ) ;
2182+ if let Some ( timelock) = env. storage ( ) . instance ( ) . get :: < DataKey , u64 > ( & DataKey :: VolatilityUnpauseTimelock ) {
2183+ if env. ledger ( ) . timestamp ( ) < timelock {
2184+ return Err ( Error :: VolatilityTimelockActive ) ;
2185+ }
2186+ }
2187+ env. storage ( ) . instance ( ) . set ( & DataKey :: VolatilityCircuitBreakerTripped , & false ) ;
2188+ crate :: events:: EventLogger :: log_volatility_circuit_breaker_unpaused ( & env, admin) ;
2189+ Ok ( ( ) )
2190+ }
2191+
21342192 /// Requests randomness from the VRF oracle for a task.
21352193 /// The oracle will call back with the random number when ready.
21362194 pub fn request_vrf_randomness (
0 commit comments