diff --git a/Cargo.lock b/Cargo.lock index 2e9292bfe..39092ffbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -739,13 +739,13 @@ dependencies = [ "cw-utils 1.0.3", "cw2 1.1.2", "dao-interface", + "dao-proposal-single", "dao-testing", + "dao-voting 2.4.1", + "dao-voting-token-staked", "getrandom", - "integer-cbrt", - "integer-sqrt", "osmosis-std", "osmosis-test-tube", - "rust_decimal", "serde", "serde_json", "speculoos", diff --git a/contracts/dao-dao-core/schema/dao-dao-core.json b/contracts/dao-dao-core/schema/dao-dao-core.json index 624208e82..4581a2a16 100644 --- a/contracts/dao-dao-core/schema/dao-dao-core.json +++ b/contracts/dao-dao-core/schema/dao-dao-core.json @@ -1723,7 +1723,7 @@ "additionalProperties": false }, { - "description": "Lists all of the items associted with the contract. For example, given the items `{ \"group\": \"foo\", \"subdao\": \"bar\"}` this query would return `[(\"group\", \"foo\"), (\"subdao\", \"bar\")]`.", + "description": "Lists all of the items associated with the contract. For example, given the items `{ \"group\": \"foo\", \"subdao\": \"bar\"}` this query would return `[(\"group\", \"foo\"), (\"subdao\", \"bar\")]`.", "type": "object", "required": [ "list_items" diff --git a/contracts/external/cw-abc/Cargo.toml b/contracts/external/cw-abc/Cargo.toml index 1d11c4637..3b77b54f3 100644 --- a/contracts/external/cw-abc/Cargo.toml +++ b/contracts/external/cw-abc/Cargo.toml @@ -37,9 +37,6 @@ cw-ownable = { workspace = true } cw-paginate-storage = { workspace = true } cw-tokenfactory-issuer = { workspace = true, features = ["library"] } dao-interface = { workspace = true } -rust_decimal = { workspace = true } -integer-sqrt = { workspace = true } -integer-cbrt = { workspace = true } getrandom = { workspace = true, features = ["js"] } thiserror = { workspace = true } cw-curves = { workspace = true } @@ -53,4 +50,7 @@ osmosis-std = { workspace = true } osmosis-test-tube = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -cw-tokenfactory-issuer = { workspace = true } \ No newline at end of file +cw-tokenfactory-issuer = { workspace = true } +dao-voting-token-staked = { workspace = true } +dao-proposal-single = { workspace = true } +dao-voting = { workspace = true } \ No newline at end of file diff --git a/contracts/external/cw-abc/src/abc.rs b/contracts/external/cw-abc/src/abc.rs index 5fec074a1..5eaadb67a 100644 --- a/contracts/external/cw-abc/src/abc.rs +++ b/contracts/external/cw-abc/src/abc.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{ensure, Decimal as StdDecimal, Uint128}; +use cosmwasm_std::{ensure, Decimal, Uint128}; use cw_curves::{ curves::{Constant, Linear, SquareRoot}, utils::decimal, @@ -45,9 +45,9 @@ pub struct HatchConfig { /// The initial raise range (min, max) in the reserve token pub initial_raise: MinMax, /// The initial allocation (θ), percentage of the initial raise allocated to the Funding Pool - pub entry_fee: StdDecimal, + pub entry_fee: Decimal, /// Exit tax for the hatch phase - pub exit_fee: StdDecimal, + pub exit_fee: Decimal, } impl HatchConfig { @@ -69,14 +69,14 @@ impl HatchConfig { ); ensure!( - self.entry_fee <= StdDecimal::percent(100u64), + self.entry_fee <= Decimal::percent(100u64), ContractError::HatchPhaseConfigError( "Initial allocation percentage must be between 0 and 100.".to_string() ) ); ensure!( - self.exit_fee <= StdDecimal::percent(100u64), + self.exit_fee <= Decimal::percent(100u64), ContractError::HatchPhaseConfigError( "Exit taxation percentage must be less than or equal to 100.".to_string() ) @@ -90,23 +90,23 @@ impl HatchConfig { pub struct OpenConfig { /// Percentage of capital put into the Reserve Pool during the Open phase /// when buying from the curve. - pub entry_fee: StdDecimal, + pub entry_fee: Decimal, /// Exit taxation ratio - pub exit_fee: StdDecimal, + pub exit_fee: Decimal, } impl OpenConfig { /// Validate the open config pub fn validate(&self) -> Result<(), ContractError> { ensure!( - self.entry_fee <= StdDecimal::percent(100u64), + self.entry_fee <= Decimal::percent(100u64), ContractError::OpenPhaseConfigError( "Reserve percentage must be between 0 and 100.".to_string() ) ); ensure!( - self.exit_fee <= StdDecimal::percent(100u64), + self.exit_fee <= Decimal::percent(100u64), ContractError::OpenPhaseConfigError( "Exit taxation percentage must be between 0 and 100.".to_string() ) @@ -247,12 +247,12 @@ mod unit_tests { min: Uint128::one(), max: Uint128::from(1000000u128), }, - entry_fee: StdDecimal::percent(10u64), - exit_fee: StdDecimal::percent(10u64), + entry_fee: Decimal::percent(10u64), + exit_fee: Decimal::percent(10u64), }, open: OpenConfig { - entry_fee: StdDecimal::percent(10u64), - exit_fee: StdDecimal::percent(10u64), + entry_fee: Decimal::percent(10u64), + exit_fee: Decimal::percent(10u64), }, closed: ClosedConfig {}, }; diff --git a/contracts/external/cw-abc/src/commands.rs b/contracts/external/cw-abc/src/commands.rs index 48f8141fe..ee38dff6d 100644 --- a/contracts/external/cw-abc/src/commands.rs +++ b/contracts/external/cw-abc/src/commands.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ - ensure, to_json_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal as StdDecimal, DepsMut, Env, - MessageInfo, QuerierWrapper, Response, StdResult, Storage, Uint128, WasmMsg, + ensure, to_json_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, + QuerierWrapper, Response, StdResult, Storage, Uint128, WasmMsg, }; use cw_tokenfactory_issuer::msg::ExecuteMsg as IssuerExecuteMsg; use cw_utils::must_pay; @@ -212,7 +212,7 @@ pub fn sell(deps: DepsMut, _env: Env, info: MessageInfo) -> Result (Uint128, Uint128) { let funded = payment * allocation_ratio; let reserved = payment.checked_sub(funded).unwrap(); // Since allocation_ratio is < 1, this subtraction is safe @@ -253,10 +253,7 @@ fn calculate_exit_fee( }; // Ensure the exit fee is not greater than 100% - ensure!( - exit_fee <= StdDecimal::one(), - ContractError::InvalidExitFee {} - ); + ensure!(exit_fee <= Decimal::one(), ContractError::InvalidExitFee {}); // This won't ever overflow because it's checked let taxed_amount = sell_amount * exit_fee; @@ -465,9 +462,7 @@ pub fn update_hatch_allowlist( for allow in to_add { let addr = deps.api.addr_validate(allow.addr.as_str())?; - if !list.has(deps.storage, &addr) { - list.save(deps.storage, &addr, &allow.config)?; - } + list.save(deps.storage, &addr, &allow.config)?; } // Remove addresses from the allowlist diff --git a/contracts/external/cw-abc/src/msg.rs b/contracts/external/cw-abc/src/msg.rs index d0a0baa56..a60b92efe 100644 --- a/contracts/external/cw-abc/src/msg.rs +++ b/contracts/external/cw-abc/src/msg.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Decimal as StdDecimal, Uint128}; +use cosmwasm_std::{Addr, Decimal, Uint128}; use cw_address_like::AddressLike; use crate::{ @@ -42,13 +42,13 @@ pub enum UpdatePhaseConfigMsg { contribution_limits: Option, // TODO what is the minimum used for? initial_raise: Option, - entry_fee: Option, - exit_fee: Option, + entry_fee: Option, + exit_fee: Option, }, /// Update the open phase configuration. Open { - exit_fee: Option, - entry_fee: Option, + exit_fee: Option, + entry_fee: Option, }, /// Update the closed phase configuration. /// TODO Set the curve type to be used on close? @@ -179,7 +179,7 @@ pub struct CurveInfoResponse { /// The amount of tokens in the funding pool pub funding: Uint128, /// Current spot price of the token - pub spot_price: StdDecimal, + pub spot_price: Decimal, /// Current reserve denom pub reserve_denom: String, } diff --git a/contracts/external/cw-abc/src/test_tube/integration_tests.rs b/contracts/external/cw-abc/src/test_tube/integration_tests.rs index 104501169..185eeb92b 100644 --- a/contracts/external/cw-abc/src/test_tube/integration_tests.rs +++ b/contracts/external/cw-abc/src/test_tube/integration_tests.rs @@ -541,3 +541,52 @@ fn test_update_curve() { }) ); } + +#[test] +fn test_dao_hatcher() { + let app = OsmosisTestApp::new(); + let builder = TestEnvBuilder::new(); + let env = builder.default_setup(&app); + let TestEnv { + ref abc, + ref accounts, + .. + } = env; + + // Setup a dao with the 1st half of accounts + let dao = env.setup_dao(); + app.increase_time(1u64); + + // Update hatcher allowlist for DAO membership + let result = abc.execute( + &ExecuteMsg::UpdateHatchAllowlist { + to_add: vec![HatcherAllowlistEntry:: { + addr: dao.contract_addr.to_string(), + config: HatcherAllowlistConfig { + config_type: HatcherAllowlistConfigType::DAO {}, + }, + }], + to_remove: vec![], + }, + &[], + &accounts[0], + ); + assert!(result.is_ok()); + + // Only DAO members (1st half of accounts) can hatch + let result = abc.execute(&ExecuteMsg::Buy {}, &coins(1000, RESERVE), &accounts[0]); + assert!(result.is_ok()); + + // Check not member + let result = abc.execute( + &ExecuteMsg::Buy {}, + &coins(1000, RESERVE), + &accounts[accounts.len() - 1], + ); + assert_eq!( + result.unwrap_err(), + abc.execute_error(ContractError::SenderNotAllowlisted { + sender: accounts[accounts.len() - 1].address().to_string() + }) + ); +} diff --git a/contracts/external/cw-abc/src/test_tube/test_env.rs b/contracts/external/cw-abc/src/test_tube/test_env.rs index f6502282e..27fc00b17 100644 --- a/contracts/external/cw-abc/src/test_tube/test_env.rs +++ b/contracts/external/cw-abc/src/test_tube/test_env.rs @@ -11,12 +11,29 @@ use crate::{ ContractError, }; -use cosmwasm_std::{Coin, Decimal, Uint128}; -use dao_testing::test_tube::cw_tokenfactory_issuer::TokenfactoryIssuer; +use cosmwasm_std::{to_json_binary, Addr, Coin, Decimal, Uint128}; +use cw_utils::Duration; +use dao_interface::{ + state::{Admin, ModuleInstantiateInfo}, + token::{DenomUnit, InitialBalance, NewDenomMetadata, NewTokenInfo}, + voting::DenomResponse, +}; +use dao_testing::test_tube::{ + cw_tokenfactory_issuer::TokenfactoryIssuer, dao_dao_core::DaoCore, + dao_proposal_single::DaoProposalSingle, dao_voting_token_staked::TokenVotingContract, +}; +use dao_voting::{ + pre_propose::PreProposeInfo, + threshold::{ActiveThreshold, PercentageThreshold, Threshold}, +}; +use dao_voting_token_staked::msg::TokenInfo; use osmosis_test_tube::{ - osmosis_std::types::cosmos::bank::v1beta1::QueryAllBalancesRequest, - osmosis_std::types::cosmwasm::wasm::v1::MsgExecuteContractResponse, Account, Bank, Module, - OsmosisTestApp, RunnerError, RunnerExecuteResult, SigningAccount, Wasm, + osmosis_std::types::{ + cosmos::bank::v1beta1::QueryAllBalancesRequest, + cosmwasm::wasm::v1::MsgExecuteContractResponse, + }, + Account, Bank, Module, OsmosisTestApp, RunnerError, RunnerExecuteResult, RunnerResult, + SigningAccount, Wasm, }; use serde::de::DeserializeOwned; use std::fmt::Debug; @@ -51,6 +68,111 @@ impl<'a> TestEnv<'a> { Bank::new(self.app) } + pub fn setup_dao(&self) -> DaoCore<'a> { + // Only the 1st half of self.accounts are part of the DAO + let initial_balances: Vec = self + .accounts + .iter() + .take(self.accounts.len() / 2) + .map(|acc| InitialBalance { + address: acc.address(), + amount: Uint128::from(100u128), + }) + .collect(); + + let vp_contract_id = TokenVotingContract::upload(self.app, &self.accounts[0]).unwrap(); + let proposal_single_id = DaoProposalSingle::upload(self.app, &self.accounts[0]).unwrap(); + + let msg = dao_interface::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that makes DAO tooling".to_string(), + image_url: None, + automatically_add_cw20s: false, + automatically_add_cw721s: false, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: vp_contract_id, + msg: to_json_binary(&dao_voting_token_staked::msg::InstantiateMsg { + token_info: TokenInfo::New(NewTokenInfo { + token_issuer_code_id: self.tf_issuer.code_id, + subdenom: DENOM.to_string(), + metadata: Some(NewDenomMetadata { + description: "Awesome token, get it meow!".to_string(), + additional_denom_units: Some(vec![DenomUnit { + denom: "cat".to_string(), + exponent: 6, + aliases: vec![], + }]), + display: "cat".to_string(), + name: "Cat Token".to_string(), + symbol: "CAT".to_string(), + }), + initial_balances, + initial_dao_balance: Some(Uint128::new(900)), + }), + unstaking_duration: Some(Duration::Time(2)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(75), + }), + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Voting Module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: proposal_single_id, + msg: to_json_binary(&dao_proposal_single::msg::InstantiateMsg { + min_voting_period: None, + threshold: Threshold::ThresholdQuorum { + threshold: PercentageThreshold::Majority {}, + quorum: PercentageThreshold::Percent(Decimal::percent(35)), + }, + max_voting_period: Duration::Time(432000), + allow_revoting: false, + only_members_execute: true, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Proposal Module".to_string(), + }], + initial_items: None, + }; + + let dao = DaoCore::new(self.app, &msg, &self.accounts[0], &[]).unwrap(); + + // Get voting module address, setup vp_contract helper + let vp_addr: Addr = dao + .query(&dao_interface::msg::QueryMsg::VotingModule {}) + .unwrap(); + let vp_contract = + TokenVotingContract::new_with_values(self.app, vp_contract_id, vp_addr.to_string()) + .unwrap(); + + // Get the denom + let result: RunnerResult = + vp_contract.query(&dao_voting_token_staked::msg::QueryMsg::Denom {}); + let denom = result.unwrap().denom; + + // Stake all members + for acc in self.accounts.iter().take(self.accounts.len() / 2) { + vp_contract + .execute( + &dao_voting_token_staked::msg::ExecuteMsg::Stake {}, + &[Coin::new(100, denom.clone())], + acc, + ) + .unwrap(); + } + + dao + } + pub fn assert_account_balances( &self, account: SigningAccount, diff --git a/contracts/external/cw-tokenfactory-issuer/schema/cw-tokenfactory-issuer.json b/contracts/external/cw-tokenfactory-issuer/schema/cw-tokenfactory-issuer.json index 5a5ca6b86..7f7f44465 100644 --- a/contracts/external/cw-tokenfactory-issuer/schema/cw-tokenfactory-issuer.json +++ b/contracts/external/cw-tokenfactory-issuer/schema/cw-tokenfactory-issuer.json @@ -21,7 +21,7 @@ ], "properties": { "subdenom": { - "description": "component of fulldenom (`factory//`).", + "description": "component of full denom (`factory//`).", "type": "string" } }, @@ -31,7 +31,7 @@ "additionalProperties": false }, { - "description": "`ExistingToken` will use already created token. So to set this up, Token Factory admin for the existing token needs trasfer admin over to this contract, and optionally set the `BeforeSendHook` manually.", + "description": "`ExistingToken` will use already created token. So to set this up, Token Factory admin for the existing token needs transfer admin over to this contract, and optionally set the `BeforeSendHook` manually.", "type": "object", "required": [ "existing_token" @@ -60,7 +60,7 @@ "description": "State changing methods available to this smart contract.", "oneOf": [ { - "description": "Allow adds the target address to the allowlist to be able to send or recieve tokens even if the token is frozen. Token Factory's BeforeSendHook listener must be set to this contract in order for this feature to work.\n\nThis functionality is intedended for DAOs who do not wish to have a their tokens liquid while bootstrapping their DAO. For example, a DAO may wish to white list a Token Staking contract (to allow users to stake their tokens in the DAO) or a Merkle Drop contract (to allow users to claim their tokens).", + "description": "Allow adds the target address to the allowlist to be able to send or receive tokens even if the token is frozen. Token Factory's BeforeSendHook listener must be set to this contract in order for this feature to work.\n\nThis functionality is intended for DAOs who do not wish to have a their tokens liquid while bootstrapping their DAO. For example, a DAO may wish to white list a Token Staking contract (to allow users to stake their tokens in the DAO) or a Merkle Drop contract (to allow users to claim their tokens).", "type": "object", "required": [ "allow" @@ -86,7 +86,7 @@ "additionalProperties": false }, { - "description": "Burn token to address. Burn allowance is required and wiil be deducted after successful burn.", + "description": "Burn token to address. Burn allowance is required and will be deducted after successful burn.", "type": "object", "required": [ "burn" @@ -112,7 +112,7 @@ "additionalProperties": false }, { - "description": "Mint token to address. Mint allowance is required and wiil be deducted after successful mint.", + "description": "Mint token to address. Mint allowance is required and will be deducted after successful mint.", "type": "object", "required": [ "mint" @@ -138,7 +138,7 @@ "additionalProperties": false }, { - "description": "Deny adds the target address to the denylist, whis prevents them from sending/receiving the token attached to this contract tokenfactory's BeforeSendHook listener must be set to this contract in order for this feature to work as intended.", + "description": "Deny adds the target address to the denylist, which prevents them from sending/receiving the token attached to this contract tokenfactory's BeforeSendHook listener must be set to this contract in order for this feature to work as intended.", "type": "object", "required": [ "deny" @@ -601,7 +601,7 @@ "additionalProperties": false }, { - "description": "Enumerates over all burn allownances. Response: AllowancesResponse", + "description": "Enumerates over all burn allowances. Response: AllowancesResponse", "type": "object", "required": [ "burn_allowances" @@ -653,7 +653,7 @@ "additionalProperties": false }, { - "description": "Enumerates over all mint allownances. Response: AllowancesResponse", + "description": "Enumerates over all mint allowances. Response: AllowancesResponse", "type": "object", "required": [ "mint_allowances" diff --git a/contracts/external/dao-abc-factory/schema/dao-abc-factory.json b/contracts/external/dao-abc-factory/schema/dao-abc-factory.json index db9be42c1..7b0314307 100644 --- a/contracts/external/dao-abc-factory/schema/dao-abc-factory.json +++ b/contracts/external/dao-abc-factory/schema/dao-abc-factory.json @@ -244,11 +244,74 @@ }, "additionalProperties": false }, + "HatcherAllowlistConfig": { + "description": "The configuration for a member of the hatcher allowlist", + "type": "object", + "required": [ + "config_type" + ], + "properties": { + "config_type": { + "description": "The type of the configuration", + "allOf": [ + { + "$ref": "#/definitions/HatcherAllowlistConfigType" + } + ] + } + }, + "additionalProperties": false + }, + "HatcherAllowlistConfigType": { + "oneOf": [ + { + "type": "object", + "required": [ + "d_a_o" + ], + "properties": { + "d_a_o": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "HatcherAllowlistEntry_for_String": { + "type": "object", + "required": [ + "addr", + "config" + ], + "properties": { + "addr": { + "type": "string" + }, + "config": { + "$ref": "#/definitions/HatcherAllowlistConfig" + } + }, + "additionalProperties": false + }, "InstantiateMsg": { "type": "object", "required": [ "curve_type", - "fees_recipient", "phase_config", "reserve", "supply", @@ -263,9 +326,12 @@ } ] }, - "fees_recipient": { - "description": "The recipient for any fees collected from bonding curve operation", - "type": "string" + "funding_pool_forwarding": { + "description": "An optional address for automatically forwarding funding pool gains", + "type": [ + "string", + "null" + ] }, "hatcher_allowlist": { "description": "TODO different ways of doing this, for example DAO members? Using a whitelist contract? Merkle tree? Hatcher allowlist", @@ -274,7 +340,7 @@ "null" ], "items": { - "type": "string" + "$ref": "#/definitions/HatcherAllowlistEntry_for_String" } }, "phase_config": { @@ -311,7 +377,7 @@ "additionalProperties": false }, "MinMax": { - "description": "Struct for minimium and maximum values", + "description": "Struct for minimum and maximum values", "type": "object", "required": [ "max", diff --git a/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json b/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json index c5203092d..3b6a7e888 100644 --- a/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json +++ b/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json @@ -32,7 +32,7 @@ ] }, "min_voting_period": { - "description": "The minimum amount of time a proposal must be open before passing. A proposal may fail before this amount of time has elapsed, but it will not pass. This can be useful for preventing governance attacks wherein an attacker aquires a large number of tokens and forces a proposal through.", + "description": "The minimum amount of time a proposal must be open before passing. A proposal may fail before this amount of time has elapsed, but it will not pass. This can be useful for preventing governance attacks wherein an attacker acquires a large number of tokens and forces a proposal through.", "anyOf": [ { "$ref": "#/definitions/Duration"