andromeda_std/ado_contract/
ownership.rs

1use crate::common::MillisecondsExpiration;
2use crate::error::ContractError;
3use crate::{
4    ado_base::ownership::{ContractPotentialOwnerResponse, OwnershipMessage},
5    ado_contract::ADOContract,
6};
7use cosmwasm_std::{attr, ensure, Addr, DepsMut, Env, MessageInfo, Response, Storage};
8use cw_storage_plus::Item;
9
10const POTENTIAL_OWNER: Item<Addr> = Item::new("andr_potential_owner");
11const POTENTIAL_OWNER_EXPIRATION: Item<MillisecondsExpiration> =
12    Item::new("andr_potential_owner_expiration");
13
14impl<'a> ADOContract<'a> {
15    pub fn execute_ownership(
16        &self,
17        deps: DepsMut,
18        env: Env,
19        info: MessageInfo,
20        msg: OwnershipMessage,
21    ) -> Result<Response, ContractError> {
22        match msg {
23            OwnershipMessage::UpdateOwner {
24                new_owner,
25                expiration,
26            } => self.update_owner(deps, info, new_owner, expiration),
27            OwnershipMessage::RevokeOwnershipOffer => self.revoke_ownership_offer(deps, info),
28            OwnershipMessage::AcceptOwnership => self.accept_ownership(deps, env, info),
29            OwnershipMessage::Disown => self.disown(deps, info),
30        }
31    }
32
33    /// Updates the current contract owner. **Only executable by the current contract owner.**
34    pub fn update_owner(
35        &self,
36        deps: DepsMut,
37        info: MessageInfo,
38        new_owner: Addr,
39        expiration: Option<MillisecondsExpiration>,
40    ) -> Result<Response, ContractError> {
41        ensure!(
42            self.is_contract_owner(deps.storage, info.sender.as_str())?,
43            ContractError::Unauthorized {}
44        );
45        ensure!(
46            !self.is_contract_owner(deps.storage, new_owner.as_str())?,
47            ContractError::Unauthorized {}
48        );
49        let new_owner_addr = deps.api.addr_validate(new_owner.as_ref())?;
50        POTENTIAL_OWNER.save(deps.storage, &new_owner_addr)?;
51
52        if let Some(exp) = expiration {
53            POTENTIAL_OWNER_EXPIRATION.save(deps.storage, &exp)?;
54        } else {
55            // In case an offer is already pending
56            POTENTIAL_OWNER_EXPIRATION.remove(deps.storage);
57        }
58
59        Ok(Response::new().add_attributes(vec![
60            attr("action", "update_owner"),
61            attr("value", new_owner),
62        ]))
63    }
64
65    /// Revokes the ownership offer. **Only executable by the current contract owner.**
66    pub fn revoke_ownership_offer(
67        &self,
68        deps: DepsMut,
69        info: MessageInfo,
70    ) -> Result<Response, ContractError> {
71        ensure!(
72            self.is_contract_owner(deps.storage, info.sender.as_str())?,
73            ContractError::Unauthorized {}
74        );
75        POTENTIAL_OWNER.remove(deps.storage);
76        POTENTIAL_OWNER_EXPIRATION.remove(deps.storage);
77        Ok(Response::new().add_attributes(vec![attr("action", "revoke_ownership_offer")]))
78    }
79
80    /// Accepts the ownership of the contract. **Only executable by the new contract owner.**
81    pub fn accept_ownership(
82        &self,
83        deps: DepsMut,
84        env: Env,
85        info: MessageInfo,
86    ) -> Result<Response, ContractError> {
87        let new_owner_addr = POTENTIAL_OWNER.load(deps.storage)?;
88        ensure!(
89            info.sender == new_owner_addr,
90            ContractError::Unauthorized {}
91        );
92        let expiration = POTENTIAL_OWNER_EXPIRATION.may_load(deps.storage)?;
93        if let Some(exp) = expiration {
94            ensure!(!exp.is_expired(&env.block), ContractError::Unauthorized {});
95        }
96
97        self.owner.save(deps.storage, &new_owner_addr)?;
98        POTENTIAL_OWNER.remove(deps.storage);
99        POTENTIAL_OWNER_EXPIRATION.remove(deps.storage);
100        Ok(Response::new().add_attributes(vec![
101            attr("action", "accept_ownership"),
102            attr("value", new_owner_addr.to_string()),
103        ]))
104    }
105
106    /// Disowns the contract. **Only executable by the current contract owner.**
107    pub fn disown(&self, deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
108        ensure!(
109            self.is_contract_owner(deps.storage, info.sender.as_str())?,
110            ContractError::Unauthorized {}
111        );
112        self.owner.save(deps.storage, &Addr::unchecked("null"))?;
113        Ok(Response::new().add_attributes(vec![attr("action", "disown")]))
114    }
115
116    /// Helper function to query if a given address is the current contract owner.
117    ///
118    /// Returns a boolean value indicating if the given address is the contract owner.
119    pub fn owner(&self, storage: &dyn Storage) -> Result<Addr, ContractError> {
120        let owner = self.owner.load(storage)?;
121        Ok(owner)
122    }
123
124    /// Helper function to query if a given address is the current contract owner.
125    ///
126    /// Returns a boolean value indicating if the given address is the contract owner.
127    pub fn is_contract_owner(
128        &self,
129        storage: &dyn Storage,
130        addr: &str,
131    ) -> Result<bool, ContractError> {
132        let owner = self.owner.load(storage)?;
133        Ok(addr == owner)
134    }
135
136    /// Helper function to query if a given address is the current contract owner or operator.
137    ///
138    /// Returns a boolean value indicating if the given address is the contract owner or operator.
139    pub fn is_owner_or_operator(
140        &self,
141        storage: &dyn Storage,
142        addr: &str,
143    ) -> Result<bool, ContractError> {
144        self.is_contract_owner(storage, addr)
145    }
146
147    pub fn ownership_request(
148        &self,
149        storage: &dyn Storage,
150    ) -> Result<ContractPotentialOwnerResponse, ContractError> {
151        let potential_owner = POTENTIAL_OWNER.may_load(storage)?;
152        let expiration = POTENTIAL_OWNER_EXPIRATION.may_load(storage)?;
153        Ok(ContractPotentialOwnerResponse {
154            potential_owner,
155            expiration,
156        })
157    }
158}
159
160#[cfg(test)]
161mod test {
162    use cosmwasm_std::{
163        testing::{mock_dependencies, mock_env, mock_info},
164        Addr, DepsMut,
165    };
166
167    use crate::{
168        ado_contract::{
169            ownership::{POTENTIAL_OWNER, POTENTIAL_OWNER_EXPIRATION},
170            ADOContract,
171        },
172        common::MillisecondsExpiration,
173    };
174
175    fn init(deps: DepsMut, owner: impl Into<String>) {
176        ADOContract::default()
177            .owner
178            .save(deps.storage, &Addr::unchecked(owner))
179            .unwrap();
180    }
181
182    #[test]
183    fn test_update_owner() {
184        let mut deps = mock_dependencies();
185        let contract = ADOContract::default();
186        let new_owner = Addr::unchecked("new_owner");
187        init(deps.as_mut(), "owner");
188
189        let res = contract.update_owner(
190            deps.as_mut(),
191            mock_info("owner", &[]),
192            new_owner.clone(),
193            None,
194        );
195        assert!(res.is_ok());
196        let saved_new_owner = POTENTIAL_OWNER.load(deps.as_ref().storage).unwrap();
197        assert_eq!(saved_new_owner, new_owner);
198
199        let res = contract.update_owner(
200            deps.as_mut(),
201            mock_info("owner", &[]),
202            Addr::unchecked("owner"),
203            None,
204        );
205        assert!(res.is_err());
206        let res =
207            contract.update_owner(deps.as_mut(), mock_info("new_owner", &[]), new_owner, None);
208        assert!(res.is_err());
209    }
210
211    #[test]
212    fn test_revoke_ownership_offer() {
213        let mut deps = mock_dependencies();
214        let contract = ADOContract::default();
215        init(deps.as_mut(), "owner");
216
217        let res = contract.revoke_ownership_offer(deps.as_mut(), mock_info("owner", &[]));
218        assert!(res.is_ok());
219        let saved_new_owner = POTENTIAL_OWNER.may_load(deps.as_ref().storage).unwrap();
220        assert!(saved_new_owner.is_none());
221    }
222
223    #[test]
224    fn test_accept_ownership() {
225        let mut deps = mock_dependencies();
226        let contract = ADOContract::default();
227        let new_owner = Addr::unchecked("new_owner");
228        init(deps.as_mut(), "owner");
229        POTENTIAL_OWNER
230            .save(deps.as_mut().storage, &new_owner)
231            .unwrap();
232
233        let res = contract.accept_ownership(deps.as_mut(), mock_env(), mock_info("owner", &[]));
234        assert!(res.is_err());
235        let res = contract.accept_ownership(deps.as_mut(), mock_env(), mock_info("new_owner", &[]));
236        assert!(res.is_ok());
237        let saved_owner = contract.owner.load(deps.as_ref().storage).unwrap();
238        assert_eq!(saved_owner, new_owner);
239        let saved_new_owner = POTENTIAL_OWNER.may_load(deps.as_ref().storage).unwrap();
240        assert!(saved_new_owner.is_none());
241    }
242
243    #[test]
244    fn test_accept_ownership_expired() {
245        let mut deps = mock_dependencies();
246        let contract = ADOContract::default();
247        let new_owner = Addr::unchecked("new_owner");
248        init(deps.as_mut(), "owner");
249        POTENTIAL_OWNER
250            .save(deps.as_mut().storage, &new_owner)
251            .unwrap();
252        POTENTIAL_OWNER_EXPIRATION
253            .save(
254                deps.as_mut().storage,
255                &MillisecondsExpiration::from_nanos(1),
256            )
257            .unwrap();
258
259        let mut env = mock_env();
260        env.block.time = MillisecondsExpiration::from_nanos(2).into();
261        let res = contract.accept_ownership(deps.as_mut(), env, mock_info("new_owner", &[]));
262        assert!(res.is_err());
263        let saved_owner = contract.owner.load(deps.as_ref().storage).unwrap();
264        assert_eq!(saved_owner, Addr::unchecked("owner"));
265    }
266
267    #[test]
268    fn test_disown() {
269        let mut deps = mock_dependencies();
270        let contract = ADOContract::default();
271        init(deps.as_mut(), "owner");
272
273        let res = contract.disown(deps.as_mut(), mock_info("owner", &[]));
274        assert!(res.is_ok());
275        let saved_owner = contract.owner.load(deps.as_ref().storage).unwrap();
276        assert_eq!(saved_owner, Addr::unchecked("null"));
277    }
278}