abstract_std/objects/
gov_type.rs

1//! # Governance structure object
2
3use crate::{account::state::ACCOUNT_ID, native_addrs, registry};
4use cosmwasm_std::{Addr, Deps, QuerierWrapper};
5use cw_address_like::AddressLike;
6use cw_utils::Expiration;
7
8use crate::AbstractError;
9
10use super::ownership::cw721;
11
12const MIN_GOV_TYPE_LENGTH: usize = 4;
13const MAX_GOV_TYPE_LENGTH: usize = 64;
14
15/// Governance types
16#[cosmwasm_schema::cw_serde]
17#[derive(Eq)]
18#[non_exhaustive]
19pub enum GovernanceDetails<T: AddressLike> {
20    /// A single address is admin
21    Monarchy {
22        /// The monarch's address
23        monarch: T,
24    },
25    /// Used when the account is a sub-account of another account.
26    SubAccount {
27        // Account address
28        account: T,
29    },
30    /// An external governance source. This could be a cw3 contract for instance
31    /// The `governance_address` will be the admin of the Account.
32    External {
33        /// The external contract address
34        governance_address: T,
35        /// Governance type used for doing extra off-chain queries depending on the type.
36        governance_type: String,
37    },
38    /// This account is linked to an NFT collection.
39    /// The owner of the specified token_id is the owner of the account
40    NFT {
41        collection_addr: T,
42        token_id: String,
43    },
44    /// Abstract account.
45    /// Admin actions have to be sent through signature bit flag
46    ///
47    /// More details: https://github.com/burnt-labs/abstract-account/blob/2c933a7b2a8dacc0ae5bf4344159a7d4ab080135/README.md
48    AbstractAccount {
49        /// Address of this abstract account
50        address: Addr,
51    },
52    /// Renounced account
53    /// This account no longer has an owner and cannot be used.
54    Renounced {},
55}
56
57/// Actions that can be taken to alter the contract's governance ownership
58#[cosmwasm_schema::cw_serde]
59pub enum GovAction {
60    /// Propose to transfer the contract's ownership to another account,
61    /// optionally with an expiry time.
62    ///
63    /// Can only be called by the contract's current owner.
64    ///
65    /// Any existing pending ownership transfer is overwritten.
66    TransferOwnership {
67        new_owner: GovernanceDetails<String>,
68        expiry: Option<Expiration>,
69    },
70
71    /// Accept the pending ownership transfer.
72    ///
73    /// Can only be called by the pending owner.
74    AcceptOwnership,
75
76    /// Give up the contract's ownership and the possibility of appointing
77    /// a new owner.
78    ///
79    /// Can only be invoked by the contract's current owner.
80    ///
81    /// Any existing pending ownership transfer is canceled.
82    RenounceOwnership,
83}
84
85impl GovernanceDetails<String> {
86    /// Verify the governance details and convert to `Self<Addr>`
87    pub fn verify(self, deps: Deps) -> Result<GovernanceDetails<Addr>, AbstractError> {
88        match self {
89            GovernanceDetails::Monarchy { monarch } => {
90                let addr = deps.api.addr_validate(&monarch)?;
91                Ok(GovernanceDetails::Monarchy { monarch: addr })
92            }
93            GovernanceDetails::SubAccount { account } => {
94                let account_addr = deps.api.addr_validate(&account)?;
95
96                let abstract_code_id = native_addrs::abstract_code_id(&deps.querier, account)?;
97                let registry_address = native_addrs::registry_address(deps, abstract_code_id)?;
98                let registry_address = deps.api.addr_humanize(&registry_address)?;
99
100                let account_id = ACCOUNT_ID.query(&deps.querier, account_addr.clone())?;
101                let base = registry::state::ACCOUNT_ADDRESSES.query(
102                    &deps.querier,
103                    registry_address,
104                    &account_id,
105                )?;
106                let Some(b) = base else {
107                    return Err(AbstractError::Std(cosmwasm_std::StdError::generic_err(
108                        format!(
109                            "Version control does not have account id of account {account_addr}"
110                        ),
111                    )));
112                };
113                if b.addr() == account_addr {
114                    Ok(GovernanceDetails::SubAccount {
115                        account: account_addr,
116                    })
117                } else {
118                    Err(AbstractError::Std(cosmwasm_std::StdError::generic_err(
119                        "Verification of sub-account failed, account has different account ids",
120                    )))
121                }
122            }
123            GovernanceDetails::External {
124                governance_address,
125                governance_type,
126            } => {
127                let addr = deps.api.addr_validate(&governance_address)?;
128
129                if governance_type.len() < MIN_GOV_TYPE_LENGTH {
130                    return Err(AbstractError::FormattingError {
131                        object: "governance type".into(),
132                        expected: "at least 3 characters".into(),
133                        actual: governance_type.len().to_string(),
134                    });
135                }
136                if governance_type.len() > MAX_GOV_TYPE_LENGTH {
137                    return Err(AbstractError::FormattingError {
138                        object: "governance type".into(),
139                        expected: "at most 64 characters".into(),
140                        actual: governance_type.len().to_string(),
141                    });
142                }
143                if governance_type.contains(|c: char| !c.is_ascii_alphanumeric() && c != '-') {
144                    return Err(AbstractError::FormattingError {
145                        object: "governance type".into(),
146                        expected: "alphanumeric characters and hyphens".into(),
147                        actual: governance_type,
148                    });
149                }
150
151                if governance_type != governance_type.to_lowercase() {
152                    return Err(AbstractError::FormattingError {
153                        object: "governance type".into(),
154                        expected: governance_type.to_ascii_lowercase(),
155                        actual: governance_type,
156                    });
157                }
158
159                Ok(GovernanceDetails::External {
160                    governance_address: addr,
161                    governance_type,
162                })
163            }
164            GovernanceDetails::Renounced {} => Ok(GovernanceDetails::Renounced {}),
165            GovernanceDetails::NFT {
166                collection_addr,
167                token_id,
168            } => Ok(GovernanceDetails::NFT {
169                collection_addr: deps.api.addr_validate(&collection_addr.to_string())?,
170                token_id,
171            }),
172            GovernanceDetails::AbstractAccount { address } => {
173                Ok(GovernanceDetails::AbstractAccount { address })
174            }
175        }
176    }
177}
178
179impl GovernanceDetails<Addr> {
180    /// Get the owner address from the governance details
181    pub fn owner_address(&self, querier: &QuerierWrapper) -> Option<Addr> {
182        match self {
183            GovernanceDetails::Monarchy { monarch } => Some(monarch.clone()),
184            GovernanceDetails::SubAccount { account } => Some(account.clone()),
185            GovernanceDetails::External {
186                governance_address, ..
187            } => Some(governance_address.clone()),
188            GovernanceDetails::Renounced {} => None,
189            GovernanceDetails::NFT {
190                collection_addr,
191                token_id,
192            } => {
193                let res: Option<cw721::OwnerOfResponse> = querier
194                    .query_wasm_smart(
195                        collection_addr,
196                        &cw721::Cw721QueryMsg::OwnerOf {
197                            token_id: token_id.to_string(),
198                            include_expired: None,
199                        },
200                    )
201                    .ok();
202                res.map(|owner_response| Addr::unchecked(owner_response.owner))
203            }
204            GovernanceDetails::AbstractAccount { address } => Some(address.to_owned()),
205        }
206    }
207}
208
209impl From<GovernanceDetails<Addr>> for GovernanceDetails<String> {
210    fn from(value: GovernanceDetails<Addr>) -> Self {
211        match value {
212            GovernanceDetails::Monarchy { monarch } => GovernanceDetails::Monarchy {
213                monarch: monarch.into_string(),
214            },
215            GovernanceDetails::SubAccount { account } => GovernanceDetails::SubAccount {
216                account: account.into_string(),
217            },
218            GovernanceDetails::External {
219                governance_address,
220                governance_type,
221            } => GovernanceDetails::External {
222                governance_address: governance_address.into_string(),
223                governance_type,
224            },
225            GovernanceDetails::Renounced {} => GovernanceDetails::Renounced {},
226            GovernanceDetails::NFT {
227                collection_addr,
228                token_id,
229            } => GovernanceDetails::NFT {
230                collection_addr: collection_addr.to_string(),
231                token_id,
232            },
233            GovernanceDetails::AbstractAccount { address } => {
234                GovernanceDetails::AbstractAccount { address }
235            }
236        }
237    }
238}
239
240impl<T: AddressLike> std::fmt::Display for GovernanceDetails<T> {
241    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242        let str = match self {
243            GovernanceDetails::Monarchy { .. } => "monarch",
244            GovernanceDetails::SubAccount { .. } => "sub-account",
245            GovernanceDetails::External {
246                governance_type, ..
247            } => governance_type.as_str(),
248            GovernanceDetails::Renounced {} => "renounced",
249            GovernanceDetails::NFT { .. } => "nft",
250            GovernanceDetails::AbstractAccount { .. } => "abstract-account",
251        };
252        write!(f, "{str}")
253    }
254}
255
256#[cosmwasm_schema::cw_serde]
257pub struct TopLevelOwnerResponse {
258    pub address: Addr,
259}
260
261#[cfg(test)]
262mod test {
263    #![allow(clippy::needless_borrows_for_generic_args)]
264    use super::*;
265
266    use cosmwasm_std::testing::mock_dependencies;
267
268    #[coverage_helper::test]
269    fn test_verify() {
270        let deps = mock_dependencies();
271        let owner = deps.api.addr_make("monarch");
272        let gov = GovernanceDetails::Monarchy {
273            monarch: owner.to_string(),
274        };
275        assert!(gov.verify(deps.as_ref()).is_ok());
276
277        let gov_addr = deps.api.addr_make("gov_addr");
278        let gov = GovernanceDetails::External {
279            governance_address: gov_addr.to_string(),
280            governance_type: "external-multisig".to_string(),
281        };
282        assert!(gov.verify(deps.as_ref()).is_ok());
283
284        let gov = GovernanceDetails::Monarchy {
285            monarch: "NOT_OK".to_string(),
286        };
287        assert!(gov.verify(deps.as_ref()).is_err());
288        let gov = GovernanceDetails::External {
289            governance_address: "gov_address".to_string(),
290            governance_type: "gov_type".to_string(),
291        };
292        // '_' not allowed
293        assert!(gov.verify(deps.as_ref()).is_err());
294
295        // too short
296        let gov_address = deps.api.addr_make("gov_address");
297        let gov = GovernanceDetails::External {
298            governance_address: gov_address.to_string(),
299            governance_type: "gov".to_string(),
300        };
301        assert!(gov.verify(deps.as_ref()).is_err());
302
303        // too long
304        let gov = GovernanceDetails::External {
305            governance_address: gov_address.to_string(),
306            governance_type: "a".repeat(190),
307        };
308        assert!(gov.verify(deps.as_ref()).is_err());
309
310        // invalid addr
311        let gov = GovernanceDetails::External {
312            governance_address: "NOT_OK".to_string(),
313            governance_type: "gov_type".to_string(),
314        };
315        assert!(gov.verify(deps.as_ref()).is_err());
316
317        // good nft
318        let collection_addr = deps.api.addr_make("collection_addr");
319        let gov = GovernanceDetails::NFT {
320            collection_addr: collection_addr.to_string(),
321            token_id: "1".to_string(),
322        };
323        assert!(gov.verify(deps.as_ref()).is_ok());
324    }
325}