diff --git a/Cargo.lock b/Cargo.lock index 26e104f3..29d14ef5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -163,9 +163,9 @@ dependencies = [ [[package]] name = "cw-controllers" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26bd89d1f5cbae950ec3946ce5874f049c98d5608b1420292d5eda913aa4948d" +checksum = "696b7511de2eb449d086b8a88a95696bb6463bcfdd03d49014f03006259823e8" dependencies = [ "cosmwasm-std", "cw-storage-plus", @@ -177,25 +177,28 @@ dependencies = [ [[package]] name = "cw-multi-test" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f7b683e48a7070e968d29b4e31466f4c2e5b333d835dff94d204f8ef74b3bbc" +checksum = "7eb26e5d4efe1404afbb7d25a725420f0c1880e632d5728b6a6766c41a632a21" dependencies = [ + "anyhow", "cosmwasm-std", "cosmwasm-storage", "cw-storage-plus", "cw0", + "derivative", "itertools", "prost", "schemars", "serde", + "thiserror", ] [[package]] name = "cw-storage-plus" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef70f7912bed72ff56a4f704aee279b6ef4cd0a5f3aa2732ad371e3f6d3ea71" +checksum = "2a7f5027f469b714b1190d0adc7513ab34581710890087021fdb5847852407de" dependencies = [ "cosmwasm-std", "schemars", @@ -204,9 +207,9 @@ dependencies = [ [[package]] name = "cw0" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "debc623e83cca463fc5f195bb6f23803a7e5e281b330b759381dcc7ef76d4393" +checksum = "31da5efc36cb72c20f666d2b70cbaf29628c1092384efa9faf5b32e351d02fbe" dependencies = [ "cosmwasm-std", "schemars", @@ -216,9 +219,9 @@ dependencies = [ [[package]] name = "cw2" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51698a50df8be3d31d7261082f052685d04fe6289df89792fd4efa41daee8b90" +checksum = "73f874504d19ef98e8c2d8d58c8a42adcaef77a55fd4078c2dd145d49554e40d" dependencies = [ "cosmwasm-std", "cw-storage-plus", @@ -228,9 +231,9 @@ dependencies = [ [[package]] name = "cw20" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f24779556263d95d5a7ebf8a0b0cd615df0b34bff69c23f409e5f17b5fb529ef" +checksum = "9af88109948111d5498b26b9b677158caf64aede477d84f665892a507178a72e" dependencies = [ "cosmwasm-std", "cw0", @@ -240,9 +243,9 @@ dependencies = [ [[package]] name = "cw3" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d644170333aece0352ee6abfa64070809e2b58211bd4355e09d8b472479fb90" +checksum = "0762c548bb7f97395d804331f327bc99be52c4d6d48b83f87216cfe1b6e86896" dependencies = [ "cosmwasm-std", "cw0", @@ -259,6 +262,17 @@ dependencies = [ "const-oid", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.9.0" @@ -681,6 +695,7 @@ dependencies = [ "cw0", "schemars", "serde", + "tg-bindings", "thiserror", ] @@ -692,6 +707,7 @@ dependencies = [ "cosmwasm-std", "schemars", "serde", + "tg-bindings", ] [[package]] @@ -706,6 +722,7 @@ dependencies = [ "cw2", "schemars", "serde", + "tg-bindings", "tg-controllers", "tg4", "thiserror", @@ -725,6 +742,7 @@ dependencies = [ "integer-sqrt", "schemars", "serde", + "tg-bindings", "tg-controllers", "tg4", "tg4-group", @@ -743,8 +761,10 @@ dependencies = [ "cw0", "cw2", "cw20", + "itertools", "schemars", "serde", + "tg-bindings", "tg-controllers", "tg4", "thiserror", diff --git a/contracts/tg4-group/Cargo.toml b/contracts/tg4-group/Cargo.toml index c712ce8d..a011996e 100644 --- a/contracts/tg4-group/Cargo.toml +++ b/contracts/tg4-group/Cargo.toml @@ -19,12 +19,13 @@ backtraces = ["cosmwasm-std/backtraces"] library = [] [dependencies] -cw0 = { version = "0.8.0" } -cw2 = { version = "0.8.0" } -cw-controllers = { version = "0.8.0" } -cw-storage-plus = { version = "0.8.0" } +cw0 = { version = "0.9.0" } +cw2 = { version = "0.9.0" } +cw-controllers = { version = "0.9.0" } +cw-storage-plus = { version = "0.9.0" } tg4 = { path = "../../packages/tg4", version = "0.3.0" } tg-controllers = { version = "0.3.0", path = "../../packages/controllers" } +tg-bindings = { version = "0.3.0", path = "../../packages/bindings" } cosmwasm-std = { version = "0.16.0" } schemars = "0.8" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/contracts/tg4-group/src/contract.rs b/contracts/tg4-group/src/contract.rs index c064f529..0b325b8c 100644 --- a/contracts/tg4-group/src/contract.rs +++ b/contracts/tg4-group/src/contract.rs @@ -1,8 +1,6 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use cosmwasm_std::{ - to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, StdResult, SubMsg, -}; +use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Order, StdResult}; use cw0::maybe_addr; use cw2::set_contract_version; use cw_storage_plus::{Bound, PrimaryKey, U64Key}; @@ -14,6 +12,10 @@ use tg4::{ use crate::error::ContractError; use crate::msg::{ExecuteMsg, InstantiateMsg, PreauthResponse, QueryMsg, SudoMsg}; use crate::state::{members, ADMIN, HOOKS, PREAUTH, TOTAL}; +use tg_bindings::TgradeMsg; + +pub type Response = cosmwasm_std::Response; +pub type SubMsg = cosmwasm_std::SubMsg; // version info for migration info const CONTRACT_NAME: &str = "crates.io:tg4-group"; diff --git a/contracts/tg4-mixer/Cargo.toml b/contracts/tg4-mixer/Cargo.toml index cd91f473..55b23188 100644 --- a/contracts/tg4-mixer/Cargo.toml +++ b/contracts/tg4-mixer/Cargo.toml @@ -19,12 +19,13 @@ backtraces = ["cosmwasm-std/backtraces"] library = [] [dependencies] -cw0 = { version = "0.8.0" } -cw2 = { version = "0.8.0" } -cw20 = { version = "0.8.0" } -cw-storage-plus = { version = "0.8.0" } +cw0 = { version = "0.9.0" } +cw2 = { version = "0.9.0" } +cw20 = { version = "0.9.0" } +cw-storage-plus = { version = "0.9.0" } tg4 = { path = "../../packages/tg4", version = "0.3.0" } tg-controllers = { path = "../../packages/controllers", version = "0.3.0" } +tg-bindings = { path = "../../packages/bindings", version = "0.3.0" } cosmwasm-std = { version = "0.16.0" } integer-sqrt = { version = "0.1.5" } schemars = "0.8" @@ -32,7 +33,7 @@ serde = { version = "1.0.103", default-features = false, features = ["derive"] } thiserror = { version = "1.0.21" } [dev-dependencies] -cw-multi-test = { version = "0.8.0" } +cw-multi-test = { version = "0.9.0" } cosmwasm-schema = { version = "0.16.0" } tg4-group = { path = "../tg4-group", version = "0.3.0", features = ["library"] } tg4-stake = { path = "../tg4-stake", version = "0.3.0", features = ["library"] } diff --git a/contracts/tg4-mixer/src/contract.rs b/contracts/tg4-mixer/src/contract.rs index 58ac85e4..dbb479ae 100644 --- a/contracts/tg4-mixer/src/contract.rs +++ b/contracts/tg4-mixer/src/contract.rs @@ -1,12 +1,11 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use cosmwasm_std::{ - to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, StdResult, SubMsg, -}; +use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Order, StdResult}; use cw0::maybe_addr; use cw2::set_contract_version; use cw_storage_plus::{Bound, PrimaryKey, U64Key}; use integer_sqrt::IntegerSquareRoot; +use tg_bindings::TgradeMsg; use tg4::{ HooksResponse, Member, MemberChangedHookMsg, MemberDiff, MemberListResponse, MemberResponse, @@ -17,6 +16,9 @@ use crate::error::ContractError; use crate::msg::{ExecuteMsg, GroupsResponse, InstantiateMsg, PreauthResponse, QueryMsg}; use crate::state::{members, Groups, GROUPS, HOOKS, PREAUTH, TOTAL}; +pub type Response = cosmwasm_std::Response; +pub type SubMsg = cosmwasm_std::SubMsg; + // version info for migration info const CONTRACT_NAME: &str = "crates.io:tg4-mixer"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -325,11 +327,11 @@ fn list_members_by_weight( #[cfg(test)] mod tests { use super::*; - use cosmwasm_std::testing::{mock_env, MockApi, MockStorage}; - use cosmwasm_std::{coins, Addr, Empty, Uint128}; + use cosmwasm_std::{coins, Addr, Uint128}; use cw20::Denom; - use cw_multi_test::{next_block, App, BankKeeper, Contract, ContractWrapper, Executor}; + use cw_multi_test::{next_block, App, AppBuilder, Contract, ContractWrapper, Executor}; use tg4_stake::state::Duration; + use tg_bindings::TgradeMsg; const STAKE_DENOM: &str = "utgd"; const OWNER: &str = "owner"; @@ -346,7 +348,7 @@ mod tests { } } - pub fn contract_mixer() -> Box> { + pub fn contract_mixer() -> Box> { let contract = ContractWrapper::new( crate::contract::execute, crate::contract::instantiate, @@ -355,7 +357,7 @@ mod tests { Box::new(contract) } - pub fn contract_group() -> Box> { + pub fn contract_group() -> Box> { let contract = ContractWrapper::new( tg4_group::contract::execute, tg4_group::contract::instantiate, @@ -364,7 +366,7 @@ mod tests { Box::new(contract) } - pub fn contract_staking() -> Box> { + pub fn contract_staking() -> Box> { let contract = ContractWrapper::new( tg4_stake::contract::execute, tg4_stake::contract::instantiate, @@ -373,16 +375,8 @@ mod tests { Box::new(contract) } - fn mock_app() -> App { - let env = mock_env(); - let api = MockApi::default(); - let bank = BankKeeper::new(); - - App::new(api, env.block, bank, MockStorage::new()) - } - // uploads code and returns address of group contract - fn instantiate_group(app: &mut App, members: Vec) -> Addr { + fn instantiate_group(app: &mut App, members: Vec) -> Addr { let admin = Some(OWNER.into()); let group_id = app.store_code(contract_group()); let msg = tg4_group::msg::InstantiateMsg { @@ -395,7 +389,7 @@ mod tests { } // uploads code and returns address of group contract - fn instantiate_staking(app: &mut App, stakers: Vec) -> Addr { + fn instantiate_staking(app: &mut App, stakers: Vec) -> Addr { let admin = Some(OWNER.into()); let group_id = app.store_code(contract_staking()); let msg = tg4_stake::msg::InstantiateMsg { @@ -405,6 +399,7 @@ mod tests { unbonding_period: Duration::new_from_seconds(3600), admin: admin.clone(), preauths: Some(1), + auto_return_limit: 0, }; let contract = app .instantiate_contract( @@ -433,7 +428,7 @@ mod tests { contract } - fn instantiate_mixer(app: &mut App, left: &Addr, right: &Addr) -> Addr { + fn instantiate_mixer(app: &mut App, left: &Addr, right: &Addr) -> Addr { let flex_id = app.store_code(contract_mixer()); let msg = crate::msg::InstantiateMsg { left_group: left.to_string(), @@ -449,7 +444,7 @@ mod tests { /// and connectioning them all to the mixer. /// /// Returns (mixer address, group address, staking address). - fn setup_test_case(app: &mut App, stakers: Vec) -> (Addr, Addr, Addr) { + fn setup_test_case(app: &mut App, stakers: Vec) -> (Addr, Addr, Addr) { // 1. Instantiate group contract with members (and OWNER as admin) let members = vec![ member(OWNER, 0), @@ -475,7 +470,7 @@ mod tests { #[allow(clippy::too_many_arguments)] fn check_membership( - app: &App, + app: &App, mixer_addr: &Addr, owner: Option, voter1: Option, @@ -508,7 +503,7 @@ mod tests { #[test] fn basic_init() { - let mut app = mock_app(); + let mut app = AppBuilder::new().build(); let stakers = vec![ member(OWNER, 88888888888), // 0 weight -> 0 mixed member(VOTER1, 10000), // 10000 stake, 100 weight -> 1000 mixed @@ -532,7 +527,7 @@ mod tests { #[test] fn update_with_upstream_change() { - let mut app = mock_app(); + let mut app = AppBuilder::new().build(); let stakers = vec![ member(VOTER1, 10000), // 10000 stake, 100 weight -> 1000 mixed member(VOTER3, 7500), // 7500 stake, 300 weight -> 1500 mixed diff --git a/contracts/tg4-stake/Cargo.toml b/contracts/tg4-stake/Cargo.toml index cc2b587f..8bb1b4a2 100644 --- a/contracts/tg4-stake/Cargo.toml +++ b/contracts/tg4-stake/Cargo.toml @@ -19,17 +19,19 @@ backtraces = ["cosmwasm-std/backtraces"] library = [] [dependencies] -cw0 = { version = "0.8.0" } -cw2 = { version = "0.8.0" } -cw20 = { version = "0.8.0" } -cw-controllers = { version = "0.8.0" } -cw-storage-plus = { version = "0.8.0" } +cw0 = { version = "0.9.0" } +cw2 = { version = "0.9.0" } +cw20 = { version = "0.9.0" } +cw-controllers = { version = "0.9.0" } +cw-storage-plus = { version = "0.9.0" } tg4 = { path = "../../packages/tg4", version = "0.3.0" } tg-controllers = { path = "../../packages/controllers", version = "0.3.0" } +tg-bindings = { path = "../../packages/bindings", version = "0.3.0" } cosmwasm-std = { version = "0.16.0" } schemars = "0.8.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } thiserror = { version = "1.0.21" } +itertools = "0.10" [dev-dependencies] cosmwasm-schema = { version = "0.16.0" } diff --git a/contracts/tg4-stake/examples/schema.rs b/contracts/tg4-stake/examples/schema.rs index 5a009190..f76614c1 100644 --- a/contracts/tg4-stake/examples/schema.rs +++ b/contracts/tg4-stake/examples/schema.rs @@ -4,8 +4,9 @@ use std::fs::create_dir_all; use cosmwasm_schema::{export_schema, export_schema_with_title, remove_schemas, schema_for}; pub use tg4::{AdminResponse, MemberListResponse, MemberResponse, TotalWeightResponse}; -pub use tg4_stake::claim::ClaimsResponse; -pub use tg4_stake::msg::{ExecuteMsg, InstantiateMsg, PreauthResponse, QueryMsg, StakedResponse}; +pub use tg4_stake::msg::{ + ClaimsResponse, ExecuteMsg, InstantiateMsg, PreauthResponse, QueryMsg, StakedResponse, +}; fn main() { let mut out_dir = current_dir().unwrap(); diff --git a/contracts/tg4-stake/schema/claims_response.json b/contracts/tg4-stake/schema/claims_response.json index 63c52c0b..2779c15f 100644 --- a/contracts/tg4-stake/schema/claims_response.json +++ b/contracts/tg4-stake/schema/claims_response.json @@ -14,24 +14,48 @@ } }, "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, "Claim": { "type": "object", "required": [ + "addr", "amount", "creation_height", "release_at" ], "properties": { + "addr": { + "description": "Address owning the claim", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, "amount": { - "$ref": "#/definitions/Uint128" + "description": "Amount of tokens in claim", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] }, "creation_height": { + "description": "Height of a blockchain in a moment of creation of this claim", "type": "integer", "format": "uint64", "minimum": 0.0 }, "release_at": { - "$ref": "#/definitions/Expiration" + "description": "Release time of the claim. Originally in `cw_controllers` it is an `Expiration` type, but here we need to query for claims via release time, and expiration is impossible to be properly sorted, as it is impossible to properly compare expiration by height and expiration by time.", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] } } }, diff --git a/contracts/tg4-stake/schema/instantiate_msg.json b/contracts/tg4-stake/schema/instantiate_msg.json index 07e0476e..643a8874 100644 --- a/contracts/tg4-stake/schema/instantiate_msg.json +++ b/contracts/tg4-stake/schema/instantiate_msg.json @@ -15,6 +15,13 @@ "null" ] }, + "auto_return_limit": { + "description": "Limits how much claims would be automatically returned at end of block, 20 by default. Setting this to 0 disables auto returning claims.", + "default": 20, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, "denom": { "description": "denom of the token to stake", "allOf": [ @@ -38,7 +45,12 @@ "$ref": "#/definitions/Uint128" }, "unbonding_period": { - "$ref": "#/definitions/Duration" + "description": "unbounding period in seconds", + "allOf": [ + { + "$ref": "#/definitions/Duration" + } + ] } }, "definitions": { diff --git a/contracts/tg4-stake/src/claim.rs b/contracts/tg4-stake/src/claim.rs index 2d1374be..84875cb0 100644 --- a/contracts/tg4-stake/src/claim.rs +++ b/contracts/tg4-stake/src/claim.rs @@ -1,40 +1,69 @@ // Copied from cw-plus repository: https://github.com/CosmWasm/cw-plus/tree/main/packages/controllers // Original file distributed on Apache license +use itertools::Itertools; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::state::Expiration; -use cosmwasm_std::{Addr, BlockInfo, Deps, StdResult, Storage, Uint128}; -use cw_storage_plus::Map; - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -pub struct ClaimsResponse { - pub claims: Vec, -} +use crate::state::{Expiration, ExpirationKey}; +use cosmwasm_std::{Addr, BlockInfo, Deps, Order, StdResult, Storage, Uint128}; +use cw_storage_plus::{Bound, Index, IndexList, IndexedMap, MultiIndex}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct Claim { + /// Address owning the claim + pub addr: Addr, + /// Amount of tokens in claim pub amount: Uint128, + /// Release time of the claim. Originally in `cw_controllers` it is an `Expiration` type, but + /// here we need to query for claims via release time, and expiration is impossible to be + /// properly sorted, as it is impossible to properly compare expiration by height and + /// expiration by time. pub release_at: Expiration, + /// Height of a blockchain in a moment of creation of this claim pub creation_height: u64, } +struct ClaimIndexes<'a> { + pub release_at: MultiIndex<'a, (ExpirationKey, Vec), Claim>, +} + +impl<'a> IndexList for ClaimIndexes<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.release_at]; + Box::new(v.into_iter()) + } +} + impl Claim { - pub fn new(amount: u128, release_at: Expiration, creation_height: u64) -> Self { + pub fn new(addr: Addr, amount: u128, released: Expiration, creation_height: u64) -> Self { Claim { + addr, amount: amount.into(), - release_at, + release_at: released, creation_height, } } } -pub struct Claims<'a>(Map<'a, &'a Addr, Vec>); +pub struct Claims<'a> { + /// Claims are indexed by `(addr, release_at)` pair. Claims falling into the same key are + /// merged (summarized) as there is no point to distinguish them. + claims: IndexedMap<'a, (Addr, ExpirationKey), Claim, ClaimIndexes<'a>>, +} impl<'a> Claims<'a> { - pub const fn new(storage_key: &'a str) -> Self { - Claims(Map::new(storage_key)) + pub fn new(storage_key: &'a str, release_subkey: &'a str) -> Self { + let indexes = ClaimIndexes { + release_at: MultiIndex::new( + |claim, k| (claim.release_at.into(), k), + storage_key, + release_subkey, + ), + }; + let claims = IndexedMap::new(storage_key, indexes); + + Self { claims } } /// This creates a claim, such that the given address can claim an amount of tokens after @@ -42,58 +71,167 @@ impl<'a> Claims<'a> { pub fn create_claim( &self, storage: &mut dyn Storage, - addr: &Addr, + addr: Addr, amount: Uint128, release_at: Expiration, creation_height: u64, ) -> StdResult<()> { - // add a claim to this user to get their tokens after the unbonding period - self.0.update(storage, addr, |old| -> StdResult<_> { - let mut claims = old.unwrap_or_default(); - claims.push(Claim { - amount, - release_at, - creation_height, - }); - Ok(claims) - })?; + // Add a claim to this user to get their tokens after the unbonding period + self.claims.update( + storage, + (addr.clone(), release_at.into()), + move |claim| -> StdResult<_> { + match claim { + Some(mut claim) => { + claim.amount += amount; + Ok(claim) + } + None => Ok(Claim { + addr, + amount, + release_at, + creation_height, + }), + } + }, + )?; + Ok(()) } /// This iterates over all mature claims for the address, and removes them, up to an optional cap. - /// it removes the finished claims and returns the total amount of tokens to be released. - pub fn claim_tokens( + /// It removes the finished claims and returns the total amount of tokens to be released. + pub fn claim_addr( &self, storage: &mut dyn Storage, addr: &Addr, block: &BlockInfo, cap: Option, ) -> StdResult { - let mut to_send = Uint128::zero(); - self.0.update(storage, addr, |claim| -> StdResult<_> { - let (_send, waiting): (Vec<_>, _) = - claim.unwrap_or_default().iter().cloned().partition(|c| { - // if mature and we can pay fully, then include in _send - if c.release_at.is_expired(block) { - if let Some(limit) = cap { - if to_send + c.amount > limit { - return false; - } - } - to_send += c.amount; - true - } else { - // not to send, leave in waiting and save again - false - } - }); - Ok(waiting) - })?; - Ok(to_send) + let claims = self + .claims + .prefix(addr.clone()) + // take all claims for the addr + .range(storage, None, None, Order::Ascending) + // filter out non-expired claims (leaving errors to stop on first + .filter(|claim| match claim { + Ok((_, claim)) => claim.release_at.is_expired(block), + Err(_) => true, + }); + + let claims = self.filter_claims(claims, cap.map(u128::from), None)?; + let amount = claims.iter().map(|claim| claim.amount).sum(); + + self.release_claims(storage, claims)?; + + Ok(amount) + } + + /// This iterates over all mature claims of any addresses, and removes them. Up to `limit` + /// claims would be processed, starting from the oldest. It removes the finished claims and + /// returns vector of pairs: `(addr, amount)`, representing amount of tokens to be released to particular addresses + pub fn claim_expired( + &self, + storage: &mut dyn Storage, + block: &BlockInfo, + limit: impl Into>, + ) -> StdResult> { + let claims = self + .claims + .idx + .release_at + // take all claims which are expired (at most same timestamp as current block) + .range( + storage, + None, + Some(Bound::inclusive(self.claims.idx.release_at.index_key(( + ExpirationKey::new(Expiration::now(block)), + vec![], + )))), + Order::Ascending, + ); + + let mut claims = self.filter_claims(claims, None, limit.into())?; + claims.sort_by_key(|claim| claim.addr.clone()); + + let releases = claims + .iter() + // TODO: use `slice::group_by` in place of `Itertools::group_by` when `slice_group_by` + // is stabilized [https://github.com/rust-lang/rust/issues/80552] + .group_by(|claim| &claim.addr) + .into_iter() + .map(|(addr, group)| (addr.clone(), group.map(|claim| claim.amount).sum())) + .collect(); + + self.release_claims(storage, claims)?; + + Ok(releases) + } + + /// Processes claims filtering those which are to be released. Returns vector of claims to be + /// released + fn filter_claims( + &self, + claims: impl IntoIterator, Claim)>>, + cap: Option, + limit: Option, + ) -> StdResult> { + let claims = claims + .into_iter() + // calculate sum for claims up to this one for cap filtering + .scan(0u128, |sum, claim| match claim { + Ok((_, claim)) => { + *sum += u128::from(claim.amount); + Some(Ok((*sum, claim))) + } + Err(err) => Some(Err(err)), + }) + // stop when sum exceeds limit + .take_while(|claim| match (cap, claim) { + (Some(cap), Ok((sum, _))) => cap <= *sum, + _ => true, + }) + // now only proper claims as in iterator, so just map them back to claim + .map(|claim| match claim { + Ok((_, claim)) => Ok(claim), + Err(err) => Err(err), + }); + + // apply limit and collect - it is needed to collect intermediately, as it is impossible to + // remove from map while iterating as it borrows map internally; collecting to result, so + // it returns early on failure; collecting would also trigger a final map, so amount would + // be properly fulfilled + let claims = if let Some(limit) = limit { + claims.take(limit as usize).collect() + } else { + claims.collect::>() + }?; + + Ok(claims) + } + + /// Releases given claims by removing them from storage + fn release_claims( + &self, + storage: &mut dyn Storage, + claims: impl IntoIterator, + ) -> StdResult<()> { + for claim in claims { + self.claims + .remove(storage, (claim.addr, claim.release_at.into()))?; + } + + Ok(()) } - pub fn query_claims(&self, deps: Deps, address: &Addr) -> StdResult { - let claims = self.0.may_load(deps.storage, address)?.unwrap_or_default(); - Ok(ClaimsResponse { claims }) + pub fn query_claims(&self, deps: Deps, address: Addr) -> StdResult> { + self.claims + .prefix(address) + .range(deps.storage, None, None, Order::Ascending) + .map(|claim| match claim { + Ok((_, claim)) => Ok(claim), + Err(err) => Err(err), + }) + .collect() } } diff --git a/contracts/tg4-stake/src/contract.rs b/contracts/tg4-stake/src/contract.rs index c84aaa23..4bebe069 100644 --- a/contracts/tg4-stake/src/contract.rs +++ b/contracts/tg4-stake/src/contract.rs @@ -2,7 +2,7 @@ use cosmwasm_std::entry_point; use cosmwasm_std::{ coin, coins, to_binary, Addr, BankMsg, Binary, Coin, Deps, DepsMut, Env, MessageInfo, Order, - Response, StdError, StdResult, Storage, SubMsg, Uint128, + StdError, StdResult, Storage, Uint128, }; use cw0::{maybe_addr, NativeBalance}; @@ -13,12 +13,17 @@ use tg4::{ HooksResponse, Member, MemberChangedHookMsg, MemberDiff, MemberListResponse, MemberResponse, TotalWeightResponse, }; +use tg_bindings::{request_privileges, Privilege, PrivilegeChangeMsg, TgradeMsg, TgradeSudoMsg}; use crate::error::ContractError; use crate::msg::{ - ExecuteMsg, InstantiateMsg, PreauthResponse, QueryMsg, StakedResponse, UnbondingPeriodResponse, + ClaimsResponse, ExecuteMsg, InstantiateMsg, PreauthResponse, QueryMsg, StakedResponse, + UnbondingPeriodResponse, }; -use crate::state::{members, Config, ADMIN, CLAIMS, CONFIG, HOOKS, PREAUTH, STAKE, TOTAL}; +use crate::state::{claims, members, Config, ADMIN, CONFIG, HOOKS, PREAUTH, STAKE, TOTAL}; + +pub type Response = cosmwasm_std::Response; +pub type SubMsg = cosmwasm_std::SubMsg; // version info for migration info const CONTRACT_NAME: &str = "crates.io:tg4-stake"; @@ -51,6 +56,7 @@ pub fn instantiate( tokens_per_weight: msg.tokens_per_weight, min_bond, unbonding_period: msg.unbonding_period, + auto_return_limit: msg.auto_return_limit, }; CONFIG.save(deps.storage, &config)?; TOTAL.save(deps.storage, &0)?; @@ -68,9 +74,9 @@ pub fn execute( ) -> Result { let api = deps.api; match msg { - ExecuteMsg::UpdateAdmin { admin } => { - Ok(ADMIN.execute_update_admin(deps, info, maybe_addr(api, admin)?)?) - } + ExecuteMsg::UpdateAdmin { admin } => ADMIN + .execute_update_admin(deps, info, maybe_addr(api, admin)?) + .map_err(Into::into), ExecuteMsg::AddHook { addr } => execute_add_hook(deps, info, addr), ExecuteMsg::RemoveHook { addr } => execute_remove_hook(deps, info, addr), ExecuteMsg::Bond {} => execute_bond(deps, env, Balance::from(info.funds), info.sender), @@ -174,9 +180,9 @@ pub fn execute_unbond( // provide them a claim let cfg = CONFIG.load(deps.storage)?; - CLAIMS.create_claim( + claims().create_claim( deps.storage, - &info.sender, + info.sender.clone(), amount, cfg.unbonding_period.after(&env.block), env.block.height, @@ -256,19 +262,18 @@ pub fn execute_claim( env: Env, info: MessageInfo, ) -> Result { - let release = CLAIMS.claim_tokens(deps.storage, &info.sender, &env.block, None)?; + let release = claims().claim_addr(deps.storage, &info.sender, &env.block, None)?; if release.is_zero() { return Err(ContractError::NothingToClaim {}); } let config = CONFIG.load(deps.storage)?; - let amount; - match &config.denom { - Denom::Native(denom) => amount = coins(release.u128(), denom), + let amount = match &config.denom { + Denom::Native(denom) => coins(release.into(), denom), Denom::Cw20(_addr) => { - unimplemented!("The CW20 coins release functionality is in progress") + return Err(ContractError::Cw20CoinsRelease {}); } - } + }; let res = Response::new() .add_attribute("action", "claim") @@ -278,6 +283,7 @@ pub fn execute_claim( to_address: info.sender.into(), amount, }); + Ok(res) } @@ -290,6 +296,63 @@ fn coins_to_string(coins: &[Coin]) -> String { strings.join(",") } +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn sudo(deps: DepsMut, env: Env, msg: TgradeSudoMsg) -> Result { + match msg { + TgradeSudoMsg::PrivilegeChange(PrivilegeChangeMsg::Promoted {}) => privilege_promote(deps), + TgradeSudoMsg::EndBlock {} => end_block(deps, env), + _ => Err(ContractError::UnknownSudoMsg {}), + } +} + +fn privilege_promote(deps: DepsMut) -> Result { + let config = CONFIG.load(deps.storage)?; + + if config.auto_return_limit > 0 { + let msgs = request_privileges(&[Privilege::EndBlocker]); + Ok(Response::new().add_submessages(msgs)) + } else { + Ok(Response::new()) + } +} + +fn end_block(deps: DepsMut, env: Env) -> Result { + let mut resp = Response::new(); + + let config = CONFIG.load(deps.storage)?; + if config.auto_return_limit > 0 { + let sub_msgs = release_expired_claims(deps, env, config)?; + resp = resp.add_submessages(sub_msgs); + } + + Ok(resp) +} + +fn release_expired_claims( + deps: DepsMut, + env: Env, + config: Config, +) -> Result, ContractError> { + let releases = claims().claim_expired(deps.storage, &env.block, config.auto_return_limit)?; + + releases + .into_iter() + .map(|(addr, amount)| { + let amount = match &config.denom { + Denom::Native(denom) => coins(amount.into(), denom), + Denom::Cw20(_) => { + return Err(ContractError::Cw20CoinsRelease {}); + } + }; + + Ok(SubMsg::new(BankMsg::Send { + to_address: addr.into(), + amount, + })) + }) + .collect() +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { @@ -304,9 +367,9 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { to_binary(&list_members_by_weight(deps, start_after, limit)?) } QueryMsg::TotalWeight {} => to_binary(&query_total_weight(deps)?), - QueryMsg::Claims { address } => { - to_binary(&CLAIMS.query_claims(deps, &deps.api.addr_validate(&address)?)?) - } + QueryMsg::Claims { address } => to_binary(&ClaimsResponse { + claims: claims().query_claims(deps, deps.api.addr_validate(&address)?)?, + }), QueryMsg::Staked { address } => to_binary(&query_staked(deps, address)?), QueryMsg::Admin {} => to_binary(&ADMIN.query_admin(deps)?), QueryMsg::Hooks {} => { @@ -455,6 +518,7 @@ mod tests { unbonding_period, admin: Some(INIT_ADMIN.into()), preauths: Some(1), + auto_return_limit: 0, }; let info = mock_info("creator", &[]); instantiate(deps, mock_env(), info, msg).unwrap(); @@ -819,8 +883,9 @@ mod tests { assert_eq!(None, member3_raw); } - fn get_claims(deps: Deps, addr: &Addr) -> Vec { - CLAIMS.query_claims(deps, addr).unwrap().claims + #[track_caller] + fn get_claims(deps: Deps, addr: Addr) -> Vec { + claims().query_claims(deps, addr).unwrap() } #[test] @@ -838,14 +903,24 @@ mod tests { // check the claims for each user let expires = Duration::new_from_seconds(UNBONDING_DURATION).after(&env.block); assert_eq!( - get_claims(deps.as_ref(), &Addr::unchecked(USER1)), - vec![Claim::new(4_500, expires, env.block.height)] + get_claims(deps.as_ref(), Addr::unchecked(USER1)), + vec![Claim::new( + Addr::unchecked(USER1), + 4_500, + expires, + env.block.height + )] ); assert_eq!( - get_claims(deps.as_ref(), &Addr::unchecked(USER2)), - vec![Claim::new(2_600, expires, env.block.height)] + get_claims(deps.as_ref(), Addr::unchecked(USER2)), + vec![Claim::new( + Addr::unchecked(USER2), + 2_600, + expires, + env.block.height + )] ); - assert_eq!(get_claims(deps.as_ref(), &Addr::unchecked(USER3)), vec![]); + assert_eq!(get_claims(deps.as_ref(), Addr::unchecked(USER3)), vec![]); // do another unbond later on let mut env2 = mock_env(); @@ -853,32 +928,41 @@ mod tests { env2.block.height += height_delta; let time_delta = 50; unbond(deps.as_mut(), 0, 1_345, 1_500, height_delta, time_delta); - let updated_creation_height = env2.block.height; // with updated claims let expires2 = Duration::new_from_seconds(UNBONDING_DURATION + time_delta).after(&env2.block); assert_ne!(expires, expires2); assert_eq!( - get_claims(deps.as_ref(), &Addr::unchecked(USER1)), - vec![Claim::new(4_500, expires, env.block.height)] + get_claims(deps.as_ref(), Addr::unchecked(USER1)), + vec![Claim::new( + Addr::unchecked(USER1), + 4_500, + expires, + env.block.height + )] ); assert_eq!( - get_claims(deps.as_ref(), &Addr::unchecked(USER2)), + get_claims(deps.as_ref(), Addr::unchecked(USER2)), vec![ - Claim::new(2_600, expires, env.block.height), - Claim::new(1_345, expires2, updated_creation_height) + Claim::new(Addr::unchecked(USER2), 2_600, expires, env.block.height), + Claim::new(Addr::unchecked(USER2), 1_345, expires2, env2.block.height) ] ); assert_eq!( - get_claims(deps.as_ref(), &Addr::unchecked(USER3)), - vec![Claim::new(1_500, expires2, updated_creation_height)] + get_claims(deps.as_ref(), Addr::unchecked(USER3)), + vec![Claim::new( + Addr::unchecked(USER3), + 1_500, + expires2, + env2.block.height + )] ); // nothing can be withdrawn yet let err = execute( deps.as_mut(), - env2, + env, mock_info(USER1, &[]), ExecuteMsg::Claim {}, ) @@ -931,14 +1015,24 @@ mod tests { assert_eq!(err, ContractError::NothingToClaim {}); // claims updated properly - assert_eq!(get_claims(deps.as_ref(), &Addr::unchecked(USER1)), vec![]); + assert_eq!(get_claims(deps.as_ref(), Addr::unchecked(USER1)), vec![]); assert_eq!( - get_claims(deps.as_ref(), &Addr::unchecked(USER2)), - vec![Claim::new(1_345, expires2, updated_creation_height)] + get_claims(deps.as_ref(), Addr::unchecked(USER2)), + vec![Claim::new( + Addr::unchecked(USER2), + 1_345, + expires2, + env2.block.height + )] ); assert_eq!( - get_claims(deps.as_ref(), &Addr::unchecked(USER3)), - vec![Claim::new(1_500, expires2, updated_creation_height)] + get_claims(deps.as_ref(), Addr::unchecked(USER3)), + vec![Claim::new( + Addr::unchecked(USER3), + 1_500, + expires2, + env2.block.height + )] ); // add another few claims for 2 @@ -966,7 +1060,7 @@ mod tests { amount: coins(2_950, DENOM), })] ); - assert_eq!(get_claims(deps.as_ref(), &Addr::unchecked(USER2)), vec![]); + assert_eq!(get_claims(deps.as_ref(), Addr::unchecked(USER2)), vec![]); } #[test] diff --git a/contracts/tg4-stake/src/error.rs b/contracts/tg4-stake/src/error.rs index 3fcea58d..a12279f2 100644 --- a/contracts/tg4-stake/src/error.rs +++ b/contracts/tg4-stake/src/error.rs @@ -38,4 +38,10 @@ pub enum ContractError { #[error("No funds sent")] NoFunds {}, + + #[error("Unrecognized sudo message")] + UnknownSudoMsg {}, + + #[error("Cw20 coins release functionality is in progress")] + Cw20CoinsRelease {}, } diff --git a/contracts/tg4-stake/src/msg.rs b/contracts/tg4-stake/src/msg.rs index db7f28c3..2ea52079 100644 --- a/contracts/tg4-stake/src/msg.rs +++ b/contracts/tg4-stake/src/msg.rs @@ -3,21 +3,31 @@ use cosmwasm_std::{Coin, Uint128}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +pub use crate::claim::Claim; use cw20::Denom; use tg4::Member; +const fn default_auto_return_limit() -> u64 { + 20 +} + #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] pub struct InstantiateMsg { /// denom of the token to stake pub denom: Denom, pub tokens_per_weight: Uint128, pub min_bond: Uint128, + /// unbounding period in seconds pub unbonding_period: Duration, // admin can only add/remove hooks, not change other parameters pub admin: Option, // or you can simply pre-authorize a number of hooks (to be done in following messages) pub preauths: Option, + /// Limits how much claims would be automatically returned at end of block, 20 by default. + /// Setting this to 0 disables auto returning claims. + #[serde(default = "default_auto_return_limit")] + pub auto_return_limit: u64, } #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] @@ -94,3 +104,8 @@ pub struct PreauthResponse { pub struct UnbondingPeriodResponse { pub unbonding_period: Duration, } + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct ClaimsResponse { + pub claims: Vec, +} diff --git a/contracts/tg4-stake/src/state.rs b/contracts/tg4-stake/src/state.rs index cd92cddf..2c2e1af0 100644 --- a/contracts/tg4-stake/src/state.rs +++ b/contracts/tg4-stake/src/state.rs @@ -6,22 +6,60 @@ use cosmwasm_std::{Addr, BlockInfo, Timestamp, Uint128}; use cw20::Denom; use cw_controllers::Admin; use cw_storage_plus::{ - Index, IndexList, IndexedSnapshotMap, Item, Map, MultiIndex, SnapshotMap, Strategy, U64Key, + Index, IndexList, IndexedSnapshotMap, Item, Map, MultiIndex, Prefixer, PrimaryKey, SnapshotMap, + Strategy, U64Key, }; use tg4::TOTAL_KEY; use tg_controllers::{Hooks, Preauth}; -pub const CLAIMS: Claims = Claims::new("claims"); +/// Builds a claims map as it cannot be done in const time +pub fn claims() -> Claims<'static> { + Claims::new("claims", "claims__release") +} #[derive(Serialize, Deserialize, Clone, Copy, PartialEq, JsonSchema, Debug)] pub struct Expiration(Timestamp); impl Expiration { + pub fn now(block: &BlockInfo) -> Self { + Self(block.time) + } + pub fn is_expired(&self, block: &BlockInfo) -> bool { block.time >= self.0 } } +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct ExpirationKey(U64Key); + +impl ExpirationKey { + pub fn new(expiration: Expiration) -> Self { + Self(U64Key::new(expiration.0.nanos())) + } +} + +impl From for ExpirationKey { + fn from(expiration: Expiration) -> Self { + Self::new(expiration) + } +} + +impl<'a> PrimaryKey<'a> for ExpirationKey { + type Prefix = (); + type SubPrefix = (); + + fn key(&self) -> Vec<&[u8]> { + self.0.key() + } +} + +impl<'a> Prefixer<'a> for ExpirationKey { + fn prefix(&self) -> Vec<&[u8]> { + self.0.prefix() + } +} + #[derive(Serialize, Deserialize, Clone, Copy, PartialEq, JsonSchema, Debug)] pub struct Duration(Timestamp); @@ -41,7 +79,10 @@ pub struct Config { pub denom: Denom, pub tokens_per_weight: Uint128, pub min_bond: Uint128, + /// time in seconds pub unbonding_period: Duration, + /// limits of how much claims can be automatically returned at end of block + pub auto_return_limit: u64, } pub const ADMIN: Admin = Admin::new("admin"); diff --git a/contracts/tgrade-dso/Cargo.toml b/contracts/tgrade-dso/Cargo.toml index aa438a39..bb2f0b4a 100644 --- a/contracts/tgrade-dso/Cargo.toml +++ b/contracts/tgrade-dso/Cargo.toml @@ -19,11 +19,11 @@ backtraces = ["cosmwasm-std/backtraces"] library = [] [dependencies] -cw0 = { version = "0.8.0" } -cw2 = { version = "0.8.0" } -cw3 = { version = "0.8.0" } -cw-controllers = { version = "0.8.0" } -cw-storage-plus = { version = "0.8.0" } +cw0 = { version = "0.9.0" } +cw2 = { version = "0.9.0" } +cw3 = { version = "0.9.0" } +cw-controllers = { version = "0.9.0" } +cw-storage-plus = { version = "0.9.0" } tg4 = { path = "../../packages/tg4", version = "0.3.0" } cosmwasm-std = { version = "0.16.0" } schemars = "0.8" diff --git a/contracts/tgrade-gov-reflect/Cargo.toml b/contracts/tgrade-gov-reflect/Cargo.toml index 944dec99..b449bcac 100644 --- a/contracts/tgrade-gov-reflect/Cargo.toml +++ b/contracts/tgrade-gov-reflect/Cargo.toml @@ -21,7 +21,7 @@ backtraces = ["cosmwasm-std/backtraces"] [dependencies] cosmwasm-std = { version = "0.16.0" } -cw-storage-plus = { version = "0.8.0" } +cw-storage-plus = { version = "0.9.0" } tg-bindings = { version = "0.3.0", path = "../../packages/bindings" } schemars = "0.8.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/contracts/tgrade-valset/Cargo.toml b/contracts/tgrade-valset/Cargo.toml index b88c2a2f..d6c23aae 100644 --- a/contracts/tgrade-valset/Cargo.toml +++ b/contracts/tgrade-valset/Cargo.toml @@ -24,11 +24,11 @@ backtraces = ["cosmwasm-std/backtraces"] library = [] [dependencies] -cw0 = { version = "0.8.0" } -cw2 = { version = "0.8.0" } +cw0 = { version = "0.9.0" } +cw2 = { version = "0.9.0" } tg4 = { path = "../../packages/tg4", version = "0.3.0" } -cw-controllers = { version = "0.8.0" } -cw-storage-plus = { version = "0.8.0" } +cw-controllers = { version = "0.9.0" } +cw-storage-plus = { version = "0.9.0" } cosmwasm-std = { version = "0.16.0" } tg-bindings = { version = "0.3.0", path = "../../packages/bindings" } schemars = "0.8" @@ -37,7 +37,7 @@ thiserror = { version = "1.0.21" } [dev-dependencies] cosmwasm-schema = { version = "0.16.0" } -cw-multi-test = { version = "0.8.0" } -cw20 = { version = "0.8.0" } +cw-multi-test = { version = "0.9.0" } +cw20 = { version = "0.9.0" } tg4-group = { path = "../tg4-group", version = "0.3.0" } tg4-stake = { path = "../tg4-stake", version = "0.3.0" } diff --git a/contracts/tgrade-valset/src/contract.rs b/contracts/tgrade-valset/src/contract.rs index 29bc5985..628c4dda 100644 --- a/contracts/tgrade-valset/src/contract.rs +++ b/contracts/tgrade-valset/src/contract.rs @@ -414,12 +414,12 @@ fn calculate_diff(cur_vals: Vec, old_vals: Vec) -> #[cfg(test)] mod test { - use cw_multi_test::{App, Contract, ContractWrapper, Executor}; + use cw_multi_test::{App, AppBuilder, Contract, ContractWrapper, Executor}; use super::*; use crate::test_helpers::{ - addrs, contract_valset, members, mock_app, mock_metadata, mock_pubkey, nonmembers, - valid_operator, valid_validator, + addrs, contract_valset, members, mock_metadata, mock_pubkey, nonmembers, valid_operator, + valid_validator, }; use cosmwasm_std::{coin, Coin}; @@ -454,7 +454,7 @@ mod test { } fn contract_group() -> Box> { - let contract = ContractWrapper::new_with_empty( + let contract = ContractWrapper::new( tg4_group::contract::execute, tg4_group::contract::instantiate, tg4_group::contract::query, @@ -518,7 +518,7 @@ mod test { #[test] fn init_and_query_state() { - let mut app = mock_app(); + let mut app = AppBuilder::new().build(); // make a simple group let group_addr = instantiate_group(&mut app, 36); @@ -588,7 +588,7 @@ mod test { // TODO: test this with other cutoffs... higher max_vals, higher min_weight so they cannot all be filled #[test] fn simulate_validators() { - let mut app = mock_app(); + let mut app = AppBuilder::new().build(); // make a simple group let group_addr = instantiate_group(&mut app, 36); @@ -623,7 +623,7 @@ mod test { #[test] fn update_metadata_works() { - let mut app = mock_app(); + let mut app = AppBuilder::new().build(); // make a simple group let group_addr = instantiate_group(&mut app, 36); @@ -671,18 +671,18 @@ mod test { &[], ) .unwrap_err(); - assert_eq!(err, (ContractError::InvalidMoniker {}).to_string()); + assert_eq!(ContractError::InvalidMoniker {}, err.downcast().unwrap()); // test that non-members cannot set data let err = app .execute_contract(Addr::unchecked("random"), valset_addr, &exec, &[]) .unwrap_err(); - assert_eq!(err, (ContractError::Unauthorized {}).to_string()); + assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap()); } #[test] fn validator_list() { - let mut app = mock_app(); + let mut app = AppBuilder::new().build(); // make a simple group let group_addr = instantiate_group(&mut app, 36); @@ -800,7 +800,7 @@ mod test { #[test] fn end_block_run() { - let mut app = mock_app(); + let mut app = AppBuilder::new().build(); // make a simple group let group_addr = instantiate_group(&mut app, 36); diff --git a/contracts/tgrade-valset/src/test_helpers.rs b/contracts/tgrade-valset/src/test_helpers.rs index 3e6ba7b0..8f2e81ca 100644 --- a/contracts/tgrade-valset/src/test_helpers.rs +++ b/contracts/tgrade-valset/src/test_helpers.rs @@ -1,7 +1,6 @@ #![cfg(test)] -use cosmwasm_std::testing::{mock_env, MockApi, MockStorage}; use cosmwasm_std::{Addr, Binary}; -use cw_multi_test::{App, BankKeeper, Contract, ContractWrapper}; +use cw_multi_test::{Contract, ContractWrapper}; use tg4::Member; use tg_bindings::{Pubkey, TgradeMsg}; @@ -11,14 +10,6 @@ use crate::state::ValidatorInfo; const ED25519_PUBKEY_LENGTH: usize = 32; -pub fn mock_app() -> App { - let env = mock_env(); - let api = MockApi::default(); - let bank = BankKeeper::new(); - - App::new(api, env.block, bank, MockStorage::new()) -} - pub fn contract_valset() -> Box> { let contract = ContractWrapper::new( crate::contract::execute, diff --git a/contracts/tgrade-valset/src/test_valset_stake.rs b/contracts/tgrade-valset/src/test_valset_stake.rs index 3a99e67d..f15be47b 100644 --- a/contracts/tgrade-valset/src/test_valset_stake.rs +++ b/contracts/tgrade-valset/src/test_valset_stake.rs @@ -8,14 +8,14 @@ use tg_bindings::TgradeMsg; use tg4_stake::{msg::ExecuteMsg, state::Duration}; -use cw_multi_test::{App, Contract, ContractWrapper, Executor}; +use cw_multi_test::{App, AppBuilder, Contract, ContractWrapper, Executor}; use crate::msg::{ ConfigResponse, EpochResponse, InstantiateMsg, ListActiveValidatorsResponse, QueryMsg, ValidatorResponse, }; use crate::state::ValidatorInfo; -use crate::test_helpers::{addrs, contract_valset, mock_app, valid_operator}; +use crate::test_helpers::{addrs, contract_valset, valid_operator}; const EPOCH_LENGTH: u64 = 100; @@ -41,7 +41,7 @@ fn epoch_reward() -> Coin { } fn contract_stake() -> Box> { - let contract = ContractWrapper::new_with_empty( + let contract = ContractWrapper::new( tg4_stake::contract::execute, tg4_stake::contract::instantiate, tg4_stake::contract::query, @@ -79,6 +79,7 @@ fn instantiate_stake(app: &mut App) -> Addr { unbonding_period: Duration::new_from_seconds(1234), admin: admin.clone(), preauths: None, + auto_return_limit: 0, }; app.instantiate_contract( stake_id, @@ -134,7 +135,7 @@ fn unbond(app: &mut App, addr: &Addr, stake_addr: &Addr, tokens: u128 #[test] fn init_and_query_state() { - let mut app = mock_app(); + let mut app = AppBuilder::new().build(); // make a simple stake let stake_addr = instantiate_stake(&mut app); @@ -203,7 +204,7 @@ fn init_and_query_state() { #[test] fn simulate_validators() { - let mut app = mock_app(); + let mut app = AppBuilder::new().build(); // make a simple stake let stake_addr = instantiate_stake(&mut app); diff --git a/packages/controllers/Cargo.toml b/packages/controllers/Cargo.toml index e2e7455c..8074ab1c 100644 --- a/packages/controllers/Cargo.toml +++ b/packages/controllers/Cargo.toml @@ -11,8 +11,9 @@ homepage = "https://tgrade.finance" [dependencies] cosmwasm-std = { version = "0.16.0" } -cw0 = { version = "0.8.0" } -cw-storage-plus = { version = "0.8.0" } +cw0 = { version = "0.9.0" } +cw-storage-plus = { version = "0.9.0" } schemars = "0.8.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } thiserror = { version = "1.0.21" } +tg-bindings = { path = "../bindings", version = "0.3" } diff --git a/packages/controllers/src/hooks.rs b/packages/controllers/src/hooks.rs index 90d6407c..a55b7531 100644 --- a/packages/controllers/src/hooks.rs +++ b/packages/controllers/src/hooks.rs @@ -2,8 +2,11 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use thiserror::Error; -use cosmwasm_std::{Addr, StdError, StdResult, Storage, SubMsg}; +use cosmwasm_std::{Addr, StdError, StdResult, Storage}; use cw_storage_plus::Item; +use tg_bindings::TgradeMsg; + +type SubMsg = cosmwasm_std::SubMsg; // this is copied from cw4 // TODO: pull into cw0 as common dep diff --git a/packages/tg4/Cargo.toml b/packages/tg4/Cargo.toml index c9fc5dff..811ccf17 100644 --- a/packages/tg4/Cargo.toml +++ b/packages/tg4/Cargo.toml @@ -11,6 +11,7 @@ homepage = "https://tgrade.finance" cosmwasm-std = { version = "0.16.0" } schemars = "0.8.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } +tg-bindings = { path = "../bindings", version = "0.3" } [dev-dependencies] cosmwasm-schema = { version = "0.16.0" } diff --git a/packages/tg4/src/helpers.rs b/packages/tg4/src/helpers.rs index 7c4b8905..68721147 100644 --- a/packages/tg4/src/helpers.rs +++ b/packages/tg4/src/helpers.rs @@ -3,8 +3,9 @@ use serde::{Deserialize, Serialize}; use cosmwasm_std::{ from_slice, to_binary, to_vec, Addr, Binary, ContractResult, Empty, QuerierWrapper, - QueryRequest, StdError, StdResult, SubMsg, SystemResult, WasmMsg, WasmQuery, + QueryRequest, StdError, StdResult, SystemResult, WasmMsg, WasmQuery, }; +use tg_bindings::TgradeMsg; use crate::msg::Tg4ExecuteMsg; use crate::query::HooksResponse; @@ -12,6 +13,8 @@ use crate::{ member_key, AdminResponse, Member, MemberListResponse, MemberResponse, Tg4QueryMsg, TOTAL_KEY, }; +pub type SubMsg = cosmwasm_std::SubMsg; + /// Tg4Contract is a wrapper around Addr that provides a lot of helpers /// for working with tg4 contracts /// diff --git a/packages/tg4/src/hook.rs b/packages/tg4/src/hook.rs index c1f77547..0db529c7 100644 --- a/packages/tg4/src/hook.rs +++ b/packages/tg4/src/hook.rs @@ -1,7 +1,10 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use cosmwasm_std::{to_binary, Binary, CosmosMsg, StdResult, WasmMsg}; +use cosmwasm_std::{to_binary, Binary, StdResult, WasmMsg}; +use tg_bindings::TgradeMsg; + +type CosmosMsg = cosmwasm_std::CosmosMsg; /// MemberDiff shows the old and new states for a given tg4 member /// They cannot both be None.