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;
11pub use luru20_cw_ownable_derive::{cw_ownable_execute, cw_ownable_query};
13pub use cw_utils::Expiration;
14
15
16#[cw_serde]
18pub struct Ownership<T: AddressLike> {
19 pub owner: Option<T>,
22
23 pub pending_owner: Option<T>,
26
27 pub pending_expiry: Option<Expiration>,
31}
32
33#[cw_serde]
35pub enum Action {
36 TransferOwnership {
43 new_owner: String,
44 expiry: Option<Expiration>,
45 },
46
47 AcceptOwnership,
51
52 RenounceOwnership,
59}
60
61#[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
83const OWNERSHIP: Item<Ownership<Addr>> = Item::new("ownership");
85
86pub 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
103pub 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
119pub fn assert_owner(store: &dyn Storage, sender: &Addr) -> Result<(), OwnershipError> {
121 let ownership = OWNERSHIP.load(store)?;
122 check_owner(&ownership, sender)
123}
124
125fn check_owner(ownership: &Ownership<Addr>, sender: &Addr) -> Result<(), OwnershipError> {
127 let Some(current_owner) = &ownership.owner else {
129 return Err(OwnershipError::NoOwner);
130 };
131
132 if sender != current_owner {
134 return Err(OwnershipError::NotOwner);
135 }
136
137 Ok(())
138}
139
140pub 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
158pub fn get_ownership(storage: &dyn Storage) -> StdResult<Ownership<Addr>> {
160 OWNERSHIP.load(storage)
161}
162
163impl<T: AddressLike> Ownership<T> {
164 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
211fn 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 check_owner(&ownership, sender)?;
222
223 Ok(Ownership {
235 pending_owner: Some(deps.api.addr_validate(new_owner)?),
236 pending_expiry: expiry,
237 ..ownership
238 })
239 })
240}
241
242fn accept_ownership(
244 store: &mut dyn Storage,
245 block: &BlockInfo,
246 sender: &Addr,
247) -> Result<Ownership<Addr>, OwnershipError> {
248 OWNERSHIP.update(store, |ownership| {
249 let Some(pending_owner) = &ownership.pending_owner else {
251 return Err(OwnershipError::TransferNotFound);
252 };
253
254 if sender != pending_owner {
256 return Err(OwnershipError::NotPendingOwner);
257 };
258
259 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
274fn 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#[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 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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 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 {
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}