1use crate::error::ContractError;
2use bvs_library::addr::{Operator, Service};
3use bvs_library::storage::EVERY_SECOND;
4use cosmwasm_schema::cw_serde;
5use cosmwasm_std::{Addr, Api, Env, Order, StdError, StdResult, Storage};
6use cw_storage_plus::{Map, SnapshotMap};
7
8pub(crate) const SERVICES: Map<&Service, bool> = Map::new("services");
11
12pub fn require_service_registered(
14 store: &dyn Storage,
15 service: &Addr,
16) -> Result<(), ContractError> {
17 let registered = SERVICES.may_load(store, service)?.unwrap_or(false);
18
19 if !registered {
20 return Err(ContractError::Std(StdError::not_found("service")));
21 }
22
23 Ok(())
24}
25
26pub(crate) const OPERATORS: Map<&Operator, bool> = Map::new("operators");
29
30pub fn require_operator_registered(
31 store: &dyn Storage,
32 operator: &Addr,
33) -> Result<(), ContractError> {
34 let registered = OPERATORS.may_load(store, operator)?.unwrap_or(false);
35
36 if !registered {
37 return Err(ContractError::Std(StdError::not_found("operator")));
38 }
39
40 Ok(())
41}
42
43#[cw_serde]
48pub enum RegistrationStatus {
49 Inactive = 0,
52
53 Active = 1,
56
57 OperatorRegistered = 2,
61
62 ServiceRegistered = 3,
66}
67
68impl From<RegistrationStatus> for u8 {
69 fn from(value: RegistrationStatus) -> u8 {
70 value as u8
71 }
72}
73
74impl TryFrom<u8> for RegistrationStatus {
75 type Error = StdError;
76
77 fn try_from(value: u8) -> Result<Self, StdError> {
78 match value {
79 0 => Ok(RegistrationStatus::Inactive),
80 1 => Ok(RegistrationStatus::Active),
81 2 => Ok(RegistrationStatus::OperatorRegistered),
82 3 => Ok(RegistrationStatus::ServiceRegistered),
83 _ => Err(StdError::generic_err("RegistrationStatus out of range")),
84 }
85 }
86}
87
88pub(crate) const REGISTRATION_STATUS: SnapshotMap<(&Operator, &Service), u8> = SnapshotMap::new(
92 "registration_status",
93 "registration_status_checkpoint",
94 "registration_status_changelog",
95 EVERY_SECOND,
96);
97
98pub fn get_registration_status(
100 store: &dyn Storage,
101 key: (&Operator, &Service),
102) -> StdResult<RegistrationStatus> {
103 let status = REGISTRATION_STATUS
104 .may_load(store, key)?
105 .unwrap_or(RegistrationStatus::Inactive.into());
106
107 status.try_into()
108}
109
110pub fn get_registration_status_at_timestamp(
117 store: &dyn Storage,
118 key: (&Operator, &Service),
119 timestamp: u64,
120) -> StdResult<RegistrationStatus> {
121 let status = REGISTRATION_STATUS
122 .may_load_at_height(store, key, timestamp)?
123 .unwrap_or(RegistrationStatus::Inactive.into());
124
125 status.try_into()
126}
127
128pub fn set_registration_status(
135 store: &mut dyn Storage,
136 env: &Env,
137 key: (&Operator, &Service),
138 status: RegistrationStatus,
139) -> StdResult<()> {
140 let (operator, service) = key;
141 match status {
142 RegistrationStatus::Active => {
143 increase_operator_active_registration_count(store, operator)?;
144 if is_slashing_enabled(store, service, Some(env.block.time.seconds()))? {
146 opt_in_to_slashing(store, env, service, operator)?;
147 }
148 }
149 RegistrationStatus::Inactive => {
150 decrease_operator_active_registration_count(store, operator)?;
151 }
152 _ => {}
153 }
154
155 REGISTRATION_STATUS.save(store, key, &status.into(), env.block.time.seconds())?;
156 Ok(())
157}
158
159pub fn require_active_registration_status(
160 store: &dyn Storage,
161 key: (&Operator, &Service),
162) -> Result<(), ContractError> {
163 match get_registration_status(store, key)? {
164 RegistrationStatus::Active => Ok(()),
165 _ => Err(ContractError::InvalidRegistrationStatus {
166 msg: "Operator and service must have active registration".to_string(),
167 }),
168 }
169}
170
171pub(crate) const OPERATOR_ACTIVE_REGISTRATION_COUNT: Map<&Operator, u64> =
174 Map::new("operator_active_registration_count");
175
176pub fn is_operator_active(store: &dyn Storage, operator: &Operator) -> StdResult<bool> {
178 let active_count = OPERATOR_ACTIVE_REGISTRATION_COUNT
179 .may_load(store, operator)?
180 .unwrap_or(0);
181
182 Ok(active_count > 0)
183}
184
185pub fn increase_operator_active_registration_count(
187 store: &mut dyn Storage,
188 operator: &Operator,
189) -> StdResult<u64> {
190 OPERATOR_ACTIVE_REGISTRATION_COUNT.update(store, operator, |count| {
191 let new_count = count.unwrap_or(0).checked_add(1);
192 new_count.ok_or_else(|| {
193 StdError::generic_err("Increase operator active registration count failed")
194 })
195 })
196}
197
198pub fn decrease_operator_active_registration_count(
200 store: &mut dyn Storage,
201 operator: &Operator,
202) -> StdResult<u64> {
203 OPERATOR_ACTIVE_REGISTRATION_COUNT.update(store, operator, |count| {
204 let new_count = count.unwrap_or(0).checked_sub(1);
205 new_count.ok_or_else(|| {
206 StdError::generic_err("Decrease operator active registration count failed")
207 })
208 })
209}
210
211#[cw_serde]
212pub struct SlashingParameters {
213 pub destination: Option<Addr>,
216 pub max_slashing_bips: u16,
220 pub resolution_window: u64,
226}
227
228impl SlashingParameters {
229 pub fn validate(&self, api: &dyn Api) -> Result<(), ContractError> {
230 if let Some(destination) = &self.destination {
231 api.addr_validate(destination.as_str()).map_err(|_| {
232 ContractError::InvalidSlashingParameters {
233 msg: "Invalid destination address format".to_string(),
234 }
235 })?;
236 }
237 if self.max_slashing_bips > 10_000 {
238 return Err(ContractError::InvalidSlashingParameters {
239 msg: "Max slashing bips exceeds 10,000 bips (100%)".to_string(),
240 });
241 }
242 Ok(())
243 }
244}
245
246pub(crate) const SLASHING_PARAMETERS: SnapshotMap<&Service, SlashingParameters> = SnapshotMap::new(
250 "slashing_parameters",
251 "slashing_parameters_checkpoint",
252 "slashing_parameters_changelog",
253 EVERY_SECOND,
254);
255
256pub fn is_slashing_enabled(
258 store: &dyn Storage,
259 service: &Service,
260 timestamp: Option<u64>,
261) -> StdResult<bool> {
262 let is_enabled = match timestamp {
263 Some(t) => SLASHING_PARAMETERS
264 .may_load_at_height(store, service, t)?
265 .is_some(),
266 None => SLASHING_PARAMETERS.may_load(store, service)?.is_some(),
267 };
268 Ok(is_enabled)
269}
270
271pub fn enable_slashing(
273 store: &mut dyn Storage,
274 api: &dyn Api,
275 env: &Env,
276 service: &Service,
277 slashing_parameters: &SlashingParameters,
278) -> Result<(), ContractError> {
279 slashing_parameters.validate(api)?;
281
282 SLASHING_PARAMETERS.save(
284 store,
285 service,
286 slashing_parameters,
287 env.block.time.seconds(),
288 )?;
289 Ok(())
290}
291
292pub fn disable_slashing(store: &mut dyn Storage, env: &Env, service: &Service) -> StdResult<()> {
294 SLASHING_PARAMETERS.remove(store, service, env.block.time.seconds())?;
295 Ok(())
296}
297
298pub(crate) const SLASHING_OPT_IN: SnapshotMap<(&Service, &Operator), bool> = SnapshotMap::new(
305 "slashing_opt_in",
306 "slashing_opt_in_checkpoint",
307 "slashing_opt_in_changelog",
308 EVERY_SECOND,
309);
310
311pub fn opt_in_to_slashing(
313 store: &mut dyn Storage,
314 env: &Env,
315 service: &Service,
316 operator: &Operator,
317) -> StdResult<()> {
318 SLASHING_OPT_IN.save(store, (service, operator), &true, env.block.time.seconds())?;
319 Ok(())
320}
321
322pub fn is_operator_opted_in_to_slashing(
324 store: &dyn Storage,
325 service: &Service,
326 operator: &Operator,
327 timestamp: Option<u64>,
328) -> StdResult<bool> {
329 let is_opted_in = match timestamp {
330 Some(t) => SLASHING_OPT_IN
331 .may_load_at_height(store, (service, operator), t)?
332 .is_some(),
333 None => SLASHING_OPT_IN
334 .may_load(store, (service, operator))?
335 .is_some(),
336 };
337 Ok(is_opted_in)
338}
339
340pub fn reset_slashing_opt_in(
343 store: &mut dyn Storage,
344 env: &Env,
345 service: &Service,
346) -> Result<(), ContractError> {
347 let operator_keys = SLASHING_OPT_IN
348 .prefix(service)
349 .range(store, None, None, Order::Ascending)
350 .map(|item| {
351 let (operator, _) = item?;
352 Ok(operator)
353 })
354 .collect::<Vec<StdResult<Operator>>>();
355
356 for operator in operator_keys {
357 let key = (service, &operator?);
358 SLASHING_OPT_IN.remove(store, key, env.block.time.seconds())?;
359 }
360 Ok(())
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366 use bvs_library::time::{DAYS, MINUTES};
367 use cosmwasm_std::testing::{mock_dependencies, mock_env};
368
369 #[test]
370 fn test_is_operator_active() {
371 let mut deps = mock_dependencies();
372
373 let operator = deps.api.addr_make("operator");
374 let operator2 = deps.api.addr_make("operator2");
375
376 let res = is_operator_active(&deps.storage, &operator).unwrap();
378 assert!(!res);
379
380 OPERATOR_ACTIVE_REGISTRATION_COUNT
382 .save(&mut deps.storage, &operator, &1)
383 .expect("OPERATOR_ACTIVE_REGISTRATION_COUNT save failed");
384
385 let res = is_operator_active(&deps.storage, &operator).unwrap();
387 assert!(res);
388
389 let res = is_operator_active(&deps.storage, &operator2).unwrap();
391 assert!(!res);
392 }
393
394 #[test]
395 fn test_require_service_registered() {
396 let mut deps = mock_dependencies();
397
398 let service = deps.api.addr_make("service");
399
400 let res = require_service_registered(&deps.storage, &service);
401 assert_eq!(res, Err(ContractError::Std(StdError::not_found("service"))));
402
403 SERVICES.save(&mut deps.storage, &service, &true).unwrap();
404
405 let res = require_service_registered(&deps.storage, &service);
406 assert!(res.is_ok());
407 }
408
409 #[test]
410 fn test_require_operator_registered() {
411 let mut deps = mock_dependencies();
412
413 let operator = deps.api.addr_make("operator");
414
415 let res = require_operator_registered(&deps.storage, &operator);
417 assert_eq!(
418 res,
419 Err(ContractError::Std(StdError::not_found("operator")))
420 );
421
422 OPERATORS.save(&mut deps.storage, &operator, &true).unwrap();
423
424 let res = require_operator_registered(&deps.storage, &operator);
426 assert!(res.is_ok());
427 }
428
429 #[test]
430 fn test_registration_status() {
431 let mut deps = mock_dependencies();
432 let env = mock_env();
433
434 let operator = deps.api.addr_make("operator");
435 let service = deps.api.addr_make("service");
436
437 let key = (&operator, &service);
438
439 let status = get_registration_status(&deps.storage, key).unwrap();
440 assert_eq!(status, RegistrationStatus::Inactive);
441
442 set_registration_status(&mut deps.storage, &env, key, RegistrationStatus::Active).unwrap();
443 let status = get_registration_status(&deps.storage, key).unwrap();
444 assert_eq!(status, RegistrationStatus::Active);
445
446 set_registration_status(
447 &mut deps.storage,
448 &env,
449 key,
450 RegistrationStatus::OperatorRegistered,
451 )
452 .unwrap();
453 let status = get_registration_status(&deps.storage, key).unwrap();
454 assert_eq!(status, RegistrationStatus::OperatorRegistered);
455
456 set_registration_status(
457 &mut deps.storage,
458 &env,
459 key,
460 RegistrationStatus::ServiceRegistered,
461 )
462 .unwrap();
463 let status = get_registration_status(&deps.storage, key).unwrap();
464 assert_eq!(status, RegistrationStatus::ServiceRegistered);
465 }
466
467 #[test]
468 fn test_slashing_parameters_validate() {
469 let deps = mock_dependencies();
470
471 {
473 let valid_slashing_parameters = SlashingParameters {
475 destination: Some(Addr::unchecked("invalid_address")),
476 max_slashing_bips: 100,
477 resolution_window: 60 * MINUTES,
478 };
479
480 assert_eq!(
481 valid_slashing_parameters.validate(&deps.api).unwrap_err(),
482 ContractError::InvalidSlashingParameters {
483 msg: "Invalid destination address format".to_string()
484 }
485 );
486 }
487 {
488 let valid_slashing_parameters = SlashingParameters {
490 destination: Some(deps.api.addr_make("destination")),
491 max_slashing_bips: 10_001,
492 resolution_window: 60 * MINUTES,
493 };
494
495 assert_eq!(
496 valid_slashing_parameters.validate(&deps.api).unwrap_err(),
497 ContractError::InvalidSlashingParameters {
498 msg: "Max slashing bips exceeds 10,000 bips (100%)".to_string()
499 }
500 );
501 }
502
503 {
505 let valid_slashing_parameters = SlashingParameters {
507 destination: Some(deps.api.addr_make("destination")),
508 max_slashing_bips: 10_000,
509 resolution_window: 7 * DAYS,
510 };
511
512 assert!(valid_slashing_parameters.validate(&deps.api).is_ok());
513 }
514 {
515 let valid_slashing_parameters = SlashingParameters {
517 destination: None,
518 max_slashing_bips: 0,
519 resolution_window: 0,
520 };
521
522 assert!(valid_slashing_parameters.validate(&deps.api).is_ok());
523 }
524 }
525
526 #[test]
527 fn test_is_operator_opted_in_to_slashing() {
528 let mut deps = mock_dependencies();
529 let env = mock_env();
530
531 let service = deps.api.addr_make("service");
532 let operator = deps.api.addr_make("operator");
533
534 let res =
536 is_operator_opted_in_to_slashing(&deps.storage, &service, &operator, None).unwrap();
537 assert!(!res);
538
539 SLASHING_OPT_IN
540 .save(
541 &mut deps.storage,
542 (&service, &operator),
543 &true,
544 env.block.time.seconds(),
545 )
546 .unwrap();
547
548 let res =
550 is_operator_opted_in_to_slashing(&deps.storage, &service, &operator, None).unwrap();
551 assert!(res);
552 }
553
554 #[test]
555 fn test_reset_slashing_opt_in() {
556 let mut deps = mock_dependencies();
557 let mut env = mock_env();
558
559 let service = deps.api.addr_make("service");
560 let service2 = deps.api.addr_make("service2");
561 let operator = deps.api.addr_make("operator");
562 let operator2 = deps.api.addr_make("operator2");
563
564 {
566 SLASHING_OPT_IN
567 .save(
568 &mut deps.storage,
569 (&service, &operator),
570 &true,
571 env.block.time.seconds(),
572 )
573 .unwrap();
574 SLASHING_OPT_IN
575 .save(
576 &mut deps.storage,
577 (&service, &operator2),
578 &true,
579 env.block.time.seconds(),
580 )
581 .unwrap();
582 SLASHING_OPT_IN
583 .save(
584 &mut deps.storage,
585 (&service2, &operator),
586 &true,
587 env.block.time.seconds(),
588 )
589 .unwrap();
590 SLASHING_OPT_IN
591 .save(
592 &mut deps.storage,
593 (&service2, &operator2),
594 &true,
595 env.block.time.seconds(),
596 )
597 .unwrap();
598 }
599
600 env.block.time = env.block.time.plus_seconds(10);
602
603 let res =
605 is_operator_opted_in_to_slashing(&deps.storage, &service, &operator, None).unwrap();
606 assert!(res);
607 let res =
608 is_operator_opted_in_to_slashing(&deps.storage, &service, &operator2, None).unwrap();
609 assert!(res);
610
611 reset_slashing_opt_in(&mut deps.storage, &env, &service).unwrap();
613
614 env.block.time = env.block.time.plus_seconds(10);
616
617 let res =
619 is_operator_opted_in_to_slashing(&deps.storage, &service, &operator, None).unwrap();
620 assert!(!res);
621 let res =
622 is_operator_opted_in_to_slashing(&deps.storage, &service, &operator2, None).unwrap();
623 assert!(!res);
624
625 let res =
627 is_operator_opted_in_to_slashing(&deps.storage, &service2, &operator, None).unwrap();
628 assert!(res);
629 let res =
630 is_operator_opted_in_to_slashing(&deps.storage, &service2, &operator2, None).unwrap();
631 assert!(res);
632
633 let res = is_operator_opted_in_to_slashing(
635 &deps.storage,
636 &service,
637 &operator,
638 Some(env.block.time.minus_seconds(10).seconds()),
639 )
640 .unwrap();
641 assert!(res);
642
643 let res = is_operator_opted_in_to_slashing(
644 &deps.storage,
645 &service,
646 &operator2,
647 Some(env.block.time.minus_seconds(10).seconds()),
648 )
649 .unwrap();
650 assert!(res);
651 }
652}