cw_ownable/
lib.rs

1#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
2
3use std::fmt::Display;
4
5use cosmwasm_schema::cw_serde;
6use cosmwasm_std::{Addr, Api, Attribute, BlockInfo, DepsMut, StdError, StdResult, Storage};
7use cw_address_like::AddressLike;
8use cw_storage_plus::Item;
9
10// re-export the proc macros and the Expiration class
11pub use cw_ownable_derive::{cw_ownable_execute, cw_ownable_query};
12pub use cw_utils::Expiration;
13
14/// The contract's ownership info
15#[cw_serde]
16pub struct Ownership<T: AddressLike> {
17    /// The contract's current owner.
18    /// `None` if the ownership has been renounced.
19    pub owner: Option<T>,
20
21    /// The account who has been proposed to take over the ownership.
22    /// `None` if there isn't a pending ownership transfer.
23    pub pending_owner: Option<T>,
24
25    /// The deadline for the pending owner to accept the ownership.
26    /// `None` if there isn't a pending ownership transfer, or if a transfer
27    /// exists and it doesn't have a deadline.
28    pub pending_expiry: Option<Expiration>,
29}
30
31pub struct OwnershipStore {
32    pub item: Item<Ownership<Addr>>,
33}
34
35impl OwnershipStore {
36    pub const fn new(key: &'static str) -> Self {
37        Self {
38            item: Item::new(key),
39        }
40    }
41
42    /// Set the given address as the contract owner.
43    ///
44    /// This function is only intended to be used only during contract instantiation.
45    pub fn initialize_owner(
46        &self,
47        storage: &mut dyn Storage,
48        api: &dyn Api,
49        owner: Option<&str>,
50    ) -> StdResult<Ownership<Addr>> {
51        let ownership = Ownership {
52            owner: owner.map(|h| api.addr_validate(h)).transpose()?,
53            pending_owner: None,
54            pending_expiry: None,
55        };
56        self.item.save(storage, &ownership)?;
57        Ok(ownership)
58    }
59
60    /// Return Ok(true) if the contract has an owner and it's the given address.
61    /// Return Ok(false) if the contract doesn't have an owner, of if it does but
62    /// it's not the given address.
63    /// Return Err if fails to load ownership info from storage.
64    pub fn is_owner(&self, store: &dyn Storage, addr: &Addr) -> StdResult<bool> {
65        let ownership = self.item.load(store)?;
66
67        if let Some(owner) = ownership.owner {
68            if *addr == owner {
69                return Ok(true);
70            }
71        }
72
73        Ok(false)
74    }
75
76    /// Assert that an account is the contract's current owner.
77    pub fn assert_owner(&self, store: &dyn Storage, sender: &Addr) -> Result<(), OwnershipError> {
78        let ownership = self.item.load(store)?;
79        self.check_owner(&ownership, sender)
80    }
81
82    /// Assert that an account is the contract's current owner.
83    fn check_owner(
84        &self,
85        ownership: &Ownership<Addr>,
86        sender: &Addr,
87    ) -> Result<(), OwnershipError> {
88        // the contract must have an owner
89        let Some(current_owner) = &ownership.owner else {
90            return Err(OwnershipError::NoOwner);
91        };
92
93        // the sender must be the current owner
94        if sender != current_owner {
95            return Err(OwnershipError::NotOwner);
96        }
97
98        Ok(())
99    }
100
101    /// Update the contract's ownership info based on the given action.
102    /// Return the updated ownership.
103    pub fn update_ownership(
104        &self,
105        deps: DepsMut,
106        block: &BlockInfo,
107        sender: &Addr,
108        action: Action,
109    ) -> Result<Ownership<Addr>, OwnershipError> {
110        match action {
111            Action::TransferOwnership {
112                new_owner,
113                expiry,
114            } => self.transfer_ownership(deps.api, deps.storage, sender, &new_owner, expiry),
115            Action::AcceptOwnership => self.accept_ownership(deps.storage, block, sender),
116            Action::RenounceOwnership => self.renounce_ownership(deps.storage, sender),
117        }
118    }
119
120    /// Get the current ownership value.
121    pub fn get_ownership(&self, storage: &dyn Storage) -> StdResult<Ownership<Addr>> {
122        self.item.load(storage)
123    }
124
125    /// Propose to transfer the contract's ownership to the given address, with an
126    /// optional deadline.
127    fn transfer_ownership(
128        &self,
129        api: &dyn Api,
130        storage: &mut dyn Storage,
131        sender: &Addr,
132        new_owner: &str,
133        expiry: Option<Expiration>,
134    ) -> Result<Ownership<Addr>, OwnershipError> {
135        self.item.update(storage, |ownership| {
136            // the contract must have an owner
137            self.check_owner(&ownership, sender)?;
138
139            // NOTE: We don't validate the expiry, i.e. asserting it is later than
140            // the current block time.
141            //
142            // This is because if the owner submits an invalid expiry, it won't have
143            // any negative effect - it's just that the pending owner won't be able
144            // to accept the ownership.
145            //
146            // By not doing the check, we save a little bit of gas.
147            //
148            // To fix the erorr, the owner can simply invoke `transfer_ownership`
149            // again with the correct expiry and overwrite the invalid one.
150            Ok(Ownership {
151                pending_owner: Some(api.addr_validate(new_owner)?),
152                pending_expiry: expiry,
153                ..ownership
154            })
155        })
156    }
157
158    /// Accept a pending ownership transfer.
159    fn accept_ownership(
160        &self,
161        store: &mut dyn Storage,
162        block: &BlockInfo,
163        sender: &Addr,
164    ) -> Result<Ownership<Addr>, OwnershipError> {
165        self.item.update(store, |ownership| {
166            // there must be an existing ownership transfer
167            let Some(pending_owner) = &ownership.pending_owner else {
168                return Err(OwnershipError::TransferNotFound);
169            };
170
171            // the sender must be the pending owner
172            if sender != pending_owner {
173                return Err(OwnershipError::NotPendingOwner);
174            };
175
176            // if the transfer has a deadline, it must not have been reached
177            if let Some(expiry) = &ownership.pending_expiry {
178                if expiry.is_expired(block) {
179                    return Err(OwnershipError::TransferExpired);
180                }
181            }
182
183            Ok(Ownership {
184                owner: ownership.pending_owner,
185                pending_owner: None,
186                pending_expiry: None,
187            })
188        })
189    }
190
191    /// Set the contract's ownership as vacant permanently.
192    fn renounce_ownership(
193        &self,
194        store: &mut dyn Storage,
195        sender: &Addr,
196    ) -> Result<Ownership<Addr>, OwnershipError> {
197        self.item.update(store, |ownership| {
198            self.check_owner(&ownership, sender)?;
199
200            Ok(Ownership {
201                owner: None,
202                pending_owner: None,
203                pending_expiry: None,
204            })
205        })
206    }
207}
208
209/// Actions that can be taken to alter the contract's ownership
210#[cw_serde]
211pub enum Action {
212    /// Propose to transfer the contract's ownership to another account,
213    /// optionally with an expiry time.
214    ///
215    /// Can only be called by the contract's current owner.
216    ///
217    /// Any existing pending ownership transfer is overwritten.
218    TransferOwnership {
219        new_owner: String,
220        expiry: Option<Expiration>,
221    },
222
223    /// Accept the pending ownership transfer.
224    ///
225    /// Can only be called by the pending owner.
226    AcceptOwnership,
227
228    /// Give up the contract's ownership and the possibility of appointing
229    /// a new owner.
230    ///
231    /// Can only be invoked by the contract's current owner.
232    ///
233    /// Any existing pending ownership transfer is canceled.
234    RenounceOwnership,
235}
236
237/// Errors associated with the contract's ownership
238#[derive(thiserror::Error, Debug, PartialEq)]
239pub enum OwnershipError {
240    #[error("{0}")]
241    Std(#[from] StdError),
242
243    #[error("Contract ownership has been renounced")]
244    NoOwner,
245
246    #[error("Caller is not the contract's current owner")]
247    NotOwner,
248
249    #[error("Caller is not the contract's pending owner")]
250    NotPendingOwner,
251
252    #[error("There isn't a pending ownership transfer")]
253    TransferNotFound,
254
255    #[error("A pending ownership transfer exists but it has expired")]
256    TransferExpired,
257}
258
259/// Storage constant for the contract's ownership
260pub const OWNERSHIP_KEY: &str = "ownership";
261const OWNERSHIP: OwnershipStore = OwnershipStore::new(OWNERSHIP_KEY);
262
263/// Set the given address as the contract owner.
264///
265/// This function is only intended to be used only during contract instantiation.
266pub fn initialize_owner(
267    storage: &mut dyn Storage,
268    api: &dyn Api,
269    owner: Option<&str>,
270) -> StdResult<Ownership<Addr>> {
271    OWNERSHIP.initialize_owner(storage, api, owner)
272}
273
274/// Return Ok(true) if the contract has an owner and it's the given address.
275/// Return Ok(false) if the contract doesn't have an owner, of if it does but
276/// it's not the given address.
277/// Return Err if fails to load ownership info from storage.
278pub fn is_owner(store: &dyn Storage, addr: &Addr) -> StdResult<bool> {
279    OWNERSHIP.is_owner(store, addr)
280}
281
282/// Assert that an account is the contract's current owner.
283pub fn assert_owner(store: &dyn Storage, sender: &Addr) -> Result<(), OwnershipError> {
284    OWNERSHIP.assert_owner(store, sender)
285}
286
287/// Update the contract's ownership info based on the given action.
288/// Return the updated ownership.
289pub fn update_ownership(
290    deps: DepsMut,
291    block: &BlockInfo,
292    sender: &Addr,
293    action: Action,
294) -> Result<Ownership<Addr>, OwnershipError> {
295    OWNERSHIP.update_ownership(deps, block, sender, action)
296}
297
298/// Get the current ownership value.
299pub fn get_ownership(storage: &dyn Storage) -> StdResult<Ownership<Addr>> {
300    OWNERSHIP.get_ownership(storage)
301}
302
303impl<T: AddressLike> Ownership<T> {
304    /// Serializes the current ownership state as attributes which may
305    /// be used in a message response. Serialization is done according
306    /// to the std::fmt::Display implementation for `T` and
307    /// `cosmwasm_std::Expiration` (for `pending_expiry`). If an
308    /// ownership field has no value, `"none"` will be serialized.
309    ///
310    /// Attribute keys used:
311    ///  - owner
312    ///  - pending_owner
313    ///  - pending_expiry
314    ///
315    /// Callers should take care not to use these keys elsewhere
316    /// in their response as CosmWasm will override reused attribute
317    /// keys.
318    ///
319    /// # Example
320    ///
321    /// ```rust
322    /// use cw_utils::Expiration;
323    ///
324    /// assert_eq!(
325    ///     Ownership {
326    ///         owner: Some("blue"),
327    ///         pending_owner: None,
328    ///         pending_expiry: Some(Expiration::Never {})
329    ///     }
330    ///     .into_attributes(),
331    ///     vec![
332    ///         Attribute::new("owner", "blue"),
333    ///         Attribute::new("pending_owner", "none"),
334    ///         Attribute::new("pending_expiry", "expiration: never")
335    ///     ],
336    /// )
337    /// ```
338    pub fn into_attributes(self) -> Vec<Attribute> {
339        vec![
340            Attribute::new("owner", none_or(self.owner.as_ref())),
341            Attribute::new("pending_owner", none_or(self.pending_owner.as_ref())),
342            Attribute::new("pending_expiry", none_or(self.pending_expiry.as_ref())),
343        ]
344    }
345}
346
347fn none_or<T: Display>(or: Option<&T>) -> String {
348    or.map_or_else(|| "none".to_string(), |or| or.to_string())
349}
350
351//------------------------------------------------------------------------------
352// Tests
353//------------------------------------------------------------------------------
354
355#[cfg(test)]
356mod tests {
357    use cosmwasm_std::{testing::{mock_dependencies, MockApi}, Timestamp};
358
359    use super::*;
360
361    fn mock_addresses(api: &MockApi) -> [Addr; 3] {
362        [
363            api.addr_make("larry"),
364            api.addr_make("jake"),
365            api.addr_make("pumpkin"),
366        ]
367    }
368
369    fn mock_block_at_height(height: u64) -> BlockInfo {
370        BlockInfo {
371            height,
372            time: Timestamp::from_seconds(10000),
373            chain_id: "".into(),
374        }
375    }
376
377    #[test]
378    fn initializing_ownership() {
379        let mut deps = mock_dependencies();
380        let [larry, _, _] = mock_addresses(&deps.api);
381
382        let ownership =
383            OWNERSHIP.initialize_owner(&mut deps.storage, &deps.api, Some(larry.as_str())).unwrap();
384
385        // ownership returned is same as ownership stored.
386        assert_eq!(ownership, OWNERSHIP.item.load(deps.as_ref().storage).unwrap());
387
388        assert_eq!(
389            ownership,
390            Ownership {
391                owner: Some(larry),
392                pending_owner: None,
393                pending_expiry: None,
394            },
395        );
396    }
397
398    #[test]
399    fn initialize_ownership_no_owner() {
400        let mut deps = mock_dependencies();
401        let ownership = OWNERSHIP.initialize_owner(&mut deps.storage, &deps.api, None).unwrap();
402        assert_eq!(
403            ownership,
404            Ownership {
405                owner: None,
406                pending_owner: None,
407                pending_expiry: None,
408            },
409        );
410    }
411
412    #[test]
413    fn asserting_ownership() {
414        let mut deps = mock_dependencies();
415        let [larry, jake, _] = mock_addresses(&deps.api);
416
417        // case 1. owner has not renounced
418        {
419            OWNERSHIP.initialize_owner(&mut deps.storage, &deps.api, Some(larry.as_str())).unwrap();
420
421            let res = OWNERSHIP.assert_owner(deps.as_ref().storage, &larry);
422            assert!(res.is_ok());
423
424            let res = OWNERSHIP.assert_owner(deps.as_ref().storage, &jake);
425            assert_eq!(res.unwrap_err(), OwnershipError::NotOwner);
426        }
427
428        // case 2. owner has renounced
429        {
430            OWNERSHIP.renounce_ownership(deps.as_mut().storage, &larry).unwrap();
431
432            let res = OWNERSHIP.assert_owner(deps.as_ref().storage, &larry);
433            assert_eq!(res.unwrap_err(), OwnershipError::NoOwner);
434        }
435    }
436
437    #[test]
438    fn transferring_ownership() {
439        let mut deps = mock_dependencies();
440        let [larry, jake, pumpkin] = mock_addresses(&deps.api);
441
442        OWNERSHIP.initialize_owner(&mut deps.storage, &deps.api, Some(larry.as_str())).unwrap();
443
444        // non-owner cannot transfer ownership
445        {
446            let err = OWNERSHIP
447                .update_ownership(
448                    deps.as_mut(),
449                    &mock_block_at_height(12345),
450                    &jake,
451                    Action::TransferOwnership {
452                        new_owner: pumpkin.to_string(),
453                        expiry: None,
454                    },
455                )
456                .unwrap_err();
457            assert_eq!(err, OwnershipError::NotOwner);
458        }
459
460        // owner properly transfers ownership
461        {
462            let ownership = OWNERSHIP
463                .update_ownership(
464                    deps.as_mut(),
465                    &mock_block_at_height(12345),
466                    &larry,
467                    Action::TransferOwnership {
468                        new_owner: pumpkin.to_string(),
469                        expiry: Some(Expiration::AtHeight(42069)),
470                    },
471                )
472                .unwrap();
473            assert_eq!(
474                ownership,
475                Ownership {
476                    owner: Some(larry),
477                    pending_owner: Some(pumpkin),
478                    pending_expiry: Some(Expiration::AtHeight(42069)),
479                },
480            );
481
482            let saved_ownership = OWNERSHIP.item.load(deps.as_ref().storage).unwrap();
483            assert_eq!(saved_ownership, ownership);
484        }
485    }
486
487    #[test]
488    fn accepting_ownership() {
489        let mut deps = mock_dependencies();
490        let [larry, jake, pumpkin] = mock_addresses(&deps.api);
491
492        OWNERSHIP
493            .initialize_owner(&mut deps.storage, &deps.api.clone(), Some(larry.as_str()))
494            .unwrap();
495
496        // cannot accept ownership when there isn't a pending ownership transfer
497        {
498            let err = OWNERSHIP
499                .update_ownership(
500                    deps.as_mut(),
501                    &mock_block_at_height(12345),
502                    &pumpkin,
503                    Action::AcceptOwnership,
504                )
505                .unwrap_err();
506            assert_eq!(err, OwnershipError::TransferNotFound);
507        }
508
509        OWNERSHIP
510            .transfer_ownership(
511                &deps.api.clone().clone(),
512                deps.as_mut().storage,
513                &larry,
514                pumpkin.as_str(),
515                Some(Expiration::AtHeight(42069)),
516            )
517            .unwrap();
518
519        // non-pending owner cannot accept ownership
520        {
521            let err = OWNERSHIP
522                .update_ownership(
523                    deps.as_mut(),
524                    &mock_block_at_height(12345),
525                    &jake,
526                    Action::AcceptOwnership,
527                )
528                .unwrap_err();
529            assert_eq!(err, OwnershipError::NotPendingOwner);
530        }
531
532        // cannot accept ownership if deadline has passed
533        {
534            let err = OWNERSHIP
535                .update_ownership(
536                    deps.as_mut(),
537                    &mock_block_at_height(69420),
538                    &pumpkin,
539                    Action::AcceptOwnership,
540                )
541                .unwrap_err();
542            assert_eq!(err, OwnershipError::TransferExpired);
543        }
544
545        // pending owner properly accepts ownership before deadline
546        {
547            let ownership = OWNERSHIP
548                .update_ownership(
549                    deps.as_mut(),
550                    &mock_block_at_height(10000),
551                    &pumpkin,
552                    Action::AcceptOwnership,
553                )
554                .unwrap();
555            assert_eq!(
556                ownership,
557                Ownership {
558                    owner: Some(pumpkin),
559                    pending_owner: None,
560                    pending_expiry: None,
561                },
562            );
563
564            let saved_ownership = OWNERSHIP.item.load(deps.as_ref().storage).unwrap();
565            assert_eq!(saved_ownership, ownership);
566        }
567    }
568
569    #[test]
570    fn renouncing_ownership() {
571        let mut deps = mock_dependencies();
572        let [larry, jake, pumpkin] = mock_addresses(&deps.api);
573
574        let ownership = Ownership {
575            owner: Some(larry.clone()),
576            pending_owner: Some(pumpkin),
577            pending_expiry: None,
578        };
579        OWNERSHIP.item.save(deps.as_mut().storage, &ownership).unwrap();
580
581        // non-owner cannot renounce
582        {
583            let err = OWNERSHIP
584                .update_ownership(
585                    deps.as_mut(),
586                    &mock_block_at_height(12345),
587                    &jake,
588                    Action::RenounceOwnership,
589                )
590                .unwrap_err();
591            assert_eq!(err, OwnershipError::NotOwner);
592        }
593
594        // owner properly renounces
595        {
596            let ownership = OWNERSHIP
597                .update_ownership(
598                    deps.as_mut(),
599                    &mock_block_at_height(12345),
600                    &larry,
601                    Action::RenounceOwnership,
602                )
603                .unwrap();
604
605            // ownership returned is same as ownership stored.
606            assert_eq!(ownership, OWNERSHIP.item.load(deps.as_ref().storage).unwrap());
607
608            assert_eq!(
609                ownership,
610                Ownership {
611                    owner: None,
612                    pending_owner: None,
613                    pending_expiry: None,
614                },
615            );
616        }
617
618        // cannot renounce twice
619        {
620            let err = OWNERSHIP
621                .update_ownership(
622                    deps.as_mut(),
623                    &mock_block_at_height(12345),
624                    &larry,
625                    Action::RenounceOwnership,
626                )
627                .unwrap_err();
628            assert_eq!(err, OwnershipError::NoOwner);
629        }
630    }
631
632    #[test]
633    fn into_attributes_works() {
634        use cw_utils::Expiration;
635        assert_eq!(
636            Ownership {
637                owner: Some("blue".to_string()),
638                pending_owner: None,
639                pending_expiry: Some(Expiration::Never {})
640            }
641            .into_attributes(),
642            vec![
643                Attribute::new("owner", "blue"),
644                Attribute::new("pending_owner", "none"),
645                Attribute::new("pending_expiry", "expiration: never")
646            ],
647        );
648    }
649}