@@ -237,6 +237,24 @@ pub struct AdminTransferProposal {
237237 pub is_active : bool ,
238238}
239239
240+ #[ contracttype]
241+ #[ derive( Clone ) ]
242+ pub enum EmergencyAction {
243+ SetFlowRate ( i128 ) ,
244+ PauseCycle ,
245+ }
246+
247+ #[ contracttype]
248+ #[ derive( Clone ) ]
249+ pub struct EmergencyProposal {
250+ pub proposal_id : u64 ,
251+ pub meter_id : u64 ,
252+ pub action : EmergencyAction ,
253+ pub proposed_by : Address ,
254+ pub proposed_at : u64 ,
255+ pub executed : bool ,
256+ }
257+
240258pub fn set_sorosusu_contract ( env : Env , addr : Address ) {
241259 env. storage ( )
242260 . instance ( )
@@ -502,6 +520,7 @@ const LEDGER_LIFETIME_EXTENSION: u32 = 1_000_000; // Extend by 1M ledgers
502520
503521// Task #4: Wasm Hash Rotation Constants
504522const UPGRADE_VETO_PERIOD_SECONDS : u64 = 7 * DAY_IN_SECONDS ; // 7 days veto period
523+ const ORACLE_HEARTBEAT_TIMEOUT_SECONDS : u64 = 72 * HOUR_IN_SECONDS ;
505524
506525/// Round XLM amount to nearest minimum increment (0.0000001 XLM)
507526/// This prevents value loss over time due to truncation
@@ -614,6 +633,67 @@ fn get_oracle_or_panic(env: &Env) -> Address {
614633 }
615634}
616635
636+ fn is_trust_mode_active ( env : & Env ) -> bool {
637+ let now = env. ledger ( ) . timestamp ( ) ;
638+ match env
639+ . storage ( )
640+ . instance ( )
641+ . get :: < DataKey , Address > ( & DataKey :: Oracle )
642+ {
643+ Some ( oracle_address) => {
644+ let oracle_client = PriceOracleClient :: new ( env, & oracle_address) ;
645+ let price_data = oracle_client. get_price ( ) ;
646+ now. saturating_sub ( price_data. last_updated ) > ORACLE_HEARTBEAT_TIMEOUT_SECONDS
647+ }
648+ None => true ,
649+ }
650+ }
651+
652+ fn require_trust_mode ( env : & Env ) {
653+ if !is_trust_mode_active ( env) {
654+ panic_with_error ! ( env, ContractError :: OracleHeartbeatHealthy ) ;
655+ }
656+ }
657+
658+ fn active_member_count ( env : & Env ) -> u32 {
659+ env. storage ( )
660+ . instance ( )
661+ . get :: < DataKey , u32 > ( & DataKey :: ActiveUsers )
662+ . unwrap_or ( 0 )
663+ }
664+
665+ fn require_emergency_member ( env : & Env , member : & Address ) {
666+ member. require_auth ( ) ;
667+ let is_member = env
668+ . storage ( )
669+ . instance ( )
670+ . get :: < DataKey , bool > ( & DataKey :: ActiveUserMember ( member. clone ( ) ) )
671+ . unwrap_or ( false ) ;
672+ if !is_member {
673+ panic_with_error ! ( env, ContractError :: NotEmergencyMember ) ;
674+ }
675+ }
676+
677+ fn next_emergency_proposal_id ( env : & Env ) -> u64 {
678+ let current = env
679+ . storage ( )
680+ . instance ( )
681+ . get :: < DataKey , u64 > ( & DataKey :: EmergencyProposalSeq )
682+ . unwrap_or ( 0 ) ;
683+ let next = current. saturating_add ( 1 ) ;
684+ env. storage ( )
685+ . instance ( )
686+ . set ( & DataKey :: EmergencyProposalSeq , & next) ;
687+ next
688+ }
689+
690+ fn get_emergency_proposal_or_panic ( env : & Env , proposal_id : u64 ) -> EmergencyProposal {
691+ env. storage ( )
692+ . instance ( )
693+ . get :: < DataKey , EmergencyProposal > ( & DataKey :: EmergencyProposal ( proposal_id) )
694+ . unwrap_or_else ( || panic_with_error ! ( env, ContractError :: EmergencyProposalNotFound ) )
695+ }
696+
617697fn convert_xlm_to_usd_if_needed (
618698 env : & Env ,
619699 amount : i128 ,
@@ -2066,6 +2146,193 @@ impl UtilityContract {
20662146 . publish ( ( symbol_short ! ( "Paused" ) , meter_id) , paused) ;
20672147 }
20682148
2149+ pub fn is_trust_mode ( env : Env ) -> bool {
2150+ is_trust_mode_active ( & env)
2151+ }
2152+
2153+ pub fn propose_emergency_flow_rate (
2154+ env : Env ,
2155+ member : Address ,
2156+ meter_id : u64 ,
2157+ max_rate_per_hour : i128 ,
2158+ ) -> u64 {
2159+ require_trust_mode ( & env) ;
2160+ require_emergency_member ( & env, & member) ;
2161+ if active_member_count ( & env) == 0 {
2162+ panic_with_error ! ( & env, ContractError :: EmergencyNoMembers ) ;
2163+ }
2164+ if max_rate_per_hour <= 0 {
2165+ panic_with_error ! ( & env, ContractError :: InvalidTokenAmount ) ;
2166+ }
2167+
2168+ // Ensure the target meter exists before proposal creation.
2169+ let _meter = get_meter_or_panic ( & env, meter_id) ;
2170+ let proposal_id = next_emergency_proposal_id ( & env) ;
2171+ let proposal = EmergencyProposal {
2172+ proposal_id,
2173+ meter_id,
2174+ action : EmergencyAction :: SetFlowRate ( max_rate_per_hour) ,
2175+ proposed_by : member. clone ( ) ,
2176+ proposed_at : env. ledger ( ) . timestamp ( ) ,
2177+ executed : false ,
2178+ } ;
2179+
2180+ env. storage ( )
2181+ . instance ( )
2182+ . set ( & DataKey :: EmergencyProposal ( proposal_id) , & proposal) ;
2183+ env. storage ( ) . instance ( ) . set (
2184+ & DataKey :: EmergencyProposalApproval ( proposal_id, member) ,
2185+ & true ,
2186+ ) ;
2187+ env. storage ( )
2188+ . instance ( )
2189+ . set ( & DataKey :: EmergencyProposalApprovalCount ( proposal_id) , & 1u32 ) ;
2190+
2191+ env. events ( ) . publish (
2192+ ( symbol_short ! ( "EmgProp" ) , proposal_id) ,
2193+ ( meter_id, env. ledger ( ) . timestamp ( ) ) ,
2194+ ) ;
2195+
2196+ proposal_id
2197+ }
2198+
2199+ pub fn propose_emergency_pause ( env : Env , member : Address , meter_id : u64 ) -> u64 {
2200+ require_trust_mode ( & env) ;
2201+ require_emergency_member ( & env, & member) ;
2202+ if active_member_count ( & env) == 0 {
2203+ panic_with_error ! ( & env, ContractError :: EmergencyNoMembers ) ;
2204+ }
2205+
2206+ // Ensure the target meter exists before proposal creation.
2207+ let _meter = get_meter_or_panic ( & env, meter_id) ;
2208+ let proposal_id = next_emergency_proposal_id ( & env) ;
2209+ let proposal = EmergencyProposal {
2210+ proposal_id,
2211+ meter_id,
2212+ action : EmergencyAction :: PauseCycle ,
2213+ proposed_by : member. clone ( ) ,
2214+ proposed_at : env. ledger ( ) . timestamp ( ) ,
2215+ executed : false ,
2216+ } ;
2217+
2218+ env. storage ( )
2219+ . instance ( )
2220+ . set ( & DataKey :: EmergencyProposal ( proposal_id) , & proposal) ;
2221+ env. storage ( ) . instance ( ) . set (
2222+ & DataKey :: EmergencyProposalApproval ( proposal_id, member) ,
2223+ & true ,
2224+ ) ;
2225+ env. storage ( )
2226+ . instance ( )
2227+ . set ( & DataKey :: EmergencyProposalApprovalCount ( proposal_id) , & 1u32 ) ;
2228+
2229+ env. events ( ) . publish (
2230+ ( symbol_short ! ( "EmgProp" ) , proposal_id) ,
2231+ ( meter_id, env. ledger ( ) . timestamp ( ) ) ,
2232+ ) ;
2233+
2234+ proposal_id
2235+ }
2236+
2237+ pub fn approve_emergency_action ( env : Env , member : Address , proposal_id : u64 ) {
2238+ require_trust_mode ( & env) ;
2239+ require_emergency_member ( & env, & member) ;
2240+ if active_member_count ( & env) == 0 {
2241+ panic_with_error ! ( & env, ContractError :: EmergencyNoMembers ) ;
2242+ }
2243+
2244+ let proposal = get_emergency_proposal_or_panic ( & env, proposal_id) ;
2245+ if proposal. executed {
2246+ panic_with_error ! ( & env, ContractError :: EmergencyProposalExecuted ) ;
2247+ }
2248+
2249+ if env. storage ( ) . instance ( ) . has ( & DataKey :: EmergencyProposalApproval (
2250+ proposal_id,
2251+ member. clone ( ) ,
2252+ ) ) {
2253+ panic_with_error ! ( & env, ContractError :: AlreadyVoted ) ;
2254+ }
2255+
2256+ env. storage ( ) . instance ( ) . set (
2257+ & DataKey :: EmergencyProposalApproval ( proposal_id, member) ,
2258+ & true ,
2259+ ) ;
2260+
2261+ let approvals = env
2262+ . storage ( )
2263+ . instance ( )
2264+ . get :: < DataKey , u32 > ( & DataKey :: EmergencyProposalApprovalCount ( proposal_id) )
2265+ . unwrap_or ( 0 )
2266+ . saturating_add ( 1 ) ;
2267+ env. storage ( )
2268+ . instance ( )
2269+ . set ( & DataKey :: EmergencyProposalApprovalCount ( proposal_id) , & approvals) ;
2270+
2271+ env. events ( ) . publish (
2272+ ( symbol_short ! ( "EmgAppr" ) , proposal_id) ,
2273+ approvals,
2274+ ) ;
2275+ }
2276+
2277+ pub fn execute_emergency_action ( env : Env , member : Address , proposal_id : u64 ) {
2278+ require_trust_mode ( & env) ;
2279+ require_emergency_member ( & env, & member) ;
2280+
2281+ let total_members = active_member_count ( & env) ;
2282+ if total_members == 0 {
2283+ panic_with_error ! ( & env, ContractError :: EmergencyNoMembers ) ;
2284+ }
2285+
2286+ let mut proposal = get_emergency_proposal_or_panic ( & env, proposal_id) ;
2287+ if proposal. executed {
2288+ panic_with_error ! ( & env, ContractError :: EmergencyProposalExecuted ) ;
2289+ }
2290+
2291+ let approvals = env
2292+ . storage ( )
2293+ . instance ( )
2294+ . get :: < DataKey , u32 > ( & DataKey :: EmergencyProposalApprovalCount ( proposal_id) )
2295+ . unwrap_or ( 0 ) ;
2296+ if approvals != total_members {
2297+ panic_with_error ! ( & env, ContractError :: EmergencyApprovalIncomplete ) ;
2298+ }
2299+
2300+ let mut meter = get_meter_or_panic ( & env, proposal. meter_id ) ;
2301+ match proposal. action {
2302+ EmergencyAction :: SetFlowRate ( rate) => {
2303+ if rate <= 0 {
2304+ panic_with_error ! ( & env, ContractError :: InvalidTokenAmount ) ;
2305+ }
2306+ meter. max_flow_rate_per_hour = rate;
2307+ }
2308+ EmergencyAction :: PauseCycle => {
2309+ meter. is_paused = true ;
2310+ let now = env. ledger ( ) . timestamp ( ) ;
2311+ refresh_activity ( & mut meter, now) ;
2312+ }
2313+ }
2314+
2315+ env. storage ( )
2316+ . instance ( )
2317+ . set ( & DataKey :: Meter ( proposal. meter_id ) , & meter) ;
2318+
2319+ proposal. executed = true ;
2320+ env. storage ( )
2321+ . instance ( )
2322+ . set ( & DataKey :: EmergencyProposal ( proposal_id) , & proposal) ;
2323+
2324+ env. events ( ) . publish (
2325+ ( symbol_short ! ( "EmgExec" ) , proposal_id) ,
2326+ proposal. meter_id ,
2327+ ) ;
2328+ }
2329+
2330+ pub fn get_emergency_proposal ( env : Env , proposal_id : u64 ) -> Option < EmergencyProposal > {
2331+ env. storage ( )
2332+ . instance ( )
2333+ . get ( & DataKey :: EmergencyProposal ( proposal_id) )
2334+ }
2335+
20692336 pub fn set_tiered_pricing ( env : Env , meter_id : u64 , threshold : i128 , rate : i128 ) {
20702337 let mut meter = get_meter_or_panic ( & env, meter_id) ;
20712338 meter. provider . require_auth ( ) ;
@@ -3508,15 +3775,26 @@ impl UtilityContract {
35083775 pub fn register_active_user ( env : Env , user : Address ) {
35093776 user. require_auth ( ) ;
35103777
3511- // Simplified: just increment counter
3512- let count : u32 = env
3778+ // Track members uniquely so unanimous emergency approvals are exact.
3779+ let already_member = env
35133780 . storage ( )
35143781 . instance ( )
3515- . get ( & DataKey :: ActiveUsers )
3516- . unwrap_or ( 0 ) ;
3517- env. storage ( )
3518- . instance ( )
3519- . set ( & DataKey :: ActiveUsers , & ( count + 1 ) ) ;
3782+ . get :: < DataKey , bool > ( & DataKey :: ActiveUserMember ( user. clone ( ) ) )
3783+ . unwrap_or ( false ) ;
3784+
3785+ if !already_member {
3786+ let count: u32 = env
3787+ . storage ( )
3788+ . instance ( )
3789+ . get ( & DataKey :: ActiveUsers )
3790+ . unwrap_or ( 0 ) ;
3791+ env. storage ( )
3792+ . instance ( )
3793+ . set ( & DataKey :: ActiveUsers , & ( count + 1 ) ) ;
3794+ env. storage ( )
3795+ . instance ( )
3796+ . set ( & DataKey :: ActiveUserMember ( user. clone ( ) ) , & true ) ;
3797+ }
35203798
35213799 env. events ( )
35223800 . publish ( ( soroban_sdk:: symbol_short!( "ActvUser" ) , ) , user) ;
0 commit comments