nibiru_ownable/
lib.rs

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