abstract_std/objects/
gov_type.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
//! # Governance structure object

use crate::{account::state::ACCOUNT_ID, native_addrs, registry};
use cosmwasm_std::{Addr, Deps, QuerierWrapper};
use cw_address_like::AddressLike;
use cw_utils::Expiration;

use crate::AbstractError;

use super::ownership::cw721;

const MIN_GOV_TYPE_LENGTH: usize = 4;
const MAX_GOV_TYPE_LENGTH: usize = 64;

/// Governance types
#[cosmwasm_schema::cw_serde]
#[derive(Eq)]
#[non_exhaustive]
pub enum GovernanceDetails<T: AddressLike> {
    /// A single address is admin
    Monarchy {
        /// The monarch's address
        monarch: T,
    },
    /// Used when the account is a sub-account of another account.
    SubAccount {
        // Account address
        account: T,
    },
    /// An external governance source. This could be a cw3 contract for instance
    /// The `governance_address` will be the admin of the Account.
    External {
        /// The external contract address
        governance_address: T,
        /// Governance type used for doing extra off-chain queries depending on the type.
        governance_type: String,
    },
    /// This account is linked to an NFT collection.
    /// The owner of the specified token_id is the owner of the account
    NFT {
        collection_addr: T,
        token_id: String,
    },
    /// Abstract account.
    /// Admin actions have to be sent through signature bit flag
    ///
    /// More details: https://github.com/burnt-labs/abstract-account/blob/2c933a7b2a8dacc0ae5bf4344159a7d4ab080135/README.md
    AbstractAccount {
        /// Address of this abstract account
        address: Addr,
    },
    /// Renounced account
    /// This account no longer has an owner and cannot be used.
    Renounced {},
}

/// Actions that can be taken to alter the contract's governance ownership
#[cosmwasm_schema::cw_serde]
pub enum GovAction {
    /// Propose to transfer the contract's ownership to another account,
    /// optionally with an expiry time.
    ///
    /// Can only be called by the contract's current owner.
    ///
    /// Any existing pending ownership transfer is overwritten.
    TransferOwnership {
        new_owner: GovernanceDetails<String>,
        expiry: Option<Expiration>,
    },

    /// Accept the pending ownership transfer.
    ///
    /// Can only be called by the pending owner.
    AcceptOwnership,

    /// Give up the contract's ownership and the possibility of appointing
    /// a new owner.
    ///
    /// Can only be invoked by the contract's current owner.
    ///
    /// Any existing pending ownership transfer is canceled.
    RenounceOwnership,
}

impl GovernanceDetails<String> {
    /// Verify the governance details and convert to `Self<Addr>`
    pub fn verify(self, deps: Deps) -> Result<GovernanceDetails<Addr>, AbstractError> {
        match self {
            GovernanceDetails::Monarchy { monarch } => {
                let addr = deps.api.addr_validate(&monarch)?;
                Ok(GovernanceDetails::Monarchy { monarch: addr })
            }
            GovernanceDetails::SubAccount { account } => {
                let account_addr = deps.api.addr_validate(&account)?;

                let abstract_code_id = native_addrs::abstract_code_id(&deps.querier, account)?;
                let registry_address = native_addrs::registry_address(deps, abstract_code_id)?;
                let registry_address = deps.api.addr_humanize(&registry_address)?;

                let account_id = ACCOUNT_ID.query(&deps.querier, account_addr.clone())?;
                let base = registry::state::ACCOUNT_ADDRESSES.query(
                    &deps.querier,
                    registry_address,
                    &account_id,
                )?;
                let Some(b) = base else {
                    return Err(AbstractError::Std(cosmwasm_std::StdError::generic_err(
                        format!(
                            "Version control does not have account id of account {account_addr}"
                        ),
                    )));
                };
                if b.addr() == account_addr {
                    Ok(GovernanceDetails::SubAccount {
                        account: account_addr,
                    })
                } else {
                    Err(AbstractError::Std(cosmwasm_std::StdError::generic_err(
                        "Verification of sub-account failed, account has different account ids",
                    )))
                }
            }
            GovernanceDetails::External {
                governance_address,
                governance_type,
            } => {
                let addr = deps.api.addr_validate(&governance_address)?;

                if governance_type.len() < MIN_GOV_TYPE_LENGTH {
                    return Err(AbstractError::FormattingError {
                        object: "governance type".into(),
                        expected: "at least 3 characters".into(),
                        actual: governance_type.len().to_string(),
                    });
                }
                if governance_type.len() > MAX_GOV_TYPE_LENGTH {
                    return Err(AbstractError::FormattingError {
                        object: "governance type".into(),
                        expected: "at most 64 characters".into(),
                        actual: governance_type.len().to_string(),
                    });
                }
                if governance_type.contains(|c: char| !c.is_ascii_alphanumeric() && c != '-') {
                    return Err(AbstractError::FormattingError {
                        object: "governance type".into(),
                        expected: "alphanumeric characters and hyphens".into(),
                        actual: governance_type,
                    });
                }

                if governance_type != governance_type.to_lowercase() {
                    return Err(AbstractError::FormattingError {
                        object: "governance type".into(),
                        expected: governance_type.to_ascii_lowercase(),
                        actual: governance_type,
                    });
                }

                Ok(GovernanceDetails::External {
                    governance_address: addr,
                    governance_type,
                })
            }
            GovernanceDetails::Renounced {} => Ok(GovernanceDetails::Renounced {}),
            GovernanceDetails::NFT {
                collection_addr,
                token_id,
            } => Ok(GovernanceDetails::NFT {
                collection_addr: deps.api.addr_validate(&collection_addr.to_string())?,
                token_id,
            }),
            GovernanceDetails::AbstractAccount { address } => {
                Ok(GovernanceDetails::AbstractAccount { address })
            }
        }
    }
}

