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
14pub use cw_utils::Expiration;
16pub use nibiru_ownable_derive::{ownable_execute, ownable_query};
17
18#[cw_serde]
20pub struct Ownership<T: AddressLike> {
21 pub owner: Option<T>,
24
25 pub pending_owner: Option<T>,
28
29 pub pending_expiry: Option<Expiration>,
33}
34
35#[cw_serde]
37pub enum Action {
38 TransferOwnership {
45 new_owner: String,
46 expiry: Option<Expiration>,
47 },
48
49 AcceptOwnership,
53
54 RenounceOwnership,
61}
62
63#[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
85const OWNERSHIP: Item<Ownership<String>> = Item::new("ownership");
87
88pub 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
104pub 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
120pub 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
129fn check_owner(
131 ownership: &Ownership<String>,
132 sender: &str,
133) -> Result<(), OwnershipError> {
134 let Some(current_owner) = &ownership.owner else {
136 return Err(OwnershipError::NoOwner);
137 };
138
139 if sender != current_owner {
141 return Err(OwnershipError::NotOwner);
142 }
143
144 Ok(())
145}
146
147pub 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
164pub fn get_ownership(storage: &dyn Storage) -> StdResult<Ownership<String>> {
166 OWNERSHIP.load(storage)
167}
168
169impl<T: AddressLike> Ownership<T> {
170 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
223fn 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 check_owner(&ownership, sender)?;
234
235 Ok(Ownership {
247 pending_owner: Some(new_owner.to_string()),
248 pending_expiry: expiry,
249 ..ownership
250 })
251 })
252}
253
254fn accept_ownership(
256 store: &mut dyn Storage,
257 block: &BlockInfo,
258 sender: &str,
259) -> Result<Ownership<String>, OwnershipError> {
260 OWNERSHIP.update(store, |ownership| {
261 let Some(pending_owner) = &ownership.pending_owner else {
263 return Err(OwnershipError::TransferNotFound);
264 };
265
266 if sender != pending_owner {
268 return Err(OwnershipError::NotPendingOwner);
269 };
270
271 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
286fn 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#[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 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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 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 {
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}