1use anyhow::{bail, Result as AnyResult};
2use schemars::JsonSchema;
3use serde::de::DeserializeOwned;
4use serde::{Deserialize, Serialize};
5use std::cmp::max;
6use std::fmt::Debug;
7use std::ops::{Deref, DerefMut};
8use thiserror::Error;
9
10use cosmwasm_std::testing::{MockApi, MockQuerier, MockStorage};
11use cosmwasm_std::OwnedDeps;
12use std::marker::PhantomData;
13
14use cosmwasm_std::Order::Ascending;
15use cosmwasm_std::{
16 from_slice, to_binary, Addr, Api, Binary, BlockInfo, Coin, CustomQuery, Empty, Order, Querier,
17 QuerierResult, StdError, StdResult, Storage, Timestamp,
18};
19use cw_multi_test::{
20 App, AppResponse, BankKeeper, BankSudo, BasicAppBuilder, CosmosRouter, Executor, Module,
21 WasmKeeper, WasmSudo,
22};
23use cw_storage_plus::{Item, Map};
24
25use tg_bindings::{
26 Evidence, GovProposal, ListPrivilegedResponse, Privilege, PrivilegeChangeMsg, PrivilegeMsg,
27 TgradeMsg, TgradeQuery, TgradeSudoMsg, ValidatorDiff, ValidatorVote, ValidatorVoteResponse,
28};
29
30pub struct TgradeModule {}
31
32pub type Privileges = Vec<Privilege>;
33
34pub const BLOCK_TIME: u64 = 5;
37
38const PRIVILEGES: Map<&Addr, Privileges> = Map::new("privileges");
39const VOTES: Item<ValidatorVoteResponse> = Item::new("votes");
40const PINNED: Item<Vec<u64>> = Item::new("pinned");
41const PLANNED_UPGRADE: Item<UpgradePlan> = Item::new("planned_upgrade");
42const PARAMS: Map<String, String> = Map::new("params");
43
44const ADMIN_PRIVILEGES: &[Privilege] = &[
45 Privilege::GovProposalExecutor,
46 Privilege::Sudoer,
47 Privilege::TokenMinter,
48 Privilege::ConsensusParamChanger,
49];
50
51pub type TgradeDeps = OwnedDeps<MockStorage, MockApi, MockQuerier, TgradeQuery>;
52
53pub fn mock_deps_tgrade() -> TgradeDeps {
54 OwnedDeps {
55 storage: MockStorage::default(),
56 api: MockApi::default(),
57 querier: MockQuerier::default(),
58 custom_query_type: PhantomData,
59 }
60}
61
62impl TgradeModule {
63 pub fn set_owner(&self, storage: &mut dyn Storage, owner: &Addr) -> StdResult<()> {
66 PRIVILEGES.save(storage, owner, &ADMIN_PRIVILEGES.to_vec())?;
67 Ok(())
68 }
69
70 pub fn set_votes(&self, storage: &mut dyn Storage, votes: Vec<ValidatorVote>) -> StdResult<()> {
72 VOTES.save(storage, &ValidatorVoteResponse { votes })
73 }
74
75 pub fn is_pinned(&self, storage: &dyn Storage, code: u64) -> StdResult<bool> {
76 let pinned = PINNED.may_load(storage)?;
77 match pinned {
78 Some(pinned) => Ok(pinned.contains(&code)),
79 None => Ok(false),
80 }
81 }
82
83 pub fn upgrade_is_planned(&self, storage: &dyn Storage) -> StdResult<Option<UpgradePlan>> {
84 PLANNED_UPGRADE.may_load(storage)
85 }
86
87 pub fn get_params(&self, storage: &dyn Storage) -> StdResult<Vec<(String, String)>> {
88 PARAMS.range(storage, None, None, Ascending).collect()
89 }
90
91 fn require_privilege(
92 &self,
93 storage: &dyn Storage,
94 addr: &Addr,
95 required: Privilege,
96 ) -> AnyResult<()> {
97 let allowed = PRIVILEGES
98 .may_load(storage, addr)?
99 .unwrap_or_default()
100 .into_iter()
101 .any(|p| p == required);
102 if !allowed {
103 return Err(TgradeError::Unauthorized("Admin privileges required".to_owned()).into());
104 }
105 Ok(())
106 }
107}
108
109impl Module for TgradeModule {
110 type ExecT = TgradeMsg;
111 type QueryT = TgradeQuery;
112 type SudoT = Empty;
113
114 fn execute<ExecC, QueryC>(
115 &self,
116 api: &dyn Api,
117 storage: &mut dyn Storage,
118 router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
119 block: &BlockInfo,
120 sender: Addr,
121 msg: TgradeMsg,
122 ) -> AnyResult<AppResponse>
123 where
124 ExecC: Debug + Clone + PartialEq + JsonSchema + DeserializeOwned + 'static,
125 QueryC: CustomQuery + DeserializeOwned + 'static,
126 {
127 match msg {
128 TgradeMsg::Privilege(PrivilegeMsg::Request(add)) => {
129 if add == Privilege::ValidatorSetUpdater {
130 let validator_registered =
132 PRIVILEGES
133 .range(storage, None, None, Order::Ascending)
134 .fold(Ok(false), |val, item| match (val, item) {
135 (Err(e), _) => Err(e),
136 (_, Err(e)) => Err(e),
137 (Ok(found), Ok((_, privs))) => Ok(found
138 || privs.iter().any(|p| *p == Privilege::ValidatorSetUpdater)),
139 })?;
140 if validator_registered {
141 bail!(
142 "One ValidatorSetUpdater already registered, cannot register a second"
143 );
144 }
145 }
146
147 let mut powers = PRIVILEGES.may_load(storage, &sender)?.ok_or_else(|| {
149 TgradeError::Unauthorized("Admin privileges required".to_owned())
150 })?;
151 powers.push(add);
152 PRIVILEGES.save(storage, &sender, &powers)?;
153 Ok(AppResponse::default())
154 }
155 TgradeMsg::Privilege(PrivilegeMsg::Release(remove)) => {
156 let powers = PRIVILEGES.may_load(storage, &sender)?;
157 if let Some(powers) = powers {
158 let updated = powers.into_iter().filter(|p| *p != remove).collect();
159 PRIVILEGES.save(storage, &sender, &updated)?;
160 }
161 Ok(AppResponse::default())
162 }
163 TgradeMsg::WasmSudo { contract_addr, msg } => {
164 self.require_privilege(storage, &sender, Privilege::Sudoer)?;
165 let contract_addr = api.addr_validate(&contract_addr)?;
166 let sudo = WasmSudo { contract_addr, msg };
167 router.sudo(api, storage, block, sudo.into())
168 }
169 TgradeMsg::ConsensusParams(_) => {
170 self.require_privilege(storage, &sender, Privilege::ConsensusParamChanger)?;
172 Ok(AppResponse::default())
173 }
174 TgradeMsg::ExecuteGovProposal {
175 title: _,
176 description: _,
177 proposal,
178 } => {
179 self.require_privilege(storage, &sender, Privilege::GovProposalExecutor)?;
180 match proposal {
181 GovProposal::PromoteToPrivilegedContract { contract } => {
182 let contract_addr = api.addr_validate(&contract)?;
184 PRIVILEGES.update(storage, &contract_addr, |current| -> StdResult<_> {
185 Ok(current.unwrap_or_default())
187 })?;
188
189 let msg = to_binary(&TgradeSudoMsg::<Empty>::PrivilegeChange(
191 PrivilegeChangeMsg::Promoted {},
192 ))?;
193 let sudo = WasmSudo { contract_addr, msg };
194 router.sudo(api, storage, block, sudo.into())
195 }
196 GovProposal::DemotePrivilegedContract { contract } => {
197 let contract_addr = api.addr_validate(&contract)?;
199 PRIVILEGES.remove(storage, &contract_addr);
200
201 let msg = to_binary(&TgradeSudoMsg::<Empty>::PrivilegeChange(
203 PrivilegeChangeMsg::Demoted {},
204 ))?;
205 let sudo = WasmSudo { contract_addr, msg };
206 router.sudo(api, storage, block, sudo.into())
207 }
208 GovProposal::PinCodes { code_ids } => {
209 let mut pinned = PINNED.may_load(storage)?.unwrap_or_default();
210 pinned.extend(code_ids);
211 pinned.sort_unstable();
212 pinned.dedup();
213 PINNED.save(storage, &pinned)?;
214
215 Ok(AppResponse::default())
216 }
217 GovProposal::UnpinCodes { code_ids } => {
218 let pinned = PINNED
219 .may_load(storage)?
220 .unwrap_or_default()
221 .into_iter()
222 .filter(|id| !code_ids.contains(id))
223 .collect();
224 PINNED.save(storage, &pinned)?;
225
226 Ok(AppResponse::default())
227 }
228 GovProposal::RegisterUpgrade { name, height, info } => {
229 match PLANNED_UPGRADE.may_load(storage)? {
230 Some(_) => Err(anyhow::anyhow!("an upgrade plan already exists")),
231 None => {
232 PLANNED_UPGRADE
233 .save(storage, &UpgradePlan::new(name, height, info))?;
234 Ok(AppResponse::default())
235 }
236 }
237 }
238 GovProposal::CancelUpgrade {} => match PLANNED_UPGRADE.may_load(storage)? {
239 None => Err(anyhow::anyhow!("an upgrade plan doesn't exist")),
240 Some(_) => {
241 PLANNED_UPGRADE.remove(storage);
242 Ok(AppResponse::default())
243 }
244 },
245 GovProposal::InstantiateContract { .. } => {
247 bail!("GovProposal::InstantiateContract not implemented")
248 }
249 GovProposal::MigrateContract { .. } => {
251 bail!("GovProposal::MigrateContract not implemented")
252 }
253 GovProposal::ChangeParams(params) => {
254 let mut sorted_params = params.clone();
255 sorted_params.sort_unstable();
256 sorted_params.dedup_by(|a, b| a.subspace == b.subspace && a.key == b.key);
257 if sorted_params.len() < params.len() {
258 return Err(anyhow::anyhow!(
259 "duplicate subspace + keys in params vector"
260 ));
261 }
262 for p in params {
263 if p.subspace.is_empty() {
264 return Err(anyhow::anyhow!("empty subspace key"));
265 }
266 if p.key.is_empty() {
267 return Err(anyhow::anyhow!("empty key key"));
268 }
269 PARAMS.save(storage, format!("{}/{}", p.subspace, p.key), &p.value)?;
270 }
271 Ok(AppResponse::default())
272 }
273 _ => Ok(AppResponse::default()),
275 }
276 }
277 TgradeMsg::MintTokens {
278 denom,
279 amount,
280 recipient,
281 } => {
282 self.require_privilege(storage, &sender, Privilege::TokenMinter)?;
283 let mint = BankSudo::Mint {
284 to_address: recipient,
285 amount: vec![Coin { denom, amount }],
286 };
287 router.sudo(api, storage, block, mint.into())
288 }
289 TgradeMsg::Delegate {
290 funds: _funds,
291 staker: _staker,
292 } => {
293 self.require_privilege(storage, &sender, Privilege::Delegator)?;
294 Ok(AppResponse::default())
296 }
297 TgradeMsg::Undelegate {
298 funds: _funds,
299 recipient: _recipient,
300 } => {
301 self.require_privilege(storage, &sender, Privilege::Delegator)?;
302 Ok(AppResponse::default())
304 }
305 }
306 }
307
308 fn sudo<ExecC, QueryC>(
309 &self,
310 _api: &dyn Api,
311 _storage: &mut dyn Storage,
312 _router: &dyn CosmosRouter<ExecC = ExecC, QueryC = QueryC>,
313 _block: &BlockInfo,
314 _msg: Self::SudoT,
315 ) -> AnyResult<AppResponse>
316 where
317 ExecC: Debug + Clone + PartialEq + JsonSchema + DeserializeOwned + 'static,
318 QueryC: CustomQuery + DeserializeOwned + 'static,
319 {
320 bail!("sudo not implemented for TgradeModule")
321 }
322
323 fn query(
324 &self,
325 _api: &dyn Api,
326 storage: &dyn Storage,
327 _querier: &dyn Querier,
328 _block: &BlockInfo,
329 request: TgradeQuery,
330 ) -> anyhow::Result<Binary> {
331 match request {
332 TgradeQuery::ListPrivileged(check) => {
333 let privileged = PRIVILEGES
335 .range(storage, None, None, Order::Ascending)
336 .filter_map(|r| {
337 r.map(|(addr, privs)| match privs.iter().any(|p| *p == check) {
338 true => Some(addr),
339 false => None,
340 })
341 .transpose()
342 })
343 .collect::<StdResult<Vec<_>>>()?;
344 Ok(to_binary(&ListPrivilegedResponse { privileged })?)
345 }
346 TgradeQuery::ValidatorVotes {} => {
347 let res = VOTES.may_load(storage)?.unwrap_or_default();
348 Ok(to_binary(&res)?)
349 }
350 }
351 }
352}
353
354#[derive(Error, Debug, PartialEq)]
355pub enum TgradeError {
356 #[error("{0}")]
357 Std(#[from] StdError),
358
359 #[error("Unauthorized: {0}")]
360 Unauthorized(String),
361}
362
363pub type TgradeAppWrapped =
364 App<BankKeeper, MockApi, MockStorage, TgradeModule, WasmKeeper<TgradeMsg, TgradeQuery>>;
365
366pub struct TgradeApp(TgradeAppWrapped);
367
368impl Deref for TgradeApp {
369 type Target = TgradeAppWrapped;
370
371 fn deref(&self) -> &Self::Target {
372 &self.0
373 }
374}
375
376impl DerefMut for TgradeApp {
377 fn deref_mut(&mut self) -> &mut Self::Target {
378 &mut self.0
379 }
380}
381
382impl Querier for TgradeApp {
383 fn raw_query(&self, bin_request: &[u8]) -> QuerierResult {
384 self.0.raw_query(bin_request)
385 }
386}
387
388impl TgradeApp {
389 pub fn new(owner: &str) -> Self {
390 let owner = Addr::unchecked(owner);
391 Self(
392 BasicAppBuilder::<TgradeMsg, TgradeQuery>::new_custom()
393 .with_custom(TgradeModule {})
394 .build(|router, _, storage| {
395 router.custom.set_owner(storage, &owner).unwrap();
396 }),
397 )
398 }
399
400 pub fn new_genesis(owner: &str) -> Self {
401 let owner = Addr::unchecked(owner);
402 let block_info = BlockInfo {
403 height: 0,
404 time: Timestamp::from_nanos(1_571_797_419_879_305_533),
405 chain_id: "tgrade-testnet-14002".to_owned(),
406 };
407
408 Self(
409 BasicAppBuilder::<TgradeMsg, TgradeQuery>::new_custom()
410 .with_custom(TgradeModule {})
411 .with_block(block_info)
412 .build(|router, _, storage| {
413 router.custom.set_owner(storage, &owner).unwrap();
414 }),
415 )
416 }
417
418 pub fn block_info(&self) -> BlockInfo {
419 self.0.block_info()
420 }
421
422 pub fn promote(&mut self, owner: &str, contract: &str) -> AnyResult<AppResponse> {
423 let msg = TgradeMsg::ExecuteGovProposal {
424 title: "Promote Contract".to_string(),
425 description: "Promote Contract".to_string(),
426 proposal: GovProposal::PromoteToPrivilegedContract {
427 contract: contract.to_string(),
428 },
429 };
430 self.execute(Addr::unchecked(owner), msg.into())
431 }
432
433 pub fn back_to_genesis(&mut self) {
435 self.update_block(|block| {
436 block.time = block.time.minus_seconds(BLOCK_TIME * block.height);
437 block.height = 0;
438 });
439 }
440
441 pub fn advance_blocks(&mut self, blocks: u64) {
444 self.update_block(|block| {
445 block.time = block.time.plus_seconds(BLOCK_TIME * blocks);
446 block.height += blocks;
447 });
448 }
449
450 pub fn advance_seconds(&mut self, seconds: u64) {
453 self.update_block(|block| {
454 block.time = block.time.plus_seconds(seconds);
455 block.height += max(1, seconds / BLOCK_TIME);
456 });
457 }
458
459 pub fn next_block(&mut self) -> AnyResult<Option<ValidatorDiff>> {
466 let (_, diff) = self.end_block()?;
467 self.update_block(|block| {
468 block.time = block.time.plus_seconds(BLOCK_TIME);
469 block.height += 1;
470 });
471 self.begin_block(vec![])?;
472 Ok(diff)
473 }
474
475 pub fn with_privilege(&self, requested: Privilege) -> AnyResult<Vec<Addr>> {
477 let ListPrivilegedResponse { privileged } = self
478 .wrap()
479 .query(&TgradeQuery::ListPrivileged(requested).into())?;
480 Ok(privileged)
481 }
482
483 fn valset_updater(&self) -> AnyResult<Option<Addr>> {
484 let mut updaters = self.with_privilege(Privilege::ValidatorSetUpdater)?;
485 if updaters.len() > 1 {
486 bail!("Multiple ValidatorSetUpdater registered")
487 } else {
488 Ok(updaters.pop())
489 }
490 }
491
492 pub fn begin_block(&mut self, evidence: Vec<Evidence>) -> AnyResult<Vec<AppResponse>> {
495 let to_call = self.with_privilege(Privilege::BeginBlocker)?;
496 let msg = TgradeSudoMsg::<Empty>::BeginBlock { evidence };
497 let res = to_call
498 .into_iter()
499 .map(|contract| self.wasm_sudo(contract, &msg))
500 .collect::<AnyResult<_>>()?;
501 Ok(res)
502 }
503
504 pub fn end_block(&mut self) -> AnyResult<(Vec<AppResponse>, Option<ValidatorDiff>)> {
508 let to_call = self.with_privilege(Privilege::EndBlocker)?;
509 let msg = TgradeSudoMsg::<Empty>::EndBlock {};
510
511 let mut res: Vec<AppResponse> = to_call
512 .into_iter()
513 .map(|contract| self.wasm_sudo(contract, &msg))
514 .collect::<AnyResult<_>>()?;
515
516 let diff = match self.valset_updater()? {
517 Some(contract) => {
518 let mut r =
519 self.wasm_sudo(contract, &TgradeSudoMsg::<Empty>::EndWithValidatorUpdate {})?;
520 let data = r.data.take();
521 res.push(r);
522 match data {
523 Some(b) if !b.is_empty() => Some(from_slice(&b)?),
524 _ => None,
525 }
526 }
527 None => None,
528 };
529 Ok((res, diff))
530 }
531}
532
533#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
534pub struct UpgradePlan {
535 name: String,
536 height: u64,
537 info: String,
538}
539
540impl UpgradePlan {
541 pub fn new(name: impl ToString, height: u64, info: impl ToString) -> Self {
542 Self {
543 name: name.to_string(),
544 height,
545 info: info.to_string(),
546 }
547 }
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553 use cosmwasm_std::coin;
554 use cw_multi_test::Executor;
555
556 #[test]
557 fn init_and_owner_mints_tokens() {
558 let owner = Addr::unchecked("govner");
559 let rcpt = Addr::unchecked("townies");
560
561 let mut app = TgradeApp::new(owner.as_str());
562
563 let start = app.wrap().query_all_balances(rcpt.as_str()).unwrap();
565 assert_eq!(start, vec![]);
566
567 let mintable = coin(123456, "shilling");
569 let msg = TgradeMsg::MintTokens {
570 denom: mintable.denom.clone(),
571 amount: mintable.amount,
572 recipient: rcpt.to_string(),
573 };
574
575 let _ = app.execute(rcpt.clone(), msg.clone().into()).unwrap_err();
577
578 app.execute(owner, msg.into()).unwrap();
580
581 let end = app
583 .wrap()
584 .query_balance(rcpt.as_str(), &mintable.denom)
585 .unwrap();
586 assert_eq!(end, mintable);
587 }
588
589 }