impl GovernanceDetails<Addr> {
    /// Get the owner address from the governance details
    pub fn owner_address(&self, querier: &QuerierWrapper) -> Option<Addr> {
        match self {
            GovernanceDetails::Monarchy { monarch } => Some(monarch.clone()),
            GovernanceDetails::SubAccount { account } => Some(account.clone()),
            GovernanceDetails::External {
                governance_address, ..
            } => Some(governance_address.clone()),
            GovernanceDetails::Renounced {} => None,
            GovernanceDetails::NFT {
                collection_addr,
                token_id,
            } => {
                let res: Option<cw721::OwnerOfResponse> = querier
                    .query_wasm_smart(
                        collection_addr,
                        &cw721::Cw721QueryMsg::OwnerOf {
                            token_id: token_id.to_string(),
                            include_expired: None,
                        },
                    )
                    .ok();
                res.map(|owner_response| Addr::unchecked(owner_response.owner))
            }
            GovernanceDetails::AbstractAccount { address } => Some(address.to_owned()),
        }
    }
}

impl From<GovernanceDetails<Addr>> for GovernanceDetails<String> {
    fn from(value: GovernanceDetails<Addr>) -> Self {
        match value {
            GovernanceDetails::Monarchy { monarch } => GovernanceDetails::Monarchy {
                monarch: monarch.into_string(),
            },
            GovernanceDetails::SubAccount { account } => GovernanceDetails::SubAccount {
                account: account.into_string(),
            },
            GovernanceDetails::External {
                governance_address,
                governance_type,
            } => GovernanceDetails::External {
                governance_address: governance_address.into_string(),
                governance_type,
            },
            GovernanceDetails::Renounced {} => GovernanceDetails::Renounced {},
            GovernanceDetails::NFT {
                collection_addr,
                token_id,
            } => GovernanceDetails::NFT {
                collection_addr: collection_addr.to_string(),
                token_id,
            },
            GovernanceDetails::AbstractAccount { address } => {
                GovernanceDetails::AbstractAccount { address }
            }
        }
    }
}

impl<T: AddressLike> std::fmt::Display for GovernanceDetails<T> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let str = match self {
            GovernanceDetails::Monarchy { .. } => "monarch",
            GovernanceDetails::SubAccount { .. } => "sub-account",
            GovernanceDetails::External {
                governance_type, ..
            } => governance_type.as_str(),
            GovernanceDetails::Renounced {} => "renounced",
            GovernanceDetails::NFT { .. } => "nft",
            GovernanceDetails::AbstractAccount { .. } => "abstract-account",
        };
        write!(f, "{str}")
    }
}

#[cosmwasm_schema::cw_serde]
pub struct TopLevelOwnerResponse {
    pub address: Addr,
}

#[cfg(test)]
mod test {
    #![allow(clippy::needless_borrows_for_generic_args)]
    use super::*;

    use cosmwasm_std::testing::mock_dependencies;

    #[coverage_helper::test]
    fn test_verify() {
        let deps = mock_dependencies();
        let owner = deps.api.addr_make("monarch");
        let gov = GovernanceDetails::Monarchy {
            monarch: owner.to_string(),
        };
        assert!(gov.verify(deps.as_ref()).is_ok());

        let gov_addr = deps.api.addr_make("gov_addr");
        let gov = GovernanceDetails::External {
            governance_address: gov_addr.to_string(),
            governance_type: "external-multisig".to_string(),
        };
        assert!(gov.verify(deps.as_ref()).is_ok());

        let gov = GovernanceDetails::Monarchy {
            monarch: "NOT_OK".to_string(),
        };
        assert!(gov.verify(deps.as_ref()).is_err());
        let gov = GovernanceDetails::External {
            governance_address: "gov_address".to_string(),
            governance_type: "gov_type".to_string(),
        };
        // '_' not allowed
        assert!(gov.verify(deps.as_ref()).is_err());

        // too short
        let gov_address = deps.api.addr_make("gov_address");
        let gov = GovernanceDetails::External {
            governance_address: gov_address.to_string(),
            governance_type: "gov".to_string(),
        };
        assert!(gov.verify(deps.as_ref()).is_err());

        // too long
        let gov = GovernanceDetails::External {
            governance_address: gov_address.to_string(),
            governance_type: "a".repeat(190),
        };
        assert!(gov.verify(deps.as_ref()).is_err());

        // invalid addr
        let gov = GovernanceDetails::External {
            governance_address: "NOT_OK".to_string(),
            governance_type: "gov_type".to_string(),
        };
        assert!(gov.verify(deps.as_ref()).is_err());

        // good nft
        let collection_addr = deps.api.addr_make("collection_addr");
        let gov = GovernanceDetails::NFT {
            collection_addr: collection_addr.to_string(),
            token_id: "1".to_string(),
        };
        assert!(gov.verify(deps.as_ref()).is_ok());
    }
}