#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
use std::fmt::Display;
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Api, Attribute, BlockInfo, DepsMut, StdError, StdResult, Storage};
use cw_address_like::AddressLike;
use cw_storage_plus::Item;
pub use cw_ownable_derive::{cw_ownable_execute, cw_ownable_query};
pub use cw_utils::Expiration;
#[cw_serde]
pub struct Ownership<T: AddressLike> {
pub owner: Option<T>,
pub pending_owner: Option<T>,
pub pending_expiry: Option<Expiration>,
}
pub struct OwnershipStore {
pub item: Item<Ownership<Addr>>,
}
impl OwnershipStore {
pub const fn new(key: &'static str) -> Self {
Self {
item: Item::new(key),
}
}
pub fn initialize_owner(
&self,
storage: &mut dyn Storage,
api: &dyn Api,
owner: Option<&str>,
) -> StdResult<Ownership<Addr>> {
let ownership = Ownership {
owner: owner.map(|h| api.addr_validate(h)).transpose()?,
pending_owner: None,
pending_expiry: None,
};
self.item.save(storage, &ownership)?;
Ok(ownership)
}
pub fn is_owner(&self, store: &dyn Storage, addr: &Addr) -> StdResult<bool> {
let ownership = self.item.load(store)?;
if let Some(owner) = ownership.owner {
if *addr == owner {
return Ok(true);
}
}
Ok(false)
}
pub fn assert_owner(&self, store: &dyn Storage, sender: &Addr) -> Result<(), OwnershipError> {
let ownership = self.item.load(store)?;
self.check_owner(&ownership, sender)
}
fn check_owner(
&self,
ownership: &Ownership<Addr>,
sender: &Addr,
) -> Result<(), OwnershipError> {
let Some(current_owner) = &ownership.owner else {
return Err(OwnershipError::NoOwner);
};
if sender != current_owner {
return Err(OwnershipError::NotOwner);
}
Ok(())
}
pub fn update_ownership(
&self,
deps: DepsMut,
block: &BlockInfo,
sender: &Addr,
action: Action,
) -> Result<Ownership<Addr>, OwnershipError> {
match action {
Action::TransferOwnership {
new_owner,
expiry,
} => self.transfer_ownership(deps.api, deps.storage, sender, &new_owner, expiry),
Action::AcceptOwnership => self.accept_ownership(deps.storage, block, sender),
Action::RenounceOwnership => self.renounce_ownership(deps.storage, sender),
}
}
pub fn get_ownership(&self, storage: &dyn Storage) -> StdResult<Ownership<Addr>> {
self.item.load(storage)
}
fn transfer_ownership(
&self,
api: &dyn Api,
storage: &mut dyn Storage,
sender: &Addr,
new_owner: &str,
expiry: Option<Expiration>,
) -> Result<Ownership<Addr>, OwnershipError> {
self.item.update(storage, |ownership| {
self.check_owner(&ownership, sender)?;
Ok(Ownership {
pending_owner: Some(api.addr_validate(new_owner)?),
pending_expiry: expiry,
..ownership
})
})
}
fn accept_ownership(
&self,
store: &mut dyn Storage,
block: &BlockInfo,
sender: &Addr,
) -> Result<Ownership<Addr>, OwnershipError> {
self.item.update(store, |ownership| {
let Some(pending_owner) = &ownership.pending_owner else {
return Err(OwnershipError::TransferNotFound);
};
if sender != pending_owner {
return Err(OwnershipError::NotPendingOwner);
};
if let Some(expiry) = &ownership.pending_expiry {
if expiry.is_expired(block) {
return Err(OwnershipError::TransferExpired);
}
}
Ok(Ownership {
owner: ownership.pending_owner,
pending_owner: None,
pending_expiry: None,
})
})
}
fn renounce_ownership(
&self,
store: &mut dyn Storage,
sender: &Addr,
) -> Result<Ownership<Addr>, OwnershipError> {
self.item.update(store, |ownership| {
self.check_owner(&ownership, sender)?;
Ok(Ownership {
owner: None,
pending_owner: None,
pending_expiry: None,
})
})
}
}
#[cw_serde]
pub enum Action {
TransferOwnership {
new_owner: String,
expiry: Option<Expiration>,
},
AcceptOwnership,
RenounceOwnership,
}
#[derive(thiserror::Error, Debug, PartialEq)]
pub enum OwnershipError {
#[error("{0}")]
Std(#[from] StdError),
#[error("Contract ownership has been renounced")]
NoOwner,
#[error("Caller is not the contract's current owner")]
NotOwner,
#[error("Caller is not the contract's pending owner")]
NotPendingOwner,
#[error("There isn't a pending ownership transfer")]
TransferNotFound,
#[error("A pending ownership transfer exists but it has expired")]
TransferExpired,
}
pub const OWNERSHIP_KEY: &str = "ownership";
const OWNERSHIP: OwnershipStore = OwnershipStore::new(OWNERSHIP_KEY);
pub fn initialize_owner(
storage: &mut dyn Storage,
api: &dyn Api,
owner: Option<&str>,
) -> StdResult<Ownership<Addr>> {
OWNERSHIP.initialize_owner(storage, api, owner)
}
pub fn is_owner(store: &dyn Storage, addr: &Addr) -> StdResult<bool> {
OWNERSHIP.is_owner(store, addr)
}
pub fn assert_owner(store: &dyn Storage, sender: &Addr) -> Result<(), OwnershipError> {
OWNERSHIP.assert_owner(store, sender)
}
pub fn update_ownership(
deps: DepsMut,
block: &BlockInfo,
sender: &Addr,
action: Action,
) -> Result<Ownership<Addr>, OwnershipError> {
OWNERSHIP.update_ownership(deps, block, sender, action)
}
pub fn get_ownership(storage: &dyn Storage) -> StdResult<Ownership<Addr>> {
OWNERSHIP.get_ownership(storage)
}
impl<T: AddressLike> Ownership<T> {
pub fn into_attributes(self) -> Vec<Attribute> {
vec![
Attribute::new("owner", none_or(self.owner.as_ref())),
Attribute::new("pending_owner", none_or(self.pending_owner.as_ref())),
Attribute::new("pending_expiry", none_or(self.pending_expiry.as_ref())),
]
}
}
fn none_or<T: Display>(or: Option<&T>) -> String {
or.map_or_else(|| "none".to_string(), |or| or.to_string())
}
#[cfg(test)]
mod tests {
use cosmwasm_std::{testing::{mock_dependencies, MockApi}, Timestamp};
use super::*;
fn mock_addresses(api: &MockApi) -> [Addr; 3] {
[
api.addr_make("larry"),
api.addr_make("jake"),
api.addr_make("pumpkin"),
]
}
fn mock_block_at_height(height: u64) -> BlockInfo {
BlockInfo {
height,
time: Timestamp::from_seconds(10000),
chain_id: "".into(),
}
}
#[test]
fn initializing_ownership() {
let mut deps = mock_dependencies();
let [larry, _, _] = mock_addresses(&deps.api);
let ownership =
OWNERSHIP.initialize_owner(&mut deps.storage, &deps.api, Some(larry.as_str())).unwrap();
assert_eq!(ownership, OWNERSHIP.item.load(deps.as_ref().storage).unwrap());
assert_eq!(
ownership,
Ownership {
owner: Some(larry),
pending_owner: None,
pending_expiry: None,
},
);
}
#[test]
fn initialize_ownership_no_owner() {
let mut deps = mock_dependencies();
let ownership = OWNERSHIP.initialize_owner(&mut deps.storage, &deps.api, None).unwrap();
assert_eq!(
ownership,
Ownership {
owner: None,
pending_owner: None,
pending_expiry: None,
},
);
}
#[test]
fn asserting_ownership() {
let mut deps = mock_dependencies();
let [larry, jake, _] = mock_addresses(&deps.api);
{
OWNERSHIP.initialize_owner(&mut deps.storage, &deps.api, Some(larry.as_str())).unwrap();
let res = OWNERSHIP.assert_owner(deps.as_ref().storage, &larry);
assert!(res.is_ok());
let res = OWNERSHIP.assert_owner(deps.as_ref().storage, &jake);
assert_eq!(res.unwrap_err(), OwnershipError::NotOwner);
}
{
OWNERSHIP.renounce_ownership(deps.as_mut().storage, &larry).unwrap();
let res = OWNERSHIP.assert_owner(deps.as_ref().storage, &larry);
assert_eq!(res.unwrap_err(), OwnershipError::NoOwner);
}
}
#[test]
fn transferring_ownership() {
let mut deps = mock_dependencies();
let [larry, jake, pumpkin] = mock_addresses(&deps.api);
OWNERSHIP.initialize_owner(&mut deps.storage, &deps.api, Some(larry.as_str())).unwrap();
{
let err = OWNERSHIP
.update_ownership(
deps.as_mut(),
&mock_block_at_height(12345),
&jake,
Action::TransferOwnership {
new_owner: pumpkin.to_string(),
expiry: None,
},
)
.unwrap_err();
assert_eq!(err, OwnershipError::NotOwner);
}
{
let ownership = OWNERSHIP
.update_ownership(
deps.as_mut(),
&mock_block_at_height(12345),
&larry,
Action::TransferOwnership {
new_owner: pumpkin.to_string(),
expiry: Some(Expiration::AtHeight(42069)),
},
)
.unwrap();
assert_eq!(
ownership,
Ownership {
owner: Some(larry),
pending_owner: Some(pumpkin),
pending_expiry: Some(Expiration::AtHeight(42069)),
},
);
let saved_ownership = OWNERSHIP.item.load(deps.as_ref().storage).unwrap();
assert_eq!(saved_ownership, ownership);
}
}
#[test]
fn accepting_ownership() {
let mut deps = mock_dependencies();
let [larry, jake, pumpkin] = mock_addresses(&deps.api);
OWNERSHIP
.initialize_owner(&mut deps.storage, &deps.api.clone(), Some(larry.as_str()))
.unwrap();
{
let err = OWNERSHIP
.update_ownership(
deps.as_mut(),
&mock_block_at_height(12345),
&pumpkin,
Action::AcceptOwnership,
)
.unwrap_err();
assert_eq!(err, OwnershipError::TransferNotFound);
}
OWNERSHIP
.transfer_ownership(
&deps.api.clone().clone(),
deps.as_mut().storage,
&larry,
pumpkin.as_str(),
Some(Expiration::AtHeight(42069)),
)
.unwrap();
{
let err = OWNERSHIP
.update_ownership(
deps.as_mut(),
&mock_block_at_height(12345),
&jake,
Action::AcceptOwnership,
)
.unwrap_err();
assert_eq!(err, OwnershipError::NotPendingOwner);
}
{
let err = OWNERSHIP
.update_ownership(
deps.as_mut(),
&mock_block_at_height(69420),
&pumpkin,
Action::AcceptOwnership,
)
.unwrap_err();
assert_eq!(err, OwnershipError::TransferExpired);
}
{
let ownership = OWNERSHIP
.update_ownership(
deps.as_mut(),
&mock_block_at_height(10000),
&pumpkin,
Action::AcceptOwnership,
)
.unwrap();
assert_eq!(
ownership,
Ownership {
owner: Some(pumpkin),
pending_owner: None,
pending_expiry: None,
},
);
let saved_ownership = OWNERSHIP.item.load(deps.as_ref().storage).unwrap();
assert_eq!(saved_ownership, ownership);
}
}
#[test]
fn renouncing_ownership() {
let mut deps = mock_dependencies();
let [larry, jake, pumpkin] = mock_addresses(&deps.api);
let ownership = Ownership {
owner: Some(larry.clone()),
pending_owner: Some(pumpkin),
pending_expiry: None,
};
OWNERSHIP.item.save(deps.as_mut().storage, &ownership).unwrap();
{
let err = OWNERSHIP
.update_ownership(
deps.as_mut(),
&mock_block_at_height(12345),
&jake,
Action::RenounceOwnership,
)
.unwrap_err();
assert_eq!(err, OwnershipError::NotOwner);
}
{
let ownership = OWNERSHIP
.update_ownership(
deps.as_mut(),
&mock_block_at_height(12345),
&larry,
Action::RenounceOwnership,
)
.unwrap();
assert_eq!(ownership, OWNERSHIP.item.load(deps.as_ref().storage).unwrap());
assert_eq!(
ownership,
Ownership {
owner: None,
pending_owner: None,
pending_expiry: None,
},
);
}
{
let err = OWNERSHIP
.update_ownership(
deps.as_mut(),
&mock_block_at_height(12345),
&larry,
Action::RenounceOwnership,
)
.unwrap_err();
assert_eq!(err, OwnershipError::NoOwner);
}
}
#[test]
fn into_attributes_works() {
use cw_utils::Expiration;
assert_eq!(
Ownership {
owner: Some("blue".to_string()),
pending_owner: None,
pending_expiry: Some(Expiration::Never {})
}
.into_attributes(),
vec![
Attribute::new("owner", "blue"),
Attribute::new("pending_owner", "none"),
Attribute::new("pending_expiry", "expiration: never")
],
);
}
}