luru20_cw_ownable/
lib.rs

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