1use borsh::{BorshDeserialize, BorshSerialize};
27use serde::{Deserialize, Serialize};
28
29pub 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}