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
10pub use cw_ownable_derive::{cw_ownable_execute, cw_ownable_query};
12pub use cw_utils::Expiration;
13
14#[cw_serde]
16pub struct Ownership<T: AddressLike> {
17 pub owner: Option<T>,
20
21 pub pending_owner: Option<T>,
24
25 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 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 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 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 fn check_owner(
84 &self,
85 ownership: &Ownership<Addr>,
86 sender: &Addr,
87 ) -> Result<(), OwnershipError> {
88 let Some(current_owner) = &ownership.owner else {
90 return Err(OwnershipError::NoOwner);
91 };
92
93 if sender != current_owner {
95 return Err(OwnershipError::NotOwner);
96 }
97
98 Ok(())
99 }
100
101 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 pub fn get_ownership(&self, storage: &dyn Storage) -> StdResult<Ownership<Addr>> {
122 self.item.load(storage)
123 }
124
125 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 self.check_owner(&ownership, sender)?;
138
139 Ok(Ownership {
151 pending_owner: Some(api.addr_validate(new_owner)?),
152 pending_expiry: expiry,
153 ..ownership
154 })
155 })
156 }
157
158 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 let Some(pending_owner) = &ownership.pending_owner else {
168 return Err(OwnershipError::TransferNotFound);
169 };
170
171 if sender != pending_owner {
173 return Err(OwnershipError::NotPendingOwner);
174 };
175
176 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 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#[cw_serde]
211pub enum Action {
212 TransferOwnership {
219 new_owner: String,
220 expiry: Option<Expiration>,
221 },
222
223 AcceptOwnership,
227
228 RenounceOwnership,
235}
236
237#[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
259pub const OWNERSHIP_KEY: &str = "ownership";
261const OWNERSHIP: OwnershipStore = OwnershipStore::new(OWNERSHIP_KEY);
262
263pub 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
274pub fn is_owner(store: &dyn Storage, addr: &Addr) -> StdResult<bool> {
279 OWNERSHIP.is_owner(store, addr)
280}
281
282pub fn assert_owner(store: &dyn Storage, sender: &Addr) -> Result<(), OwnershipError> {
284 OWNERSHIP.assert_owner(store, sender)
285}
286
287pub 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
298pub fn get_ownership(storage: &dyn Storage) -> StdResult<Ownership<Addr>> {
300 OWNERSHIP.get_ownership(storage)
301}
302
303impl<T: AddressLike> Ownership<T> {
304 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#[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 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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 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 {
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}