bvs_registry/
state.rs

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
8/// Mapping of service address to boolean value
9/// indicating if the service is registered with the registry
10pub(crate) const SERVICES: Map<&Service, bool> = Map::new("services");
11
12/// Require that the service is registered in the state
13pub 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
26/// Mapping of operator address to boolean value
27/// indicating if the operator is registered with the registry
28pub(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/// Registered status of the Operator to Service
44/// Can be initiated by the Operator or the Service
45/// Becomes Active when the Operator and Service both have registered
46/// Becomes Inactive when the Operator or Service have unregistered (default state)
47#[cw_serde]
48pub enum RegistrationStatus {
49    /// Default state when neither the Operator nor the Service has registered,
50    /// or when either the Operator or Service has unregistered
51    Inactive = 0,
52
53    /// State when both the Operator and Service have registered with each other,
54    /// indicating a fully established relationship
55    Active = 1,
56
57    /// State when only the Operator has registered but the Service hasn't yet registered,
58    /// indicating a pending registration from the Service side
59    /// This is Operator-initiated registration, waiting for Service to finalize
60    OperatorRegistered = 2,
61
62    /// State when only the Service has registered but the Operator hasn't yet registered,
63    /// indicating a pending registration from the Operator side
64    /// This is Service-initiated registration, waiting for Operator to finalize
65    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
88/// Mapping of (operator_service) address.
89/// See `RegistrationStatus` for more of the status.
90/// Use [get_registration_status] and [set_registration_status] to interact with this map.
91pub(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
98/// Get the registration status of the Operator to Service
99pub 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
110/// Get the registration status of the Operator to Service at a specific timestamp
111///
112/// #### Warning
113/// This function will return previous state.
114/// If timestamp is equal to the timestamp of the save operation.
115/// New state will only be available at timestamp + 1
116pub 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
128/// Set the registration status of the Operator to Service at current timestamp
129///
130/// #### Warning
131/// This function will only save the state at the end of the block.
132/// So the new state will only be available at timestamp + 1.
133/// This is so that re-ordering of txs won't cause the state to be inconsistent.
134pub 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 service has enabled slashing, opt-in operator to slashing
145            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
171/// Stores the active registration count of the operator to services.
172/// This is used to check if the operator is actively registered to any service (> 0)
173pub(crate) const OPERATOR_ACTIVE_REGISTRATION_COUNT: Map<&Operator, u64> =
174    Map::new("operator_active_registration_count");
175
176/// Check if the operator is actively registered to any service
177pub 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
185/// Increase the operator active registration count by 1
186pub 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
198/// Decrease the operator active registration count by 1
199pub 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    /// The address to which the slashed funds will be sent after the slashing is finalized.  
214    /// None, indicates that the slashed funds will be burned.
215    pub destination: Option<Addr>,
216    /// The maximum percentage of the operator's total stake that can be slashed.  
217    /// The value is represented in bips (basis points), where 100 bips = 1%.  
218    /// And the value must be between 0 and 10_000 (inclusive).
219    pub max_slashing_bips: u16,
220    /// The minimum amount of time (in seconds)
221    /// that the slashing can be delayed before it is executed and finalized.  
222    /// Setting this value to a duration less than the queued withdrawal delay is recommended.
223    /// To prevent restaker's early withdrawal of their assets from the vault due to the impending slash,
224    /// defeating the purpose of shared security.
225    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
246/// Mapping of service to the latest slashing parameters.
247///
248/// The presence of the Service key in the map indicates that slashing is enabled for that service.
249pub(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
256/// Returns whether slashing is enabled for the given service at the given timestamp.
257pub 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
271/// Enable slashing for the given service at current timestamp
272pub 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    // Validate the slashing parameters
280    slashing_parameters.validate(api)?;
281
282    // Save the slashing parameters to the store
283    SLASHING_PARAMETERS.save(
284        store,
285        service,
286        slashing_parameters,
287        env.block.time.seconds(),
288    )?;
289    Ok(())
290}
291
292/// Disable slashing for the given service at current timestamp
293pub 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
298/// Stores the slashing parameters opt-in status for (service, operator) pair.
299///
300/// If value is `true`,
301/// operator has opted in to slashing parameters for that service at the given timestamp.
302/// If key isn't found, it means the operator hasn't opted in to slashing parameters for that service.
303/// The `false` value is not used.
304pub(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
311/// Opt-in operator to the current service slashing parameters at current timestamp
312pub 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
322/// Check if the operator has opted in to slashing for the given service at the given timestamp.
323pub 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
340/// Clears the slashing parameters opt-in status for the given service at current timestamp.
341/// This happens only when a new slashing condition is set/updated.
342pub 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        // assert that the operator is not active
377        let res = is_operator_active(&deps.storage, &operator).unwrap();
378        assert!(!res);
379
380        // set the operator active count to 1
381        OPERATOR_ACTIVE_REGISTRATION_COUNT
382            .save(&mut deps.storage, &operator, &1)
383            .expect("OPERATOR_ACTIVE_REGISTRATION_COUNT save failed");
384
385        // assert that the operator is active
386        let res = is_operator_active(&deps.storage, &operator).unwrap();
387        assert!(res);
388
389        // assert that the operator2 is not active
390        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        // assert that the operator is not registered
416        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        // assert that the operator is registered
425        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        // NEGATIVE tests
472        {
473            // Invalid destination address
474            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            // max_slashing_percentage too high
489            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        // POSITIVE tests
504        {
505            // Valid slashing parameters
506            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            // Valid slashing parameters with None destination
516            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        // assert that the operator is not opted in
535        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        // assert that the operator is opted in
549        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        // set the slashing parameters opt-in status
565        {
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        // move the block time forward
601        env.block.time = env.block.time.plus_seconds(10);
602
603        // assert that the operator and operator2 are opted in to service
604        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 the slashing parameters opt-in status for service
612        reset_slashing_opt_in(&mut deps.storage, &env, &service).unwrap();
613
614        // move the block time forward
615        env.block.time = env.block.time.plus_seconds(10);
616
617        // assert that the operator and operator2 are not opted in to service
618        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        // assert that operator and operator2 are still opted in to service2
626        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        // assert that the operator and operator2 are opted in to service at the previous timestamp
634        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}