fadroma/admin/
mod.rs

1//! Transaction authentication by pre-configured admin address.
2//! See the [examples](https://github.com/hackbg/fadroma/tree/master/examples) on how to implement it.
3
4pub use fadroma_proc_auth::*;
5
6use serde::{Serialize, Deserialize};
7
8use crate::{
9    dsl::*,
10    core::Canonize,
11    storage::SingleItem,
12    schemars::JsonSchema,
13    cosmwasm_std::{
14        self,
15        Deps, DepsMut, Response, MessageInfo,
16        CanonicalAddr, StdResult, StdError, Addr
17    }
18};
19
20crate::namespace!(pub AdminNs, b"ltp5P6sFZT");
21pub const STORE: SingleItem<CanonicalAddr, AdminNs> = SingleItem::new();
22
23crate::namespace!(pub PendingAdminNs, b"b5QaJXDibK");
24pub const PENDING_ADMIN: SingleItem<CanonicalAddr, PendingAdminNs> = SingleItem::new();
25
26#[interface]
27pub trait Admin {
28    type Error: std::fmt::Display;
29
30    #[execute]
31    fn change_admin(mode: Option<Mode>) -> Result<Response, Self::Error>;
32
33    #[query]
34    fn admin() -> Result<Option<Addr>, Self::Error>;
35}
36
37#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Debug, Clone)]
38pub enum Mode {
39    /// The new admin is set using a single transaction where the current admin
40    /// calls [`Admin::change_admin`] with this variant and the new admin is set
41    /// immediately provided that the transaction succeeded.
42    /// 
43    /// Use this when the new admin is a contract and it cannot accept the role.
44    Immediate { new_admin: String },
45    /// The new admin is set using a two-step process. First, the current admin
46    /// initiates the change by nominating a new admin by calling [`Admin::change_admin`]
47    /// with this variant. Then the nominated address must accept the admin role by
48    /// calling [`Admin::change_admin`] but this time with [`None`] as an argument.
49    /// It is possible for the current admin to set the pending admin as many times
50    /// as needed. This allows to correct any mistakes in case the wrong address was
51    /// nominated.
52    /// 
53    /// Use this when the new admin is always a wallet address and not a contract.
54    TwoStep { new_admin: String }
55}
56
57/// Initializes the admin module. Sets the messages sender as the admin
58/// if `address` is [`None`]. You **must** call this in your instantiate message.
59/// 
60/// Returns the canonical address of the admin that was set.
61pub fn init(
62    deps: DepsMut,
63    address: Option<&str>,
64    info: &MessageInfo
65) -> StdResult<CanonicalAddr> {
66    let admin = if let Some(addr) = address {
67        &addr
68    } else {
69        info.sender.as_str()
70    };
71
72    let admin = admin.canonize(deps.api)?;
73    STORE.save(deps.storage, &admin)?;
74
75    Ok(admin)
76}
77
78/// Asserts that the message sender is the admin. Otherwise returns an `Err`.
79pub fn assert(deps: Deps, info: &MessageInfo) -> StdResult<()> {
80    let admin = STORE.load_humanize(deps)?;
81
82    if let Some(admin) = admin {
83        if admin == info.sender {
84            return Ok(());
85        }
86    }
87
88    Err(StdError::generic_err("Unauthorized"))
89}
90
91#[derive(Clone, Copy, Debug)]
92pub struct DefaultImpl;
93
94impl Admin for DefaultImpl {
95    type Error = StdError;
96
97    #[execute]
98    fn change_admin(mode: Option<Mode>) -> StdResult<Response> {
99        if let Some(mode) = mode {
100            assert(deps.as_ref(), &info)?;
101
102            match mode {
103                Mode::Immediate { new_admin } =>
104                    STORE.canonize_and_save(deps, new_admin.as_str())?,
105                Mode::TwoStep { new_admin } =>
106                    PENDING_ADMIN.canonize_and_save(deps, new_admin.as_str())?,
107            }
108        } else {
109            if let Some(pending) = PENDING_ADMIN.load_humanize(deps.as_ref())? {
110                if pending == info.sender {
111                    STORE.canonize_and_save(deps, pending.as_str())?;
112                } else {
113                    return Err(StdError::generic_err("Unauthorized"));
114                }
115            } else {
116                return Err(StdError::generic_err("No address is currently expected to accept the admin role."));
117            }
118        }
119
120        Ok(Response::new())
121    }
122
123    #[query]
124    fn admin() -> StdResult<Option<Addr>> {
125        STORE.load_humanize(deps)
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use crate::{
133        admin,
134        cosmwasm_std::{
135            StdError,
136            testing::{mock_dependencies, mock_env, mock_info},
137        }
138    };
139
140    #[test]
141    fn test_init_admin() {
142        let ref mut deps = mock_dependencies();
143
144        let admin = DefaultImpl::admin(deps.as_ref(), mock_env()).unwrap();
145        assert!(admin.is_none());
146
147        let admin = "admin";
148        admin::init(deps.as_mut(), Some(admin), &mock_info("Tio Macaco", &[])).unwrap();
149
150        let stored_admin = DefaultImpl::admin(deps.as_ref(), mock_env()).unwrap();
151        assert_eq!(stored_admin.unwrap(), admin);
152    }
153
154    #[test]
155    fn test_init_default_admin() {
156        let ref mut deps = mock_dependencies();
157
158        let admin = DefaultImpl::admin(deps.as_ref(), mock_env()).unwrap();
159        assert!(admin.is_none());
160
161        let admin = "admin";
162        admin::init(deps.as_mut(), None, &mock_info(admin, &[])).unwrap();
163
164        let stored_admin = DefaultImpl::admin(deps.as_ref(), mock_env()).unwrap();
165        assert_eq!(stored_admin.unwrap(), admin);
166    }
167
168    #[test]
169    fn test_change_invariants_prior_to_change() {
170        let ref mut deps = mock_dependencies();
171
172        let admin = "admin";
173        admin::init(deps.as_mut(), None, &mock_info(admin, &[])).unwrap();
174
175        let new_admin = "new_admin";
176
177        let err = DefaultImpl::change_admin(
178            deps.as_mut(),
179            mock_env(),
180            mock_info("What about me?", &[]),
181            Some(Mode::Immediate { new_admin: new_admin.into() })
182        ).unwrap_err();
183        assert_unauthorized(&err);
184
185        let err = DefaultImpl::change_admin(
186            deps.as_mut(),
187            mock_env(),
188            mock_info("What about me?", &[]),
189            Some(Mode::TwoStep { new_admin: new_admin.into() })
190        ).unwrap_err();
191        assert_unauthorized(&err);
192
193        let err = DefaultImpl::change_admin(
194            deps.as_mut(),
195            mock_env(),
196            mock_info("What about me?", &[]),
197            None
198        ).unwrap_err();
199        assert_no_pending(&err);
200
201        let err = DefaultImpl::change_admin(
202            deps.as_mut(),
203            mock_env(),
204            mock_info(admin, &[]),
205            None
206        ).unwrap_err();
207        assert_no_pending(&err);
208
209        let stored_admin = DefaultImpl::admin(deps.as_ref(), mock_env()).unwrap();
210        assert_eq!(stored_admin.unwrap(), admin);
211    }
212
213    #[test]
214    fn test_change_admin_immediate() {
215        let ref mut deps = mock_dependencies();
216
217        let admin = "admin";
218        admin::init(deps.as_mut(), None, &mock_info(admin, &[])).unwrap();
219
220        let new_admin = "new_admin";
221
222        DefaultImpl::change_admin(
223            deps.as_mut(),
224            mock_env(),
225            mock_info(admin, &[]),
226            Some(Mode::Immediate { new_admin: new_admin.into() })
227        ).unwrap();
228
229        let err = DefaultImpl::change_admin(
230            deps.as_mut(),
231            mock_env(),
232            mock_info(admin, &[]),
233            Some(Mode::Immediate { new_admin: new_admin.into() })
234        ).unwrap_err();
235        assert_unauthorized(&err);
236
237        let err = DefaultImpl::change_admin(
238            deps.as_mut(),
239            mock_env(),
240            mock_info(admin, &[]),
241            Some(Mode::TwoStep { new_admin: new_admin.into() })
242        ).unwrap_err();
243        assert_unauthorized(&err);
244
245        let err = DefaultImpl::change_admin(
246            deps.as_mut(),
247            mock_env(),
248            mock_info(admin, &[]),
249            None
250        ).unwrap_err();
251        assert_no_pending(&err);
252
253        let err = DefaultImpl::change_admin(
254            deps.as_mut(),
255            mock_env(),
256            mock_info(new_admin, &[]),
257            None
258        ).unwrap_err();
259        assert_no_pending(&err);
260
261        let stored_admin = DefaultImpl::admin(deps.as_ref(), mock_env()).unwrap();
262        assert_eq!(stored_admin.unwrap(), new_admin);
263    }
264
265    #[test]
266    fn test_change_admin_two_step() {
267        let ref mut deps = mock_dependencies();
268
269        let admin = "admin";
270        admin::init(deps.as_mut(), None, &mock_info(admin, &[])).unwrap();
271
272        let new_admin = "new_admin";
273        let new_admin2 = "new_admin2";
274
275        DefaultImpl::change_admin(
276            deps.as_mut(),
277            mock_env(),
278            mock_info(admin, &[]),
279            Some(Mode::TwoStep { new_admin: new_admin.into() })
280        ).unwrap();
281
282        // It should be possible for the admin to set a pending address
283        // at any time before the new admin has accepted.
284        DefaultImpl::change_admin(
285            deps.as_mut(),
286            mock_env(),
287            mock_info(admin, &[]),
288            Some(Mode::TwoStep { new_admin: new_admin2.into() })
289        ).unwrap();
290
291        let err = DefaultImpl::change_admin(
292            deps.as_mut(),
293            mock_env(),
294            mock_info(new_admin, &[]),
295            None
296        ).unwrap_err();
297        assert_unauthorized(&err);
298
299        DefaultImpl::change_admin(
300            deps.as_mut(),
301            mock_env(),
302            mock_info(new_admin2, &[]),
303            None
304        ).unwrap();
305
306        let stored_admin = DefaultImpl::admin(deps.as_ref(), mock_env()).unwrap();
307        assert_eq!(stored_admin.unwrap(), new_admin2);
308
309        let err = DefaultImpl::change_admin(
310            deps.as_mut(),
311            mock_env(),
312            mock_info(admin, &[]),
313            Some(Mode::TwoStep { new_admin: new_admin.into() })
314        ).unwrap_err();
315        assert_unauthorized(&err);
316
317        let err = DefaultImpl::change_admin(
318            deps.as_mut(),
319            mock_env(),
320            mock_info(admin, &[]),
321            Some(Mode::Immediate { new_admin: new_admin.into() })
322        ).unwrap_err();
323        assert_unauthorized(&err);
324
325        DefaultImpl::change_admin(
326            deps.as_mut(),
327            mock_env(),
328            mock_info(new_admin2, &[]),
329            Some(Mode::Immediate { new_admin: new_admin.into() })
330        ).unwrap();
331
332        let stored_admin = DefaultImpl::admin(deps.as_ref(), mock_env()).unwrap();
333        assert_eq!(stored_admin.unwrap(), new_admin);
334    }
335
336    fn assert_unauthorized(err: &StdError) {
337        match err {
338            StdError::GenericErr { msg } => assert_eq!(msg, "Unauthorized"),
339            _ => panic!("Expected \"StdError::GenericErr\"")
340        };
341    }
342
343    fn assert_no_pending(err: &StdError) {
344        match err {
345            StdError::GenericErr { msg } => assert_eq!(msg, "No address is currently expected to accept the admin role."),
346            _ => panic!("Expected \"StdError::GenericErr\"")
347        };
348    }
349}