Skip to main content

lez_approval/
lib.rs

1//! Agnostic single-admin approval library for LEZ programs.
2//!
3//! Provides a reusable `Authority` primitive with `gate`, `rotate`, and `revoke`
4//! operations. Satisfies [RFP-001](https://github.com/logos-co/rfp/blob/master/RFPs/RFP-001-admin-authority-lib.md).
5//!
6//! # Usage
7//!
8//! ```
9//! use lez_approval::Authority;
10//!
11//! let admin = [1u8; 32];
12//! let mut auth = Authority::new(admin);
13//!
14//! // Only the admin can call gated operations.
15//! auth.gate(admin); // OK
16//!
17//! // Rotate to a new admin.
18//! let new_admin = [2u8; 32];
19//! auth.rotate(admin, new_admin);
20//!
21//! // Permanently revoke — terminal, cannot be reversed.
22//! auth.revoke(new_admin);
23//! assert!(auth.is_renounced());
24//! ```
25
26use borsh::{BorshDeserialize, BorshSerialize};
27use serde::{Deserialize, Serialize};
28
29/// A 32-byte account identifier, compatible with `nssa_core::account::AccountId`.
30pub type AccountId = [u8; 32];
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
33pub struct Authority(Option<AccountId>);
34
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum ApprovalError {
37    Unauthorized,
38    Renounced,
39}
40
41impl std::fmt::Display for ApprovalError {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        match self {
44            Self::Unauthorized => write!(f, "Unauthorized: signer is not the current authority"),
45            Self::Renounced => write!(f, "Renounced: authority has been permanently revoked"),
46        }
47    }
48}
49
50impl Authority {
51    #[must_use]
52    pub fn new(admin: AccountId) -> Self {
53        Self(Some(admin))
54    }
55
56    #[must_use]
57    pub fn renounced() -> Self {
58        Self(None)
59    }
60
61    #[must_use]
62    pub fn is_renounced(&self) -> bool {
63        self.0.is_none()
64    }
65
66    #[must_use]
67    pub fn admin(&self) -> Option<AccountId> {
68        self.0
69    }
70
71    pub fn gate(&self, signer: AccountId) {
72        match self.0 {
73            None => panic!("{}", ApprovalError::Renounced),
74            Some(admin) => {
75                assert!(admin == signer, "{}", ApprovalError::Unauthorized);
76            }
77        }
78    }
79
80    pub fn rotate(&mut self, signer: AccountId, new_admin: AccountId) {
81        self.gate(signer);
82        self.0 = Some(new_admin);
83    }
84
85    pub fn revoke(&mut self, signer: AccountId) {
86        self.gate(signer);
87        self.0 = None;
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    fn admin_id() -> AccountId {
96        [1; 32]
97    }
98
99    fn other_id() -> AccountId {
100        [2; 32]
101    }
102
103    fn new_admin_id() -> AccountId {
104        [3; 32]
105    }
106
107    #[test]
108    fn new_authority_is_active() {
109        let auth = Authority::new(admin_id());
110        assert!(!auth.is_renounced());
111        assert_eq!(auth.admin(), Some(admin_id()));
112    }
113
114    #[test]
115    fn renounced_authority() {
116        let auth = Authority::renounced();
117        assert!(auth.is_renounced());
118        assert_eq!(auth.admin(), None);
119    }
120
121    #[test]
122    fn gate_succeeds_for_admin() {
123        let auth = Authority::new(admin_id());
124        auth.gate(admin_id());
125    }
126
127    #[test]
128    #[should_panic(expected = "Unauthorized")]
129    fn gate_fails_for_wrong_signer() {
130        let auth = Authority::new(admin_id());
131        auth.gate(other_id());
132    }
133
134    #[test]
135    #[should_panic(expected = "Renounced")]
136    fn gate_fails_when_renounced() {
137        let auth = Authority::renounced();
138        auth.gate(admin_id());
139    }
140
141    #[test]
142    fn rotate_succeeds() {
143        let mut auth = Authority::new(admin_id());
144        auth.rotate(admin_id(), new_admin_id());
145        assert_eq!(auth.admin(), Some(new_admin_id()));
146    }
147
148    #[test]
149    #[should_panic(expected = "Unauthorized")]
150    fn rotate_fails_for_wrong_signer() {
151        let mut auth = Authority::new(admin_id());
152        auth.rotate(other_id(), new_admin_id());
153    }
154
155    #[test]
156    #[should_panic(expected = "Renounced")]
157    fn rotate_fails_when_renounced() {
158        let mut auth = Authority::renounced();
159        auth.rotate(admin_id(), new_admin_id());
160    }
161
162    #[test]
163    fn revoke_succeeds() {
164        let mut auth = Authority::new(admin_id());
165        auth.revoke(admin_id());
166        assert!(auth.is_renounced());
167    }
168
169    #[test]
170    #[should_panic(expected = "Unauthorized")]
171    fn revoke_fails_for_wrong_signer() {
172        let mut auth = Authority::new(admin_id());
173        auth.revoke(other_id());
174    }
175
176    #[test]
177    #[should_panic(expected = "Renounced")]
178    fn revoke_fails_when_already_renounced() {
179        let mut auth = Authority::renounced();
180        auth.revoke(admin_id());
181    }
182
183    #[test]
184    fn rotate_then_old_admin_rejected() {
185        let mut auth = Authority::new(admin_id());
186        auth.rotate(admin_id(), new_admin_id());
187        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
188            auth.gate(admin_id());
189        }));
190        assert!(result.is_err());
191    }
192
193    #[test]
194    fn rotate_then_new_admin_accepted() {
195        let mut auth = Authority::new(admin_id());
196        auth.rotate(admin_id(), new_admin_id());
197        auth.gate(new_admin_id());
198    }
199}