diff --git a/Cargo.lock b/Cargo.lock index f5bfa6d..3286e04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -541,6 +541,7 @@ dependencies = [ "cw-it", "cw-storage-plus", "cw-vault-standard", + "cw-vault-standard-test-helpers", "cw2", "mars-owner", "osmosis-std", @@ -585,6 +586,16 @@ dependencies = [ "serde", ] +[[package]] +name = "cw-vault-standard-test-helpers" +version = "0.3.2" +dependencies = [ + "cosmwasm-std", + "cw-it", + "cw-utils", + "cw-vault-standard", +] + [[package]] name = "cw2" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index aca0c46..d34bf95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "cw-vault-standard", + "test-helpers", "mock-vault", ] @@ -25,9 +26,10 @@ cw2 = "1.1.0" mars-owner = "2.0.0" osmosis-std = "0.16.1" cw-vault-standard = { path = "./cw-vault-standard" } +cw-vault-standard-test-helpers = { path = "./test-helpers" } # dev dependencies -cw-it = "0.1.0" +cw-it = "0.1" proptest = "1.2.0" diff --git a/mock-vault/Cargo.toml b/mock-vault/Cargo.toml index 8961c41..7977bcb 100644 --- a/mock-vault/Cargo.toml +++ b/mock-vault/Cargo.toml @@ -28,6 +28,7 @@ cw-vault-standard = { workspace = true } cw-storage-plus = { workspace = true } cosmwasm-schema = { workspace = true } cw-it = { workspace = true, features = ["multi-test"] } +cw-vault-standard-test-helpers = { workspace = true } [dev-dependencies] proptest = { workspace = true } diff --git a/mock-vault/src/test_helpers.rs b/mock-vault/src/test_helpers.rs index dc6a6b2..f1497ed 100644 --- a/mock-vault/src/test_helpers.rs +++ b/mock-vault/src/test_helpers.rs @@ -8,6 +8,7 @@ use cw_it::robot::TestRobot; use cw_it::test_tube::{Account, Module, SigningAccount, Wasm}; use cw_it::traits::CwItRunner; use cw_it::{Artifact, ContractType, TestRunner}; +use cw_vault_standard_test_helpers::traits::CwVaultStandardRobot; pub const MOCK_VAULT_TOKEN_SUBDENOM: &str = "vault-token"; @@ -70,44 +71,32 @@ pub fn assert_almost_eq(left: Decimal, right: Decimal, max_rel_diff: &str) { } } -/// A trait implementing common methods for testing vault contracts. -pub trait VaultRobot<'a, R>: TestRobot<'a, R> -where - R: CwItRunner<'a> + 'a, -{ - /// Create a new default instance of the robot. - fn default_vault_robot( - runner: &'a R, - admin: &'a SigningAccount, - base_token: String, - vault_token: String, - vault_addr: String, - ) -> DefaultVaultRobot<'a, R> { - DefaultVaultRobot { - runner, - admin, - base_token, - vault_token, - vault_addr, - } - } - - /// Returns the base token. - fn base_token(&self) -> &str; +/// A simple testing robot for testing vault contracts. +pub struct MockVaultRobot<'a, R: CwItRunner<'a>> { + pub runner: &'a R, + pub admin: &'a SigningAccount, + pub vault_addr: String, +} - /// Returns the vault token. - fn vault_token(&self) -> &str; +impl<'a, R: CwItRunner<'a>> CwVaultStandardRobot<'a, R> for MockVaultRobot<'a, R> { + fn vault_addr(&self) -> String { + self.vault_addr.clone() + } - /// Returns the vault address. - fn vault_addr(&self) -> &str; + fn query_base_token_balance(&self, address: impl Into) -> Uint128 { + let base_token_denom = self.base_token(); + self.query_native_token_balance(address, base_token_denom) + } +} +impl<'a, R: CwItRunner<'a>> MockVaultRobot<'a, R> { /// Uploads and instantiates the vault contract and returns a new instance of the robot. - fn instantiate( + pub fn instantiate( runner: &'a R, admin: &'a SigningAccount, base_token: &str, denom_creation_fee: Option, - ) -> DefaultVaultRobot<'a, R> + ) -> MockVaultRobot<'a, R> where Self: Sized, { @@ -132,104 +121,16 @@ where .data .address; - let vault_token = format!("factory/{}/{}", vault_addr, MOCK_VAULT_TOKEN_SUBDENOM); - - Self::default_vault_robot( + MockVaultRobot { runner, admin, - base_token.to_string(), - vault_token, vault_addr, - ) - } - - /// Deposit base tokens into the vault and return a reference to the robot. - fn deposit_to_vault(&self, amount: impl Into, signer: &SigningAccount) -> &Self { - let amount: Uint128 = amount.into(); - - let msg = crate::msg::ExecuteMsg::Deposit { - amount, - recipient: None, - }; - self.wasm() - .execute( - self.vault_addr(), - &msg, - &[coin(amount.u128(), self.base_token())], - signer, - ) - .unwrap(); - - self - } - - /// Deposit base tokens into the vault without filling the native token funds field and return - /// a reference to the robot. This is useful for depositing cw20 tokens. - fn deposit_cw20_to_vault(&self, amount: impl Into, signer: &SigningAccount) -> &Self { - let amount: Uint128 = amount.into(); - - let msg = crate::msg::ExecuteMsg::Deposit { - amount, - recipient: None, - }; - self.wasm() - .execute(self.vault_addr(), &msg, &[], signer) - .unwrap(); - - self - } - - /// Redeem vault tokens from the vault and return a reference to the robot. - fn redeem_from_vault(&self, amount: impl Into, signer: &SigningAccount) -> &Self { - let amount: Uint128 = amount.into(); - - let msg = crate::msg::ExecuteMsg::Redeem { - amount, - recipient: None, - }; - self.wasm() - .execute( - self.vault_addr(), - &msg, - &[coin(amount.u128(), self.vault_token())], - signer, - ) - .unwrap(); - - self - } - - /// Query the vault token balance of the given account. - fn query_vault_token_balance(&self, account: impl Into) -> Uint128 { - self.query_native_token_balance(account, self.vault_token()) + } } } -/// A simple testing robot for testing vault contracts. -pub struct DefaultVaultRobot<'a, R: CwItRunner<'a>> { - pub runner: &'a R, - pub admin: &'a SigningAccount, - pub vault_addr: String, - pub base_token: String, - pub vault_token: String, -} - -impl<'a, R: CwItRunner<'a>> TestRobot<'a, R> for DefaultVaultRobot<'a, R> { +impl<'a, R: CwItRunner<'a>> TestRobot<'a, R> for MockVaultRobot<'a, R> { fn runner(&self) -> &'a R { self.runner } } - -impl<'a, R: CwItRunner<'a>> VaultRobot<'a, R> for DefaultVaultRobot<'a, R> { - fn base_token(&self) -> &str { - &self.base_token - } - - fn vault_token(&self) -> &str { - &self.vault_token - } - - fn vault_addr(&self) -> &str { - &self.vault_addr - } -} diff --git a/mock-vault/tests/property_tests.rs b/mock-vault/tests/property_tests.rs index c3b1196..8e51c39 100644 --- a/mock-vault/tests/property_tests.rs +++ b/mock-vault/tests/property_tests.rs @@ -1,10 +1,9 @@ use cosmwasm_std::coin; use cosmwasm_std::Decimal; -use cw_it::robot::TestRobot; use cw_it::test_tube::Account; use cw_it::traits::CwItRunner; use cw_mock_vault::test_helpers; -use cw_mock_vault::test_helpers::VaultRobot; +use cw_vault_standard_test_helpers::traits::CwVaultStandardRobot; use proptest::prelude::*; use proptest::proptest; @@ -30,17 +29,17 @@ proptest! { let admin = &accs[0]; let user1 = &accs[1]; let user2 = &accs[2]; - let robot = test_helpers::DefaultVaultRobot::instantiate(&runner, admin, "uosmo", Some(coin(10000000, "uosmo"))); + let base_token = "uosmo"; + let robot = test_helpers::MockVaultRobot::instantiate(&runner, admin, base_token, Some(coin(10000000, "uosmo"))); if init_amount != 0 { robot - .send_native_tokens(admin, &robot.vault_addr, init_amount, "uosmo") - .deposit_to_vault(init_amount, admin); + .deposit(init_amount, None, &[coin(init_amount, base_token)], &admin); } robot - .deposit_to_vault(amount1, user1) - .deposit_to_vault(amount2, user2); + .deposit(amount1, None, &[coin(amount1, base_token)], user1) + .deposit(amount2, None, &[coin(amount2, base_token)], user2); let user1_vault_token_balance = robot.query_vault_token_balance(user1.address()); let user2_vault_token_balance = robot.query_vault_token_balance(user2.address()); diff --git a/test-helpers/Cargo.toml b/test-helpers/Cargo.toml new file mode 100644 index 0000000..74d6d35 --- /dev/null +++ b/test-helpers/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "cw-vault-standard-test-helpers" +version = { workspace = true } +edition = "2018" + +[features] +default = ["lockup", "force-unlock"] +lockup = ["cw-vault-standard/lockup"] +force-unlock = ["cw-vault-standard/force-unlock"] + + + +[dependencies] +cosmwasm-std = { workspace = true } +cw-vault-standard = { workspace = true } +cw-utils = { workspace = true } +cw-it = { workspace = true } \ No newline at end of file diff --git a/test-helpers/src/lib.rs b/test-helpers/src/lib.rs new file mode 100644 index 0000000..f6ac8fc --- /dev/null +++ b/test-helpers/src/lib.rs @@ -0,0 +1 @@ +pub mod traits; diff --git a/test-helpers/src/traits/force_unlock.rs b/test-helpers/src/traits/force_unlock.rs new file mode 100644 index 0000000..1970506 --- /dev/null +++ b/test-helpers/src/traits/force_unlock.rs @@ -0,0 +1,78 @@ +use cosmwasm_std::Uint128; +use cw_it::test_tube::{Runner, SigningAccount}; + +use cw_vault_standard::extensions::force_unlock::ForceUnlockExecuteMsg; +use cw_vault_standard::msg::VaultStandardExecuteMsg as ExecuteMsg; +use cw_vault_standard::ExtensionExecuteMsg; + +use super::CwVaultStandardRobot; + +pub trait ForceUnlockVaultRobot<'a, R: Runner<'a> + 'a>: CwVaultStandardRobot<'a, R> { + fn force_redeem( + &self, + amount: impl Into, + recipient: Option, + signer: &SigningAccount, + ) -> &Self { + self.wasm() + .execute( + &self.vault_addr(), + &ExecuteMsg::VaultExtension(ExtensionExecuteMsg::ForceUnlock( + ForceUnlockExecuteMsg::ForceRedeem { + recipient, + amount: amount.into(), + }, + )), + &[], + signer, + ) + .unwrap(); + self + } + + fn force_withdraw_unlocking( + &self, + lockup_id: u64, + amount: Option>, + recipient: Option, + signer: &SigningAccount, + ) -> &Self { + self.wasm() + .execute( + &self.vault_addr(), + &ExecuteMsg::VaultExtension(ExtensionExecuteMsg::ForceUnlock( + ForceUnlockExecuteMsg::ForceWithdrawUnlocking { + amount: amount.map(Into::into), + lockup_id, + recipient, + }, + )), + &[], + signer, + ) + .unwrap(); + self + } + + fn update_force_withdraw_whitelist( + &self, + signer: &SigningAccount, + add_addresses: Vec, + remove_addresses: Vec, + ) -> &Self { + self.wasm() + .execute( + &self.vault_addr(), + &ExecuteMsg::VaultExtension(ExtensionExecuteMsg::ForceUnlock( + ForceUnlockExecuteMsg::UpdateForceWithdrawWhitelist { + add_addresses, + remove_addresses, + }, + )), + &[], + signer, + ) + .unwrap(); + self + } +} diff --git a/test-helpers/src/traits/lockup.rs b/test-helpers/src/traits/lockup.rs new file mode 100644 index 0000000..870b821 --- /dev/null +++ b/test-helpers/src/traits/lockup.rs @@ -0,0 +1,127 @@ +use cosmwasm_std::{Coin, Uint128}; +use cw_it::test_tube::{Runner, SigningAccount}; + +use cw_utils::Duration; +use cw_vault_standard::extensions::lockup::{LockupExecuteMsg, LockupQueryMsg, UnlockingPosition}; +use cw_vault_standard::msg::VaultStandardExecuteMsg as ExecuteMsg; +use cw_vault_standard::{ExtensionExecuteMsg, ExtensionQueryMsg, VaultStandardQueryMsg}; + +use super::CwVaultStandardRobot; + +pub trait LockedVaultRobot<'a, R: Runner<'a> + 'a>: CwVaultStandardRobot<'a, R> { + fn unlock_with_funds( + &self, + amount: impl Into, + signer: &SigningAccount, + funds: &[Coin], + ) -> &Self { + self.wasm() + .execute( + &self.vault_addr(), + &ExecuteMsg::VaultExtension(ExtensionExecuteMsg::Lockup( + LockupExecuteMsg::Unlock { + amount: amount.into(), + }, + )), + funds, + signer, + ) + .unwrap(); + self + } + + fn unlock(&self, amount: impl Into, signer: &SigningAccount) -> &Self { + let info = self.query_info(); + let amount: Uint128 = amount.into(); + self.unlock_with_funds( + amount.clone(), + signer, + &[Coin { + amount, + denom: info.vault_token, + }], + ) + } + + fn withdraw_unlocked( + &self, + lockup_id: u64, + recipient: Option, + signer: &SigningAccount, + ) -> &Self { + self.wasm() + .execute( + &self.vault_addr(), + &ExecuteMsg::VaultExtension(ExtensionExecuteMsg::Lockup( + LockupExecuteMsg::WithdrawUnlocked { + lockup_id, + recipient, + }, + )), + &[], + signer, + ) + .unwrap(); + self + } + + fn query_unlocking_positions( + &self, + address: impl Into, + start_after: Option, + limit: Option, + ) -> Vec { + self.wasm() + .query( + &self.vault_addr(), + &VaultStandardQueryMsg::VaultExtension(ExtensionQueryMsg::Lockup( + LockupQueryMsg::UnlockingPositions { + owner: address.into(), + start_after, + limit, + }, + )), + ) + .unwrap() + } + + fn query_unlocking_position(&self, lockup_id: u64) -> UnlockingPosition { + self.wasm() + .query( + &self.vault_addr(), + &VaultStandardQueryMsg::VaultExtension(ExtensionQueryMsg::Lockup( + LockupQueryMsg::UnlockingPosition { lockup_id }, + )), + ) + .unwrap() + } + + fn query_lockup_duration(&self) -> Duration { + self.wasm() + .query( + &self.vault_addr(), + &VaultStandardQueryMsg::VaultExtension(ExtensionQueryMsg::Lockup( + LockupQueryMsg::LockupDuration {}, + )), + ) + .unwrap() + } + + fn assert_number_of_unlocking_positions( + &self, + address: impl Into, + expected: usize, + ) -> &Self { + let positions = self.query_unlocking_positions(address, None, None); + assert_eq!(positions.len(), expected); + + self + } + + fn assert_unlocking_position_eq(&self, lockup_id: u64, expected: UnlockingPosition) -> &Self { + let position = self.query_unlocking_position(lockup_id); + assert_eq!(position, expected); + + self + } +} diff --git a/test-helpers/src/traits/mod.rs b/test-helpers/src/traits/mod.rs new file mode 100644 index 0000000..422b0cc --- /dev/null +++ b/test-helpers/src/traits/mod.rs @@ -0,0 +1,112 @@ +#[cfg(feature = "lockup")] +pub mod lockup; + +#[cfg(feature = "force-unlock")] +pub mod force_unlock; + +use cosmwasm_std::{Coin, Empty, Uint128}; +use cw_it::robot::TestRobot; +use cw_it::test_tube::{Account, Runner, SigningAccount}; + +use cw_vault_standard::msg::{ + VaultStandardExecuteMsg as ExecuteMsg, VaultStandardQueryMsg as QueryMsg, +}; +use cw_vault_standard::VaultInfoResponse; + +pub trait CwVaultStandardRobot<'a, R: Runner<'a> + 'a>: TestRobot<'a, R> { + fn vault_addr(&self) -> String; + + fn query_info(&self) -> VaultInfoResponse { + self.wasm() + .query(&self.vault_addr(), &QueryMsg::::Info {}) + .unwrap() + } + + /// Returns the base token. + fn base_token(&self) -> String { + self.query_info().base_token + } + + /// Returns the vault token. + fn vault_token(&self) -> String { + self.query_info().vault_token + } + + fn deposit( + &self, + amount: impl Into, + recipient: Option, + funds: &[Coin], + signer: &SigningAccount, + ) -> &Self { + let amount: Uint128 = amount.into(); + self.wasm() + .execute( + &self.vault_addr(), + &ExecuteMsg::::Deposit { amount, recipient }, + funds, + signer, + ) + .unwrap(); + self + } + + fn deposit_all(&self, recipient: Option, signer: &SigningAccount) -> &Self { + let base_token_denom = self.query_info().base_token; + let amount = self.query_native_token_balance(&signer.address(), &base_token_denom); + + self.deposit( + amount, + recipient, + &[Coin::new(amount.u128(), base_token_denom)], + signer, + ) + } + + fn query_base_token_balance(&self, address: impl Into) -> Uint128; + + fn assert_base_token_balance_eq( + &self, + address: impl Into, + amount: impl Into, + ) -> &Self { + let amount: Uint128 = amount.into(); + let balance = self.query_base_token_balance(address); + assert_eq!(balance, amount); + self + } + + fn query_vault_token_balance(&self, address: impl Into) -> Uint128 { + let info = self.query_info(); + self.query_native_token_balance(address, &info.vault_token) + } + + fn assert_vault_token_balance_eq( + &self, + address: impl Into, + amount: impl Into, + ) -> &Self { + let amount: Uint128 = amount.into(); + let balance = self.query_vault_token_balance(address); + assert_eq!(balance, amount); + self + } + + fn redeem(&self, amount: Uint128, recipient: Option, signer: &SigningAccount) -> &Self { + self.wasm() + .execute( + &self.vault_addr(), + &ExecuteMsg::::Redeem { amount, recipient }, + &[], + signer, + ) + .unwrap(); + self + } + + fn redeem_all(&self, recipient: Option, signer: &SigningAccount) -> &Self { + let amount = + self.query_native_token_balance(signer.address(), &self.query_info().vault_token); + self.redeem(amount, recipient, signer) + } +}