From 6f1e4d1e620cd7d7e3a81ed19ad4d2f9b47b0032 Mon Sep 17 00:00:00 2001
From: Philip Offtermatt
Date: Mon, 28 Oct 2024 13:51:22 +0100
Subject: [PATCH 01/15] Add liquidity deployment message
---
contracts/hydro/src/msg.rs | 23 ++++++++++++++++++++++-
1 file changed, 22 insertions(+), 1 deletion(-)
diff --git a/contracts/hydro/src/msg.rs b/contracts/hydro/src/msg.rs
index fe3c8da..dfa2d6f 100644
--- a/contracts/hydro/src/msg.rs
+++ b/contracts/hydro/src/msg.rs
@@ -1,4 +1,4 @@
-use cosmwasm_std::{Timestamp, Uint128};
+use cosmwasm_std::{Coin, Timestamp, Uint128};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -86,6 +86,10 @@ pub enum ExecuteMsg {
WithdrawICQFunds {
amount: Uint128,
},
+
+ SetLiquidityDeploymentsForRound {
+ deployments: Vec,
+ },
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
@@ -99,3 +103,20 @@ pub struct ProposalToLockups {
pub struct MigrateMsg {
pub new_first_round_start: Timestamp,
}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct LiquidityDeployment {
+ pub round_id: u64,
+ pub tranche_id: u64,
+ pub proposal_id: u64,
+ pub destinations: Vec,
+ // allocation assigned to the proposal to be deployed during the specified round
+ pub deployed_funds: Vec,
+ // allocation at the end of the last round for the proposal. this is 0 if no equivalent proposal exicted in the last round.
+ // if this is a "repeating" proposal (where proposals in subsequent rounds are for the same underlying liqudiity deployment),
+ // it's the allocation prior to any potential clawback or increase
+ pub funds_before_deployment: Vec,
+ // how many rounds this proposal has been in effect for
+ // if this is a "repeating" proposal
+ pub total_rounds: u64,
+}
From d1f5932bb828e10dd53b20ca38d18505e2adacfe Mon Sep 17 00:00:00 2001
From: Philip Offtermatt
Date: Mon, 28 Oct 2024 15:00:22 +0100
Subject: [PATCH 02/15] Add liquidity deployment message
---
contracts/hydro/src/contract.rs | 45 ++++++++++++++++++-
contracts/hydro/src/msg.rs | 12 +++--
contracts/hydro/src/state.rs | 1 +
contracts/hydro/src/testing.rs | 12 +++++
.../hydro/src/testing_fractional_voting.rs | 4 +-
.../hydro/src/testing_lsm_integration.rs | 4 ++
contracts/tribute/src/testing.rs | 8 ++++
7 files changed, 80 insertions(+), 6 deletions(-)
diff --git a/contracts/hydro/src/contract.rs b/contracts/hydro/src/contract.rs
index e1e938f..d6d8f8c 100644
--- a/contracts/hydro/src/contract.rs
+++ b/contracts/hydro/src/contract.rs
@@ -17,7 +17,7 @@ use crate::lsm_integration::{
get_validator_power_ratio_for_round, initialize_validator_store, validate_denom,
COSMOS_VALIDATOR_PREFIX,
};
-use crate::msg::{ExecuteMsg, InstantiateMsg, ProposalToLockups, TrancheInfo};
+use crate::msg::{ExecuteMsg, InstantiateMsg, LiquidityDeployment, ProposalToLockups, TrancheInfo};
use crate::query::{
AllUserLockupsResponse, ConstantsResponse, CurrentRoundResponse, ExpiredUserLockupsResponse,
ICQManagersResponse, LockEntryWithPower, ProposalResponse, QueryMsg,
@@ -158,7 +158,16 @@ pub fn execute(
tranche_id,
title,
description,
- } => create_proposal(deps, env, info, tranche_id, title, description),
+ minimum_atom_liquidity_request,
+ } => create_proposal(
+ deps,
+ env,
+ info,
+ tranche_id,
+ title,
+ description,
+ minimum_atom_liquidity_request,
+ ),
ExecuteMsg::Vote {
tranche_id,
proposals_votes,
@@ -183,6 +192,13 @@ pub fn execute(
ExecuteMsg::AddICQManager { address } => add_icq_manager(deps, info, address),
ExecuteMsg::RemoveICQManager { address } => remove_icq_manager(deps, info, address),
ExecuteMsg::WithdrawICQFunds { amount } => withdraw_icq_funds(deps, info, amount),
+ ExecuteMsg::SetRoundLiquidityDeployments {
+ round_id,
+ tranche_id,
+ liquidity_deployment,
+ } => {
+ set_round_liquidity_deployments(deps, info, round_id, tranche_id, liquidity_deployment)
+ }
}
}
@@ -573,6 +589,7 @@ fn create_proposal(
tranche_id: u64,
title: String,
description: String,
+ minimum_atom_liquidity_request: Uint128,
) -> Result, ContractError> {
let constants = CONSTANTS.load(deps.storage)?;
validate_contract_is_not_paused(&constants)?;
@@ -600,6 +617,7 @@ fn create_proposal(
percentage: Uint128::zero(),
title: title.trim().to_string(),
description: description.trim().to_string(),
+ minimum_atom_liquidity_request,
};
PROP_ID.save(deps.storage, &(proposal_id + 1))?;
@@ -1236,6 +1254,29 @@ fn withdraw_icq_funds(
}))
}
+fn set_round_liquidity_deployments(
+ deps: DepsMut,
+ info: MessageInfo,
+ round_id: u64,
+ tranche_id: u64,
+ liquidity_deployments: Vec,
+) -> Result, ContractError> {
+ let constants = CONSTANTS.load(deps.storage)?;
+
+ validate_contract_is_not_paused(&constants)?;
+ validate_sender_is_whitelist_admin(&deps, &info)?;
+
+ let round_end = compute_round_end(&constants, round_id)?;
+
+ let mut response = Response::new()
+ .add_attribute("action", "set_round_liquidity_deployments")
+ .add_attribute("sender", info.sender)
+ .add_attribute("round_id", round_id.to_string())
+ .add_attribute("tranche_id", tranche_id.to_string());
+
+ Ok(response)
+}
+
fn validate_sender_is_whitelist_admin(
deps: &DepsMut,
info: &MessageInfo,
diff --git a/contracts/hydro/src/msg.rs b/contracts/hydro/src/msg.rs
index dfa2d6f..26d3906 100644
--- a/contracts/hydro/src/msg.rs
+++ b/contracts/hydro/src/msg.rs
@@ -46,6 +46,11 @@ pub enum ExecuteMsg {
tranche_id: u64,
title: String,
description: String,
+ // the minimum amount of liquidity, in ATOM equivalent, that the project wants
+ // to receive. If they would receive less than this amount of liquidity,
+ // it is assumed that no liquidity will be deployed to them.
+ // If this is set to 0, the project is assumed to not have a minimum requirement.
+ minimum_atom_liquidity_request: Uint128,
},
Vote {
tranche_id: u64,
@@ -86,9 +91,10 @@ pub enum ExecuteMsg {
WithdrawICQFunds {
amount: Uint128,
},
-
- SetLiquidityDeploymentsForRound {
- deployments: Vec,
+ SetRoundLiquidityDeployments {
+ round_id: u64,
+ tranche_id: u64,
+ liquidity_deployment: Vec,
},
}
diff --git a/contracts/hydro/src/state.rs b/contracts/hydro/src/state.rs
index c0f5e23..20e3785 100644
--- a/contracts/hydro/src/state.rs
+++ b/contracts/hydro/src/state.rs
@@ -48,6 +48,7 @@ pub struct Proposal {
pub description: String,
pub power: Uint128,
pub percentage: Uint128,
+ pub minimum_atom_liquidity_request: Uint128,
}
// VOTE_MAP: key((round_id, tranche_id), sender_addr, lock_id) -> Vote
diff --git a/contracts/hydro/src/testing.rs b/contracts/hydro/src/testing.rs
index 6801cf9..47d1b70 100644
--- a/contracts/hydro/src/testing.rs
+++ b/contracts/hydro/src/testing.rs
@@ -317,6 +317,7 @@ fn create_proposal_basic_test() {
tranche_id: 1,
title: "proposal title 1".to_string(),
description: "proposal description 1".to_string(),
+ minimum_atom_liquidity_request: Uint128::zero(),
};
let res = execute(deps.as_mut(), env.clone(), info.clone(), msg1.clone());
assert!(res.is_ok());
@@ -325,6 +326,7 @@ fn create_proposal_basic_test() {
tranche_id: 1,
title: "proposal title 2".to_string(),
description: "proposal description 2".to_string(),
+ minimum_atom_liquidity_request: Uint128::zero(),
};
let res = execute(deps.as_mut(), env.clone(), info.clone(), msg2.clone());
assert!(res.is_ok());
@@ -426,6 +428,7 @@ fn proposal_power_change_on_lock_and_refresh_test() {
tranche_id: prop_info.0,
title: prop_info.1,
description: prop_info.2,
+ minimum_atom_liquidity_request: Uint128::zero(),
};
let res = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone());
@@ -670,6 +673,7 @@ fn proposal_power_change_on_lock_and_refresh_test() {
tranche_id: first_tranche_id,
title: "proposal title 4".to_string(),
description: "proposal description 4".to_string(),
+ minimum_atom_liquidity_request: Uint128::zero(),
};
let res = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone());
@@ -811,6 +815,7 @@ fn vote_test_with_start_time(start_time: Timestamp, current_round_id: u64) {
tranche_id: prop_info.0,
title: prop_info.1,
description: prop_info.2,
+ minimum_atom_liquidity_request: Uint128::zero(),
};
let res = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone());
@@ -920,6 +925,7 @@ fn multi_tranches_test() {
tranche_id: 1,
title: "proposal title 1".to_string(),
description: "proposal description 1".to_string(),
+ minimum_atom_liquidity_request: Uint128::zero(),
};
let res = execute(deps.as_mut(), env.clone(), info.clone(), msg1.clone());
assert!(res.is_ok());
@@ -928,6 +934,7 @@ fn multi_tranches_test() {
tranche_id: 1,
title: "proposal title 2".to_string(),
description: "proposal description 2".to_string(),
+ minimum_atom_liquidity_request: Uint128::zero(),
};
let res = execute(deps.as_mut(), env.clone(), info.clone(), msg2.clone());
assert!(res.is_ok());
@@ -937,6 +944,7 @@ fn multi_tranches_test() {
tranche_id: 2,
title: "proposal title 3".to_string(),
description: "proposal description 3".to_string(),
+ minimum_atom_liquidity_request: Uint128::zero(),
};
let res = execute(deps.as_mut(), env.clone(), info.clone(), msg3.clone());
assert!(res.is_ok());
@@ -945,6 +953,7 @@ fn multi_tranches_test() {
tranche_id: 2,
title: "proposal title 4".to_string(),
description: "proposal description 4".to_string(),
+ minimum_atom_liquidity_request: Uint128::zero(),
};
let res = execute(deps.as_mut(), env.clone(), info.clone(), msg4.clone());
assert!(res.is_ok());
@@ -1087,6 +1096,7 @@ fn test_query_round_tranche_proposals_pagination() {
tranche_id: 1,
title: format!("proposal title {}", i),
description: format!("proposal description {}", i),
+ minimum_atom_liquidity_request: Uint128::zero(),
};
let _ = execute(
deps.as_mut(),
@@ -1755,6 +1765,7 @@ fn contract_pausing_test() {
tranche_id: 0,
title: "".to_string(),
description: "".to_string(),
+ minimum_atom_liquidity_request: Uint128::zero(),
},
ExecuteMsg::Vote {
tranche_id: 0,
@@ -1822,6 +1833,7 @@ pub fn whitelist_proposal_submission_test() {
tranche_id: 1,
title: "proposal title".to_string(),
description: "proposal description".to_string(),
+ minimum_atom_liquidity_request: Uint128::zero(),
};
let res = execute(
diff --git a/contracts/hydro/src/testing_fractional_voting.rs b/contracts/hydro/src/testing_fractional_voting.rs
index 49e32b1..e170ddf 100644
--- a/contracts/hydro/src/testing_fractional_voting.rs
+++ b/contracts/hydro/src/testing_fractional_voting.rs
@@ -2,7 +2,7 @@ use std::{collections::HashMap, slice::Iter};
use cosmwasm_std::{
testing::{mock_env, MockApi, MockStorage},
- Addr, Coin, Env, OwnedDeps,
+ Addr, Coin, Env, OwnedDeps, Uint128,
};
use neutron_sdk::bindings::query::NeutronQuery;
@@ -453,11 +453,13 @@ fn fractional_voting_test() {
tranche_id: tranche_id_1,
title: "proposal title 1".to_string(),
description: "proposal description 1".to_string(),
+ minimum_atom_liquidity_request: Uint128::zero(),
},
ExecuteMsg::CreateProposal {
tranche_id: tranche_id_1,
title: "proposal title 2".to_string(),
description: "proposal description 2".to_string(),
+ minimum_atom_liquidity_request: Uint128::zero(),
},
];
diff --git a/contracts/hydro/src/testing_lsm_integration.rs b/contracts/hydro/src/testing_lsm_integration.rs
index d060af9..3845893 100644
--- a/contracts/hydro/src/testing_lsm_integration.rs
+++ b/contracts/hydro/src/testing_lsm_integration.rs
@@ -677,6 +677,7 @@ fn lock_tokens_multiple_validators_and_vote() {
tranche_id: 1,
title: "proposal title 1".to_string(),
description: "proposal description 1".to_string(),
+ minimum_atom_liquidity_request: Uint128::zero(),
};
let res = execute(deps.as_mut(), env.clone(), info.clone(), msg1.clone());
assert!(res.is_ok());
@@ -685,6 +686,7 @@ fn lock_tokens_multiple_validators_and_vote() {
tranche_id: 1,
title: "proposal title 2".to_string(),
description: "proposal description 2".to_string(),
+ minimum_atom_liquidity_request: Uint128::zero(),
};
let res = execute(deps.as_mut(), env.clone(), info.clone(), msg2.clone());
assert!(res.is_ok());
@@ -781,6 +783,7 @@ fn validator_set_initialization_test() {
tranche_id: 1,
title: "proposal title".to_string(),
description: "proposal description".to_string(),
+ minimum_atom_liquidity_request: Uint128::zero(),
},
},
ValidatorSetInitializationTestCase {
@@ -861,6 +864,7 @@ fn validator_set_initialization_test() {
tranche_id: 1,
title: "proposal title".to_string(),
description: "proposal description".to_string(),
+ minimum_atom_liquidity_request: Uint128::zero(),
};
let res = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone());
diff --git a/contracts/tribute/src/testing.rs b/contracts/tribute/src/testing.rs
index 1cb5bec..967d8a9 100644
--- a/contracts/tribute/src/testing.rs
+++ b/contracts/tribute/src/testing.rs
@@ -251,6 +251,7 @@ fn add_tribute_test() {
description: "proposal description 1".to_string(),
power: Uint128::new(10000),
percentage: Uint128::zero(),
+ minimum_atom_liquidity_request: Uint128::zero(),
};
let test_cases: Vec = vec![
@@ -378,6 +379,7 @@ fn claim_tribute_test() {
description: "proposal description 1".to_string(),
power: Uint128::new(10000),
percentage: MIN_PROP_PERCENT_FOR_CLAIMABLE_TRIBUTES,
+ minimum_atom_liquidity_request: Uint128::zero(),
};
let mock_proposal2 = Proposal {
round_id: 10,
@@ -387,6 +389,7 @@ fn claim_tribute_test() {
description: "proposal description 2".to_string(),
power: Uint128::new(10000),
percentage: MIN_PROP_PERCENT_FOR_CLAIMABLE_TRIBUTES,
+ minimum_atom_liquidity_request: Uint128::zero(),
};
let mock_proposal3 = Proposal {
round_id: 10,
@@ -396,6 +399,7 @@ fn claim_tribute_test() {
description: "proposal description 3".to_string(),
power: Uint128::new(10000),
percentage: MIN_PROP_PERCENT_FOR_CLAIMABLE_TRIBUTES,
+ minimum_atom_liquidity_request: Uint128::zero(),
};
let mock_proposals = vec![
@@ -755,6 +759,7 @@ fn refund_tribute_test() {
description: "proposal description 1".to_string(),
power: Uint128::new(10000),
percentage: Uint128::zero(),
+ minimum_atom_liquidity_request: Uint128::zero(),
};
let mock_proposals = vec![mock_proposal.clone()];
@@ -767,6 +772,7 @@ fn refund_tribute_test() {
description: "proposal description 2".to_string(),
power: Uint128::new(10000),
percentage: MIN_PROP_PERCENT_FOR_CLAIMABLE_TRIBUTES,
+ minimum_atom_liquidity_request: Uint128::zero(),
}];
let mock_top_n_voting_threshold_reached = vec![Proposal {
@@ -1360,6 +1366,7 @@ fn test_query_outstanding_tribute_claims() {
description: "Description 1".to_string(),
power: Uint128::new(1000),
percentage: Uint128::new(7),
+ minimum_atom_liquidity_request: Uint128::zero(),
},
Proposal {
round_id: 1,
@@ -1369,6 +1376,7 @@ fn test_query_outstanding_tribute_claims() {
description: "Description 2".to_string(),
power: Uint128::new(2000),
percentage: Uint128::new(7),
+ minimum_atom_liquidity_request: Uint128::zero(),
},
];
From 712b6b2331d2b1ad75385aca1868b66f7761c14a Mon Sep 17 00:00:00 2001
From: Philip Offtermatt
Date: Mon, 28 Oct 2024 16:49:03 +0100
Subject: [PATCH 03/15] Add functions to manage liquidity deployments entered
by trusted parties
---
contracts/hydro/src/contract.rs | 146 ++++++++++--
contracts/hydro/src/lib.rs | 3 +
contracts/hydro/src/msg.rs | 28 ++-
contracts/hydro/src/state.rs | 8 +
contracts/hydro/src/testing_deployments.rs | 245 +++++++++++++++++++++
5 files changed, 408 insertions(+), 22 deletions(-)
create mode 100644 contracts/hydro/src/testing_deployments.rs
diff --git a/contracts/hydro/src/contract.rs b/contracts/hydro/src/contract.rs
index d6d8f8c..73ac4f8 100644
--- a/contracts/hydro/src/contract.rs
+++ b/contracts/hydro/src/contract.rs
@@ -32,9 +32,9 @@ use crate::score_keeper::{
};
use crate::state::{
Constants, LockEntry, Proposal, Tranche, ValidatorInfo, Vote, VoteWithPower, CONSTANTS,
- ICQ_MANAGERS, LOCKED_TOKENS, LOCKS_MAP, LOCK_ID, PROPOSAL_MAP, PROPS_BY_SCORE, PROP_ID,
- TRANCHE_ID, TRANCHE_MAP, VALIDATORS_INFO, VALIDATORS_PER_ROUND, VALIDATORS_STORE_INITIALIZED,
- VALIDATOR_TO_QUERY_ID, VOTE_MAP, WHITELIST, WHITELIST_ADMINS,
+ ICQ_MANAGERS, LIQUIDITY_DEPLOYMENTS_MAP, LOCKED_TOKENS, LOCKS_MAP, LOCK_ID, PROPOSAL_MAP,
+ PROPS_BY_SCORE, PROP_ID, TRANCHE_ID, TRANCHE_MAP, VALIDATORS_INFO, VALIDATORS_PER_ROUND,
+ VALIDATORS_STORE_INITIALIZED, VALIDATOR_TO_QUERY_ID, VOTE_MAP, WHITELIST, WHITELIST_ADMINS,
};
use crate::validators_icqs::{
build_create_interchain_query_submsg, handle_delivered_interchain_query_result,
@@ -192,13 +192,33 @@ pub fn execute(
ExecuteMsg::AddICQManager { address } => add_icq_manager(deps, info, address),
ExecuteMsg::RemoveICQManager { address } => remove_icq_manager(deps, info, address),
ExecuteMsg::WithdrawICQFunds { amount } => withdraw_icq_funds(deps, info, amount),
- ExecuteMsg::SetRoundLiquidityDeployments {
+ ExecuteMsg::AddLiquidityDeployment {
round_id,
tranche_id,
- liquidity_deployment,
- } => {
- set_round_liquidity_deployments(deps, info, round_id, tranche_id, liquidity_deployment)
- }
+ proposal_id,
+ destinations,
+ deployed_funds,
+ funds_before_deployment,
+ total_rounds,
+ remaining_rounds,
+ } => add_liquidity_deployment(
+ deps,
+ env,
+ info,
+ round_id,
+ tranche_id,
+ proposal_id,
+ destinations,
+ deployed_funds,
+ funds_before_deployment,
+ total_rounds,
+ remaining_rounds,
+ ),
+ ExecuteMsg::RemoveLiquidityDeployment {
+ round_id,
+ tranche_id,
+ proposal_id,
+ } => remove_liquidity_deployment(deps, info, round_id, tranche_id, proposal_id),
}
}
@@ -1254,26 +1274,126 @@ fn withdraw_icq_funds(
}))
}
-fn set_round_liquidity_deployments(
+// This function will add a given liquidity deployment to the deployments that were performed.
+// This will not actually perform any movement of funds; it is assumed to be called when
+// a trusted party, e.g. a multisig, has performed some deployment,
+// and the contract should be updated to reflect this.
+// This will return an error if:
+// * the given round has not started yet
+// * the given tranche does not exist
+// * the given proposal does not exist
+// * there already is a deployment for the given round, tranche, and proposal
+// Having more arguments rather than nested structures actually makes the json easier to construct when calling this from the outside,
+// so we allow many arguments to this function.
+#[allow(clippy::too_many_arguments)]
+pub fn add_liquidity_deployment(
deps: DepsMut,
+ env: Env,
info: MessageInfo,
round_id: u64,
tranche_id: u64,
- liquidity_deployments: Vec,
+ proposal_id: u64,
+ destinations: Vec,
+ deployed_funds: Vec,
+ funds_before_deployment: Vec,
+ total_rounds: u64,
+ remaining_rounds: u64,
) -> Result, ContractError> {
let constants = CONSTANTS.load(deps.storage)?;
validate_contract_is_not_paused(&constants)?;
validate_sender_is_whitelist_admin(&deps, &info)?;
- let round_end = compute_round_end(&constants, round_id)?;
+ // check that the round has started
+ let current_round_id = compute_current_round_id(&env, &constants)?;
+ if round_id > current_round_id {
+ return Err(ContractError::Std(StdError::generic_err(
+ "Cannot add liquidity deployment for a round that has not started yet",
+ )));
+ }
- let mut response = Response::new()
- .add_attribute("action", "set_round_liquidity_deployments")
+ // check that the tranche with the given id exists
+ TRANCHE_MAP.load(deps.storage, tranche_id)?;
+
+ // check that the proposal with the given id exists
+ PROPOSAL_MAP
+ .load(deps.storage, (round_id, tranche_id, proposal_id))
+ .map_err(|_| {
+ ContractError::Std(StdError::generic_err(format!(
+ "Proposal for round {}, tranche {}, and id {} does not exist",
+ round_id, tranche_id, proposal_id
+ )))
+ })?;
+
+ // check that there is no deployment for the given round, tranche, and proposal
+ if LIQUIDITY_DEPLOYMENTS_MAP
+ .may_load(deps.storage, (round_id, tranche_id, proposal_id))?
+ .is_some()
+ {
+ return Err(ContractError::Std(StdError::generic_err(
+ "There already is a deployment for the given round, tranche, and proposal",
+ )));
+ }
+
+ let deployment = LiquidityDeployment {
+ round_id,
+ tranche_id,
+ proposal_id,
+ destinations,
+ deployed_funds,
+ funds_before_deployment,
+ total_rounds,
+ remaining_rounds,
+ };
+
+ let response = Response::new()
+ .add_attribute("action", "add_round_liquidity_deployments")
+ .add_attribute("sender", info.sender)
+ .add_attribute("round_id", round_id.to_string())
+ .add_attribute("tranche_id", tranche_id.to_string())
+ .add_attribute("proposal_id", proposal_id.to_string())
+ .add_attribute(
+ "deployment",
+ serde_json_wasm::to_string(&deployment).map_err(|_| {
+ ContractError::Std(StdError::generic_err("Failed to serialize deployment"))
+ })?,
+ );
+
+ LIQUIDITY_DEPLOYMENTS_MAP.save(
+ deps.storage,
+ (round_id, tranche_id, proposal_id),
+ &deployment,
+ )?;
+
+ Ok(response)
+}
+
+// This function will remove a given liquidity deployment from the deployments that were performed.
+// The main purpose is to correct a faulty entry added via add_liquidity_deployment.
+// This will return an error if the deployment does not exist.
+pub fn remove_liquidity_deployment(
+ deps: DepsMut,
+ info: MessageInfo,
+ round_id: u64,
+ tranche_id: u64,
+ proposal_id: u64,
+) -> Result, ContractError> {
+ let constants = CONSTANTS.load(deps.storage)?;
+
+ validate_contract_is_not_paused(&constants)?;
+ validate_sender_is_whitelist_admin(&deps, &info)?;
+
+ // check that the deployment exists
+ LIQUIDITY_DEPLOYMENTS_MAP.load(deps.storage, (round_id, tranche_id, proposal_id))?;
+
+ let response = Response::new()
+ .add_attribute("action", "remove_round_liquidity_deployments")
.add_attribute("sender", info.sender)
.add_attribute("round_id", round_id.to_string())
.add_attribute("tranche_id", tranche_id.to_string());
+ LIQUIDITY_DEPLOYMENTS_MAP.remove(deps.storage, (round_id, tranche_id, proposal_id));
+
Ok(response)
}
diff --git a/contracts/hydro/src/lib.rs b/contracts/hydro/src/lib.rs
index a7c8554..aa1f4b9 100644
--- a/contracts/hydro/src/lib.rs
+++ b/contracts/hydro/src/lib.rs
@@ -25,3 +25,6 @@ mod testing_validators_icqs;
#[cfg(test)]
mod testing_fractional_voting;
+
+#[cfg(test)]
+mod testing_deployments;
diff --git a/contracts/hydro/src/msg.rs b/contracts/hydro/src/msg.rs
index 26d3906..b3c7b76 100644
--- a/contracts/hydro/src/msg.rs
+++ b/contracts/hydro/src/msg.rs
@@ -46,10 +46,6 @@ pub enum ExecuteMsg {
tranche_id: u64,
title: String,
description: String,
- // the minimum amount of liquidity, in ATOM equivalent, that the project wants
- // to receive. If they would receive less than this amount of liquidity,
- // it is assumed that no liquidity will be deployed to them.
- // If this is set to 0, the project is assumed to not have a minimum requirement.
minimum_atom_liquidity_request: Uint128,
},
Vote {
@@ -91,10 +87,22 @@ pub enum ExecuteMsg {
WithdrawICQFunds {
amount: Uint128,
},
- SetRoundLiquidityDeployments {
+
+ AddLiquidityDeployment {
round_id: u64,
tranche_id: u64,
- liquidity_deployment: Vec,
+ proposal_id: u64,
+ destinations: Vec,
+ deployed_funds: Vec,
+ funds_before_deployment: Vec,
+ total_rounds: u64,
+ remaining_rounds: u64,
+ },
+
+ RemoveLiquidityDeployment {
+ round_id: u64,
+ tranche_id: u64,
+ proposal_id: u64,
},
}
@@ -118,11 +126,13 @@ pub struct LiquidityDeployment {
pub destinations: Vec,
// allocation assigned to the proposal to be deployed during the specified round
pub deployed_funds: Vec,
- // allocation at the end of the last round for the proposal. this is 0 if no equivalent proposal exicted in the last round.
+ // allocation at the end of the last round for the proposal. this is 0 if no equivalent proposal existed in the last round.
// if this is a "repeating" proposal (where proposals in subsequent rounds are for the same underlying liqudiity deployment),
// it's the allocation prior to any potential clawback or increase
pub funds_before_deployment: Vec,
- // how many rounds this proposal has been in effect for
- // if this is a "repeating" proposal
+ // how many rounds this proposal has been in effect for if the proposal has a non-zero duration
pub total_rounds: u64,
+ // how many rounds are left for this proposal to be in effect
+ // if this is a "repeating" proposal
+ pub remaining_rounds: u64,
}
diff --git a/contracts/hydro/src/state.rs b/contracts/hydro/src/state.rs
index 20e3785..10c596c 100644
--- a/contracts/hydro/src/state.rs
+++ b/contracts/hydro/src/state.rs
@@ -2,6 +2,8 @@ use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Coin, Decimal, Timestamp, Uint128};
use cw_storage_plus::{Item, Map};
+use crate::msg::LiquidityDeployment;
+
pub const CONSTANTS: Item = Item::new("constants");
#[cw_serde]
@@ -166,3 +168,9 @@ impl ValidatorInfo {
}
}
}
+
+// This map stores the liquidity deployments that were performed.
+// These can be set by whitelist admins via the SetLiquidityDeployments message.
+// LIQUIDITY_DEPLOYMENTS_MAP: key(round_id, tranche_id, prop_id) -> deployment
+pub const LIQUIDITY_DEPLOYMENTS_MAP: Map<(u64, u64, u64), LiquidityDeployment> =
+ Map::new("liquidity_deployments_map");
diff --git a/contracts/hydro/src/testing_deployments.rs b/contracts/hydro/src/testing_deployments.rs
new file mode 100644
index 0000000..9cc23c3
--- /dev/null
+++ b/contracts/hydro/src/testing_deployments.rs
@@ -0,0 +1,245 @@
+use crate::testing_mocks::{mock_dependencies, no_op_grpc_query_mock};
+use crate::{
+ contract::{execute, instantiate},
+ msg::ExecuteMsg,
+};
+use cosmwasm_std::testing::mock_env;
+use cosmwasm_std::Uint128;
+
+#[cfg(test)]
+mod tests {
+ use cosmwasm_std::coin;
+
+ use crate::{
+ msg::LiquidityDeployment,
+ state::{Proposal, LIQUIDITY_DEPLOYMENTS_MAP, PROPOSAL_MAP},
+ testing::{get_address_as_str, get_default_instantiate_msg, get_message_info},
+ };
+
+ use super::*;
+
+ #[derive(Debug)]
+ struct AddLiquidityDeploymentTestCase {
+ round_id: u64,
+ tranche_id: u64,
+ proposal_id: u64,
+ sender: String,
+ expect_error: bool,
+ }
+
+ #[test]
+ fn test_add_remove_liquidity_deployment() {
+ let (mut deps, env) = (mock_dependencies(no_op_grpc_query_mock()), mock_env());
+ let admin_address = get_address_as_str(&deps.api, "admin");
+ let info = get_message_info(&deps.api, "admin", &[]);
+ let mut instantiate_msg = get_default_instantiate_msg(&deps.api);
+ instantiate_msg.whitelist_admins = vec![admin_address.clone()];
+ let res = instantiate(deps.as_mut(), env.clone(), info.clone(), instantiate_msg);
+ assert!(res.is_ok(), "{:?}", res);
+
+ // Add a proposal to the store
+ let proposal_id = 1;
+
+ let proposal = Proposal {
+ round_id: 0,
+ tranche_id: 1,
+ proposal_id,
+ power: Uint128::zero(),
+ percentage: Uint128::zero(),
+ title: "proposal1".to_string(),
+ description: "description1".to_string(),
+ minimum_atom_liquidity_request: Uint128::zero(),
+ };
+ PROPOSAL_MAP
+ .save(deps.as_mut().storage, (0, 1, proposal_id), &proposal)
+ .unwrap();
+
+ // Define test cases
+ let test_cases = vec![
+ AddLiquidityDeploymentTestCase {
+ round_id: 0,
+ tranche_id: 1,
+ proposal_id,
+ sender: "admin".to_string(),
+ expect_error: false,
+ },
+ AddLiquidityDeploymentTestCase {
+ round_id: 1,
+ tranche_id: 1,
+ proposal_id,
+ sender: "admin".to_string(),
+ expect_error: true, // Round has not started yet
+ },
+ AddLiquidityDeploymentTestCase {
+ round_id: 0,
+ tranche_id: 2,
+ proposal_id,
+ sender: "admin".to_string(),
+ expect_error: true, // Tranche does not exist
+ },
+ AddLiquidityDeploymentTestCase {
+ round_id: 0,
+ tranche_id: 0,
+ proposal_id: 2,
+ sender: "admin".to_string(),
+ expect_error: true, // Proposal does not exist
+ },
+ AddLiquidityDeploymentTestCase {
+ round_id: 0,
+ tranche_id: 1,
+ proposal_id,
+ sender: "non_admin".to_string(),
+ expect_error: true, // Sender is not an admin
+ },
+ ];
+
+ for case in test_cases {
+ // Add or remove the sender from the whitelist admins list
+ let info = get_message_info(&deps.api, &case.sender, &[]);
+ let add_liquidity_msg = ExecuteMsg::AddLiquidityDeployment {
+ round_id: case.round_id,
+ tranche_id: case.tranche_id,
+ proposal_id: case.proposal_id,
+ destinations: vec!["destination1".to_string()],
+ deployed_funds: vec![coin(100, "token")],
+ funds_before_deployment: vec![coin(200, "token")],
+ total_rounds: 10,
+ remaining_rounds: 5,
+ };
+
+ let res = execute(deps.as_mut(), env.clone(), info.clone(), add_liquidity_msg);
+
+ if case.expect_error {
+ assert!(res.is_err(), "Expected error for case: {:#?}", case);
+ } else {
+ assert!(
+ res.is_ok(),
+ "Expected success for case: {:#?}, error: {:?}",
+ case,
+ res.err()
+ );
+ }
+ }
+ }
+
+ #[derive(Debug)]
+ struct RemoveLiquidityDeploymentTestCase {
+ round_id: u64,
+ tranche_id: u64,
+ proposal_id: u64,
+ sender: String,
+ expect_error: bool,
+ }
+
+ #[test]
+ fn test_remove_liquidity_deployment() {
+ let (mut deps, env) = (mock_dependencies(no_op_grpc_query_mock()), mock_env());
+ let admin_address = get_address_as_str(&deps.api, "admin");
+ let info = get_message_info(&deps.api, "admin", &[]);
+ let mut instantiate_msg = get_default_instantiate_msg(&deps.api);
+ instantiate_msg.whitelist_admins = vec![admin_address.clone()];
+ let res = instantiate(deps.as_mut(), env.clone(), info.clone(), instantiate_msg);
+ assert!(res.is_ok(), "{:?}", res);
+
+ // Add a proposal and a liquidity deployment to the store
+ let proposal_id = 1;
+ let round_id = 0;
+ let tranche_id = 1;
+
+ let proposal = Proposal {
+ round_id,
+ tranche_id,
+ proposal_id,
+ power: Uint128::zero(),
+ percentage: Uint128::zero(),
+ title: "proposal1".to_string(),
+ description: "description1".to_string(),
+ minimum_atom_liquidity_request: Uint128::zero(),
+ };
+ PROPOSAL_MAP
+ .save(
+ deps.as_mut().storage,
+ (round_id, tranche_id, proposal_id),
+ &proposal,
+ )
+ .unwrap();
+
+ let liquidity_deployment = LiquidityDeployment {
+ round_id,
+ tranche_id,
+ proposal_id,
+ destinations: vec!["destination1".to_string()],
+ deployed_funds: vec![coin(100, "token")],
+ funds_before_deployment: vec![coin(200, "token")],
+ total_rounds: 10,
+ remaining_rounds: 5,
+ };
+ LIQUIDITY_DEPLOYMENTS_MAP
+ .save(
+ deps.as_mut().storage,
+ (round_id, tranche_id, proposal_id),
+ &liquidity_deployment,
+ )
+ .unwrap();
+
+ // Define test cases
+ let test_cases = vec![
+ RemoveLiquidityDeploymentTestCase {
+ round_id,
+ tranche_id,
+ proposal_id,
+ sender: "admin".to_string(),
+ expect_error: false,
+ },
+ RemoveLiquidityDeploymentTestCase {
+ round_id,
+ tranche_id,
+ proposal_id: 2,
+ sender: "admin".to_string(),
+ expect_error: true, // Deployment does not exist
+ },
+ RemoveLiquidityDeploymentTestCase {
+ round_id,
+ tranche_id: 2,
+ proposal_id,
+ sender: "admin".to_string(),
+ expect_error: true, // Deployment does not exist
+ },
+ RemoveLiquidityDeploymentTestCase {
+ round_id,
+ tranche_id,
+ proposal_id,
+ sender: "non_admin".to_string(),
+ expect_error: true, // Sender is not an admin
+ },
+ ];
+
+ for case in test_cases {
+ // Remove the sender from the whitelist admins list if necessary
+ let info = get_message_info(&deps.api, &case.sender, &[]);
+ let remove_liquidity_msg = ExecuteMsg::RemoveLiquidityDeployment {
+ round_id: case.round_id,
+ tranche_id: case.tranche_id,
+ proposal_id: case.proposal_id,
+ };
+
+ let res = execute(
+ deps.as_mut(),
+ env.clone(),
+ info.clone(),
+ remove_liquidity_msg,
+ );
+
+ if case.expect_error {
+ assert!(res.is_err(), "Expected error for case: {:#?}", case);
+ } else {
+ assert!(
+ res.is_ok(),
+ "Expected success for case: {:#?}, error: {:?}",
+ case,
+ res.err()
+ );
+ }
+ }
+ }
+}
From 001e622904bcd441d6a6143f5d22ab80bd2f24ba Mon Sep 17 00:00:00 2001
From: Philip Offtermatt
Date: Tue, 29 Oct 2024 11:15:30 +0100
Subject: [PATCH 04/15] Add test cases for missing/zero/existant deployments
---
contracts/hydro/src/contract.rs | 64 +++++++-
contracts/hydro/src/query.rs | 30 +++-
contracts/hydro/src/testing.rs | 15 ++
contracts/tribute/src/contract.rs | 96 +++++++++--
contracts/tribute/src/testing.rs | 254 +++++++++++++++++++++++++++---
5 files changed, 422 insertions(+), 37 deletions(-)
diff --git a/contracts/hydro/src/contract.rs b/contracts/hydro/src/contract.rs
index 73ac4f8..905fe08 100644
--- a/contracts/hydro/src/contract.rs
+++ b/contracts/hydro/src/contract.rs
@@ -20,11 +20,11 @@ use crate::lsm_integration::{
use crate::msg::{ExecuteMsg, InstantiateMsg, LiquidityDeployment, ProposalToLockups, TrancheInfo};
use crate::query::{
AllUserLockupsResponse, ConstantsResponse, CurrentRoundResponse, ExpiredUserLockupsResponse,
- ICQManagersResponse, LockEntryWithPower, ProposalResponse, QueryMsg,
- RegisteredValidatorQueriesResponse, RoundEndResponse, RoundProposalsResponse,
- RoundTotalVotingPowerResponse, TopNProposalsResponse, TotalLockedTokensResponse,
- TranchesResponse, UserVotesResponse, UserVotingPowerResponse, ValidatorPowerRatioResponse,
- WhitelistAdminsResponse, WhitelistResponse,
+ ICQManagersResponse, LiquidityDeploymentResponse, LockEntryWithPower, ProposalResponse,
+ QueryMsg, RegisteredValidatorQueriesResponse, RoundEndResponse, RoundProposalsResponse,
+ RoundTotalVotingPowerResponse, RoundTrancheLiquidityDeploymentsResponse, TopNProposalsResponse,
+ TotalLockedTokensResponse, TranchesResponse, UserVotesResponse, UserVotingPowerResponse,
+ ValidatorPowerRatioResponse, WhitelistAdminsResponse, WhitelistResponse,
};
use crate::score_keeper::{
add_validator_shares_to_proposal, get_total_power_for_proposal,
@@ -1510,7 +1510,61 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult to_json_binary(&query_validator_power_ratio(deps, validator, round_id)?),
QueryMsg::ICQManagers {} => to_json_binary(&query_icq_managers(deps)?),
+ QueryMsg::LiquidityDeployment {
+ round_id,
+ tranche_id,
+ proposal_id,
+ } => to_json_binary(&query_liquidity_deployment(
+ deps,
+ round_id,
+ tranche_id,
+ proposal_id,
+ )?),
+ QueryMsg::RoundTrancheLiquidityDeployments {
+ round_id,
+ tranche_id,
+ start_from,
+ limit,
+ } => to_json_binary(&query_round_tranche_liquidity_deployments(
+ deps, round_id, tranche_id, start_from, limit,
+ )?),
+ }
+}
+
+fn query_liquidity_deployment(
+ deps: Deps,
+ round_id: u64,
+ tranche_id: u64,
+ proposal_id: u64,
+) -> StdResult {
+ let deployment =
+ LIQUIDITY_DEPLOYMENTS_MAP.load(deps.storage, (round_id, tranche_id, proposal_id))?;
+ Ok(LiquidityDeploymentResponse {
+ liquidity_deployment: deployment,
+ })
+}
+
+pub fn query_round_tranche_liquidity_deployments(
+ deps: Deps,
+ round_id: u64,
+ tranche_id: u64,
+ start_from: u64,
+ limit: u64,
+) -> StdResult {
+ let mut deployments = vec![];
+ for deployment in LIQUIDITY_DEPLOYMENTS_MAP
+ .prefix((round_id, tranche_id))
+ .range(deps.storage, None, None, Order::Ascending)
+ .skip(start_from as usize)
+ .take(limit as usize)
+ {
+ let (_, deployment) = deployment?;
+ deployments.push(deployment);
}
+
+ Ok(RoundTrancheLiquidityDeploymentsResponse {
+ liquidity_deployments: deployments,
+ })
}
pub fn query_round_total_power(
diff --git a/contracts/hydro/src/query.rs b/contracts/hydro/src/query.rs
index a244010..774d7df 100644
--- a/contracts/hydro/src/query.rs
+++ b/contracts/hydro/src/query.rs
@@ -1,4 +1,7 @@
-use crate::state::{Constants, LockEntry, Proposal, Tranche, VoteWithPower};
+use crate::{
+ msg::LiquidityDeployment,
+ state::{Constants, LockEntry, Proposal, Tranche, VoteWithPower},
+};
use cosmwasm_schema::{cw_serde, QueryResponses};
use cosmwasm_std::{Addr, Decimal, Timestamp, Uint128};
use schemars::JsonSchema;
@@ -87,6 +90,21 @@ pub enum QueryMsg {
#[returns(ValidatorPowerRatioResponse)]
ValidatorPowerRatio { validator: String, round_id: u64 },
+
+ #[returns(LiquidityDeploymentResponse)]
+ LiquidityDeployment {
+ round_id: u64,
+ tranche_id: u64,
+ proposal_id: u64,
+ },
+
+ #[returns(RoundTrancheLiquidityDeploymentsResponse)]
+ RoundTrancheLiquidityDeployments {
+ round_id: u64,
+ tranche_id: u64,
+ start_from: u64,
+ limit: u64,
+ },
}
#[cw_serde]
@@ -189,3 +207,13 @@ pub struct ValidatorPowerRatioResponse {
pub struct ICQManagersResponse {
pub managers: Vec,
}
+
+#[cw_serde]
+pub struct LiquidityDeploymentResponse {
+ pub liquidity_deployment: LiquidityDeployment,
+}
+
+#[cw_serde]
+pub struct RoundTrancheLiquidityDeploymentsResponse {
+ pub liquidity_deployments: Vec,
+}
diff --git a/contracts/hydro/src/testing.rs b/contracts/hydro/src/testing.rs
index 47d1b70..8665f83 100644
--- a/contracts/hydro/src/testing.rs
+++ b/contracts/hydro/src/testing.rs
@@ -1805,6 +1805,21 @@ fn contract_pausing_test() {
ExecuteMsg::WithdrawICQFunds {
amount: Uint128::new(50),
},
+ ExecuteMsg::AddLiquidityDeployment {
+ round_id: 0,
+ tranche_id: 0,
+ proposal_id: 0,
+ destinations: vec![],
+ deployed_funds: vec![],
+ funds_before_deployment: vec![],
+ total_rounds: 0,
+ remaining_rounds: 0,
+ },
+ ExecuteMsg::RemoveLiquidityDeployment {
+ round_id: 0,
+ tranche_id: 0,
+ proposal_id: 0,
+ },
];
for msg in msgs {
diff --git a/contracts/tribute/src/contract.rs b/contracts/tribute/src/contract.rs
index 4f3431f..8315edb 100644
--- a/contracts/tribute/src/contract.rs
+++ b/contracts/tribute/src/contract.rs
@@ -5,6 +5,7 @@ use cosmwasm_std::{
MessageInfo, Order, Reply, Response, StdError, StdResult, Uint128,
};
use cw2::set_contract_version;
+use hydro::msg::LiquidityDeployment;
use crate::error::ContractError;
use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg};
@@ -334,6 +335,8 @@ fn refund_tribute(
struct TopNProposalInfo {
pub top_n_proposal: Option,
pub is_above_voting_threshold: bool,
+ pub had_deployment_entered: bool,
+ pub received_nonzero_funds: bool,
}
impl TopNProposalInfo {
@@ -348,13 +351,37 @@ impl TopNProposalInfo {
"Tribute not claimable: Proposal received less voting percentage than threshold: {} required, but is {}", config.min_prop_percent_for_claimable_tributes, proposal.percentage))));
}
+ if !self.had_deployment_entered {
+ return Err(ContractError::Std(StdError::generic_err(
+ "Tribute not claimable: Proposal did not have a liquidity deployment entered",
+ )));
+ }
+
+ if !self.received_nonzero_funds {
+ return Err(ContractError::Std(StdError::generic_err(
+ "Tribute not claimable: Proposal did not receive a non-zero liquidity deployment",
+ )));
+ }
+
Ok(proposal.clone())
}
}
}
fn are_tributes_refundable(&self) -> Result<(), ContractError> {
- if self.top_n_proposal.is_some() && self.is_above_voting_threshold {
+ if !self.had_deployment_entered {
+ return Err(ContractError::Std(StdError::generic_err(
+ "Can't refund tribute for proposal that didn't have a liquidity deployment entered",
+ )));
+ }
+
+ if self.received_nonzero_funds {
+ return Err(ContractError::Std(StdError::generic_err(
+ "Can't refund tribute for proposal that received a non-zero liquidity deployment",
+ )));
+ }
+
+ if self.top_n_proposal.is_some() && (self.is_above_voting_threshold) {
return Err(ContractError::Std(StdError::generic_err(
"Can't refund top N proposal that received at least the threshold of the total voting power",
)));
@@ -374,17 +401,39 @@ fn get_top_n_proposal_info(
tranche_id: u64,
proposal_id: u64,
) -> Result {
+ let mut info = TopNProposalInfo {
+ top_n_proposal: None,
+ is_above_voting_threshold: false,
+ had_deployment_entered: false,
+ received_nonzero_funds: false,
+ };
+
+ // get the liquidity deployments for this proposal
+ let liquitidy_deployment_res =
+ get_liquidity_deployment(deps, config, round_id, tranche_id, proposal_id);
+
+ match liquitidy_deployment_res {
+ Ok(liquidity_deployment) => {
+ info.had_deployment_entered = true;
+ info.received_nonzero_funds = !liquidity_deployment.deployed_funds.is_empty()
+ && liquidity_deployment
+ .deployed_funds
+ .iter()
+ .any(|coin| coin.amount > Uint128::zero());
+ }
+ Err(_) => {}
+ }
+
match get_top_n_proposal(deps, config, round_id, tranche_id, proposal_id)? {
- Some(proposal) => Ok(TopNProposalInfo {
- top_n_proposal: Some(proposal.clone()),
- is_above_voting_threshold: proposal.percentage
- >= config.min_prop_percent_for_claimable_tributes,
- }),
- None => Ok(TopNProposalInfo {
- top_n_proposal: None,
- is_above_voting_threshold: false,
- }),
+ Some(proposal) => {
+ info.top_n_proposal = Some(proposal.clone());
+ info.is_above_voting_threshold =
+ proposal.percentage >= config.min_prop_percent_for_claimable_tributes;
+ }
+ None => {}
}
+
+ Ok(info)
}
#[cfg_attr(not(feature = "library"), entry_point)]
@@ -709,6 +758,33 @@ fn get_top_n_proposals(
Ok(proposals_resp.proposals)
}
+fn get_liquidity_deployment(
+ deps: &Deps,
+ config: &Config,
+ round_id: u64,
+ tranche_id: u64,
+ proposal_id: u64,
+) -> Result {
+ let liquidity_deployment_resp: LiquidityDeployment = deps
+ .querier
+ .query_wasm_smart(
+ &config.hydro_contract,
+ &HydroQueryMsg::LiquidityDeployment {
+ round_id,
+ tranche_id,
+ proposal_id,
+ },
+ )
+ .map_err(|err| {
+ StdError::generic_err(format!(
+ "No liquidity deployment was entered yet for proposal. Error: {:?}",
+ err
+ ))
+ })?;
+
+ Ok(liquidity_deployment_resp)
+}
+
// TODO: figure out build issue that we have if we don't define all this functions in both contracts
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn reply(_deps: DepsMut, _env: Env, _msg: Reply) -> Result {
diff --git a/contracts/tribute/src/testing.rs b/contracts/tribute/src/testing.rs
index 967d8a9..14b9a31 100644
--- a/contracts/tribute/src/testing.rs
+++ b/contracts/tribute/src/testing.rs
@@ -9,7 +9,7 @@ use crate::{
state::{Config, ConfigV1, Tribute, CONFIG, ID_TO_TRIBUTE_MAP, TRIBUTE_CLAIMS, TRIBUTE_MAP},
};
use cosmwasm_std::{
- from_json,
+ coins, from_json,
testing::{mock_dependencies, mock_env, MockApi},
to_json_binary, Addr, Binary, ContractResult, Decimal, MessageInfo, QuerierResult, Response,
StdError, StdResult, SystemError, SystemResult, Timestamp, Uint128, WasmQuery,
@@ -18,6 +18,7 @@ use cosmwasm_std::{BankMsg, Coin, CosmosMsg};
use cw2::set_contract_version;
use cw_storage_plus::Item;
use hydro::{
+ msg::LiquidityDeployment,
query::{
CurrentRoundResponse, ProposalResponse, QueryMsg as HydroQueryMsg, TopNProposalsResponse,
UserVotesResponse,
@@ -47,6 +48,32 @@ pub fn get_address_as_str(mock_api: &MockApi, addr: &str) -> String {
mock_api.addr_make(addr).to_string()
}
+fn get_nonzero_deployment_for_proposal(proposal: Proposal) -> LiquidityDeployment {
+ LiquidityDeployment {
+ round_id: proposal.round_id,
+ tranche_id: proposal.tranche_id,
+ proposal_id: proposal.proposal_id,
+ destinations: vec![],
+ deployed_funds: coins(100, "utest"),
+ funds_before_deployment: vec![],
+ total_rounds: 0,
+ remaining_rounds: 0,
+ }
+}
+
+fn get_zero_deployment_for_proposal(proposal: Proposal) -> LiquidityDeployment {
+ LiquidityDeployment {
+ round_id: proposal.round_id,
+ tranche_id: proposal.tranche_id,
+ proposal_id: proposal.proposal_id,
+ destinations: vec![],
+ deployed_funds: vec![],
+ funds_before_deployment: vec![],
+ total_rounds: 0,
+ remaining_rounds: 0,
+ }
+}
+
const DEFAULT_DENOM: &str = "uatom";
const HYDRO_CONTRACT_ADDRESS: &str = "addr0000";
const USER_ADDRESS_1: &str = "addr0001";
@@ -59,6 +86,7 @@ pub struct MockWasmQuerier {
proposals: Vec,
user_votes: Vec,
top_n_proposals: Vec,
+ liquidity_deployments: Vec,
}
impl MockWasmQuerier {
@@ -68,6 +96,7 @@ impl MockWasmQuerier {
proposals: Vec,
user_votes: Vec,
top_n_proposals: Vec,
+ liquidity_deployments: Vec,
) -> Self {
Self {
hydro_contract,
@@ -75,6 +104,7 @@ impl MockWasmQuerier {
proposals,
user_votes,
top_n_proposals,
+ liquidity_deployments,
}
}
@@ -135,6 +165,28 @@ impl MockWasmQuerier {
} => to_json_binary(&TopNProposalsResponse {
proposals: self.top_n_proposals.clone(),
}),
+ HydroQueryMsg::LiquidityDeployment {
+ round_id,
+ tranche_id,
+ proposal_id,
+ } => Ok({
+ let res = self.find_matching_liquidity_deployment(
+ round_id,
+ tranche_id,
+ proposal_id,
+ );
+
+ match res {
+ Ok(res) => res,
+ Err(_) => {
+ return SystemResult::Err(SystemError::InvalidRequest {
+ error: format!("liquidity deployment couldn't be found: round_id={}, tranche_id={}, proposal_id={}", round_id, tranche_id, proposal_id),
+ request: Binary::new(vec![]),
+ })
+ }
+ }
+ }),
+
_ => panic!("unsupported query"),
};
@@ -188,6 +240,26 @@ impl MockWasmQuerier {
}
StdResult::Err(StdError::generic_err("proposal couldn't be found"))
}
+
+ fn find_matching_liquidity_deployment(
+ &self,
+ round_id: u64,
+ tranche_id: u64,
+ proposal_id: u64,
+ ) -> StdResult {
+ for deployment in &self.liquidity_deployments {
+ if deployment.round_id == round_id
+ && deployment.tranche_id == tranche_id
+ && deployment.proposal_id == proposal_id
+ {
+ return to_json_binary(deployment);
+ }
+ }
+
+ Err(StdError::generic_err(
+ "liquidity deployment couldn't be found",
+ ))
+ }
}
struct AddTributeTestCase {
@@ -207,8 +279,15 @@ struct ClaimTributeTestCase {
}
// to make clippy happy :)
-// (add_tribute_round_id, claim_tribute_round_id, proposals, user_votes, top_n_proposals)
-type ClaimTributeMockData = (u64, u64, Vec, Vec, Vec);
+// (add_tribute_round_id, claim_tribute_round_id, proposals, user_votes, top_n_proposals, liquidity_deployments)
+type ClaimTributeMockData = (
+ u64,
+ u64,
+ Vec,
+ Vec,
+ Vec,
+ Vec,
+);
struct AddTributeInfo {
round_id: u64,
@@ -233,8 +312,14 @@ struct RefundTributeTestCase {
// (round_id, tranche_id, proposal_id, tribute_id)
tribute_info: (u64, u64, u64, u64),
tribute_to_add: Vec,
- // (add_tribute_round_id, refund_tribute_round_id, proposals, top_n_proposals)
- mock_data: (u64, u64, Vec, Vec),
+ // (add_tribute_round_id, refund_tribute_round_id, proposals, top_n_proposals, liquidity_deployments)
+ mock_data: (
+ u64,
+ u64,
+ Vec,
+ Vec,
+ Vec,
+ ),
tribute_refunder: Option,
expected_tribute_refund: u128,
expected_success: bool,
@@ -312,6 +397,7 @@ fn add_tribute_test() {
test.mock_data.1,
vec![],
vec![],
+ vec![],
);
deps.querier.update_wasm(move |q| mock_querier.handler(q));
@@ -410,6 +496,18 @@ fn claim_tribute_test() {
let mock_top_n_proposals = vec![mock_proposal1.clone(), mock_proposal2.clone()];
+ // a default liquidity deployments vector, to have a valid deployment
+ // for each mock proposal
+ let deployments_for_all_proposals = mock_proposals
+ .iter()
+ .map(|p| get_nonzero_deployment_for_proposal(p.clone()))
+ .collect::>();
+
+ let zero_deployments_for_all_proposals = mock_proposals
+ .iter()
+ .map(|p| get_zero_deployment_for_proposal(p.clone()))
+ .collect::>();
+
let mock_top_n_voting_threshold_not_reached = vec![
Proposal {
percentage: MIN_PROP_PERCENT_FOR_CLAIMABLE_TRIBUTES - Uint128::one(),
@@ -476,6 +574,7 @@ fn claim_tribute_test() {
},
)],
mock_top_n_proposals.clone(),
+ deployments_for_all_proposals.clone(),
),
},
ClaimTributeTestCase {
@@ -497,7 +596,7 @@ fn claim_tribute_test() {
expected_error_msg: "Round has not ended yet".to_string(),
}
],
- mock_data: (10, 10, mock_proposals.clone(), vec![], vec![]),
+ mock_data: (10, 10, mock_proposals.clone(), vec![], vec![], deployments_for_all_proposals.clone(),),
},
ClaimTributeTestCase {
description: "try claim tribute if user didn't vote at all".to_string(),
@@ -518,7 +617,7 @@ fn claim_tribute_test() {
expected_error_msg: "vote couldn't be found".to_string(),
}
],
- mock_data: (10, 11, mock_proposals.clone(), vec![], vec![]),
+ mock_data: (10, 11, mock_proposals.clone(), vec![], vec![], deployments_for_all_proposals.clone(),),
},
ClaimTributeTestCase {
description: "try claim tribute if user didn't vote for top N proposal".to_string(),
@@ -553,6 +652,7 @@ fn claim_tribute_test() {
},
)],
mock_top_n_proposals.clone(),
+ deployments_for_all_proposals.clone(),
),
},
ClaimTributeTestCase {
@@ -589,6 +689,7 @@ fn claim_tribute_test() {
},
)],
mock_top_n_voting_threshold_not_reached.clone(),
+ deployments_for_all_proposals.clone(),
),
},
ClaimTributeTestCase {
@@ -624,6 +725,7 @@ fn claim_tribute_test() {
},
)],
mock_top_n_proposals.clone(),
+ deployments_for_all_proposals.clone(),
),
},
ClaimTributeTestCase {
@@ -659,8 +761,72 @@ fn claim_tribute_test() {
},
)],
mock_top_n_proposals.clone(),
+ deployments_for_all_proposals.clone(),
),
},
+ ClaimTributeTestCase {
+ description: "try claim tribute for proposal with no deployment entered".to_string(),
+ tributes_to_add: vec![
+ AddTributeInfo {
+ round_id: 10,
+ tranche_id: 0,
+ proposal_id: 5,
+ token: Coin::new(1000u64, DEFAULT_DENOM),
+ }],
+ tributes_to_claim: vec![
+ ClaimTributeInfo {
+ round_id: 10,
+ tranche_id: 0,
+ tribute_id: 0,
+ expected_success: false,
+ expected_tribute_claim: 0,
+ expected_error_msg: "Proposal did not have a liquidity deployment entered".to_string(),
+ }
+ ],
+ mock_data: (10, 11, mock_proposals.clone(), vec![(
+ 10,
+ 0,
+ get_address_as_str(&deps.api, USER_ADDRESS_2),
+ VoteWithPower {
+ prop_id: 5,
+ power: Decimal::from_ratio(Uint128::new(70), Uint128::one()),
+ },
+ )],
+ mock_top_n_proposals.clone(),
+ vec![],),
+ },
+ ClaimTributeTestCase {
+ description: "try claim tribute for proposal with zero deployment".to_string(),
+ tributes_to_add: vec![
+ AddTributeInfo {
+ round_id: 10,
+ tranche_id: 0,
+ proposal_id: 5,
+ token: Coin::new(1000u64, DEFAULT_DENOM),
+ }],
+ tributes_to_claim: vec![
+ ClaimTributeInfo {
+ round_id: 10,
+ tranche_id: 0,
+ tribute_id: 0,
+ expected_success: false,
+ expected_tribute_claim: 0,
+ expected_error_msg: "Proposal did not receive a non-zero liquidity deployment".to_string(),
+ }
+ ],
+ mock_data: (10, 11, mock_proposals.clone(), vec![(
+ 10,
+ 0,
+ get_address_as_str(&deps.api, USER_ADDRESS_2),
+ VoteWithPower {
+ prop_id: 5,
+ power: Decimal::from_ratio(Uint128::new(70), Uint128::one()),
+ },
+ )],
+ mock_top_n_proposals.clone(),
+ zero_deployments_for_all_proposals,),
+ },
+
];
for test in test_cases {
@@ -676,6 +842,7 @@ fn claim_tribute_test() {
test.mock_data.2.clone(),
vec![],
vec![],
+ vec![],
);
deps.querier.update_wasm(move |q| mock_querier.handler(q));
@@ -706,6 +873,7 @@ fn claim_tribute_test() {
test.mock_data.2.clone(),
test.mock_data.3.clone(),
test.mock_data.4.clone(),
+ test.mock_data.5.clone(),
);
deps.querier.update_wasm(move |q| mock_querier.handler(q));
@@ -721,10 +889,13 @@ fn claim_tribute_test() {
let res = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone());
if !tribute_to_claim.expected_success {
- assert!(res
- .unwrap_err()
- .to_string()
- .contains(&tribute_to_claim.expected_error_msg));
+ let error_msg = res.unwrap_err().to_string();
+ assert!(
+ error_msg.contains(&tribute_to_claim.expected_error_msg),
+ "expected: {}, got: {}",
+ tribute_to_claim.expected_error_msg,
+ error_msg
+ );
continue;
}
@@ -764,6 +935,12 @@ fn refund_tribute_test() {
let mock_proposals = vec![mock_proposal.clone()];
+ let liquidity_deployments_refundable =
+ vec![get_zero_deployment_for_proposal(mock_proposal.clone())];
+
+ let liquidity_deployments_non_refundable =
+ vec![get_nonzero_deployment_for_proposal(mock_proposal.clone())];
+
let mock_top_n_different_proposals = vec![Proposal {
round_id: 10,
tranche_id: 0,
@@ -790,7 +967,7 @@ fn refund_tribute_test() {
description: "refund tribute for the non top N proposal".to_string(),
tribute_info: (10, 0, 5, 0),
tribute_to_add: vec![Coin::new(1000u64, DEFAULT_DENOM)],
- mock_data: (10, 11, mock_proposals.clone(), mock_top_n_different_proposals.clone()),
+ mock_data: (10, 11, mock_proposals.clone(), mock_top_n_different_proposals.clone(), liquidity_deployments_refundable.clone()),
tribute_refunder: None,
expected_tribute_refund: 1000,
expected_success: true,
@@ -800,7 +977,8 @@ fn refund_tribute_test() {
description: "refund tribute for the top N proposal with less voting percentage than the required threshold".to_string(),
tribute_info: (10, 0, 5, 0),
tribute_to_add: vec![Coin::new(1000u64, DEFAULT_DENOM)],
- mock_data: (10, 11, mock_proposals.clone(), mock_top_n_voting_threshold_not_reached),
+ mock_data: (10, 11, mock_proposals.clone(), mock_top_n_voting_threshold_not_reached,
+ liquidity_deployments_refundable.clone()),
tribute_refunder: None,
expected_tribute_refund: 1000,
expected_success: true,
@@ -810,7 +988,7 @@ fn refund_tribute_test() {
description: "try to get refund for the current round".to_string(),
tribute_info: (10, 0, 5, 0),
tribute_to_add: vec![Coin::new(1000u64, DEFAULT_DENOM)],
- mock_data: (10, 10, mock_proposals.clone(), mock_top_n_different_proposals.clone()),
+ mock_data: (10, 10, mock_proposals.clone(), mock_top_n_different_proposals.clone(), liquidity_deployments_refundable.clone()),
tribute_refunder: None,
expected_tribute_refund: 0,
expected_success: false,
@@ -820,7 +998,7 @@ fn refund_tribute_test() {
description: "try to get refund for the top N proposal with at least minimum voting percentage".to_string(),
tribute_info: (10, 0, 5, 0),
tribute_to_add: vec![Coin::new(1000u64, DEFAULT_DENOM)],
- mock_data: (10, 11, mock_proposals.clone(), mock_top_n_voting_threshold_reached.clone()),
+ mock_data: (10, 11, mock_proposals.clone(), mock_top_n_voting_threshold_reached.clone(), liquidity_deployments_refundable.clone()),
tribute_refunder: None,
expected_tribute_refund: 0,
expected_success: false,
@@ -830,17 +1008,39 @@ fn refund_tribute_test() {
description: "try to get refund for non existing tribute".to_string(),
tribute_info: (10, 0, 5, 1),
tribute_to_add: vec![Coin::new(1000u64, DEFAULT_DENOM)],
- mock_data: (10, 11, mock_proposals.clone(), mock_top_n_different_proposals.clone()),
+ mock_data: (10, 11, mock_proposals.clone(), mock_top_n_different_proposals.clone(),
+ liquidity_deployments_refundable.clone()),
tribute_refunder: None,
expected_tribute_refund: 0,
expected_success: false,
expected_error_msg: "not found".to_string(),
},
+ RefundTributeTestCase {
+ description: "try to get refund for tribute with no deployment entered".to_string(),
+ tribute_info: (10, 0, 5, 0),
+ tribute_to_add: vec![Coin::new(1000u64, DEFAULT_DENOM)],
+ mock_data: (10, 11, mock_proposals.clone(), mock_top_n_different_proposals.clone(), vec![]),
+ tribute_refunder: None,
+ expected_tribute_refund: 1000,
+ expected_success: false,
+ expected_error_msg: "Can't refund tribute for proposal that didn't have a liquidity deployment entered".to_string(),
+ },
+ RefundTributeTestCase {
+ description: "try to get refund for tribute with non-zero fund deployment".to_string(),
+ tribute_info: (10, 0, 5, 0),
+ tribute_to_add: vec![Coin::new(1000u64, DEFAULT_DENOM)],
+ mock_data: (10, 11, mock_proposals.clone(), mock_top_n_different_proposals.clone(), liquidity_deployments_non_refundable.clone()),
+ tribute_refunder: None,
+ expected_tribute_refund: 1000,
+ expected_success: false,
+ expected_error_msg: "Can't refund tribute for proposal that received a non-zero liquidity deployment".to_string(),
+ },
RefundTributeTestCase {
description: "try to get refund if not the depositor".to_string(),
tribute_info: (10, 0, 5, 0),
tribute_to_add: vec![Coin::new(1000u64, DEFAULT_DENOM)],
- mock_data: (10, 11, mock_proposals.clone(), mock_top_n_different_proposals.clone()),
+ mock_data: (10, 11, mock_proposals.clone(), mock_top_n_different_proposals.clone(),
+ liquidity_deployments_refundable.clone()),
tribute_refunder: Some(USER_ADDRESS_2.to_string()),
expected_tribute_refund: 0,
expected_success: false,
@@ -861,6 +1061,7 @@ fn refund_tribute_test() {
test.mock_data.2.clone(),
vec![],
vec![],
+ vec![],
);
deps.querier.update_wasm(move |q| mock_querier.handler(q));
@@ -889,6 +1090,7 @@ fn refund_tribute_test() {
test.mock_data.2.clone(),
vec![],
test.mock_data.3.clone(),
+ test.mock_data.4.clone(),
);
deps.querier.update_wasm(move |q| mock_querier.handler(q));
@@ -908,10 +1110,13 @@ fn refund_tribute_test() {
let res = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone());
if !test.expected_success {
- assert!(res
- .unwrap_err()
- .to_string()
- .contains(&test.expected_error_msg));
+ let error_msg = res.unwrap_err().to_string();
+ assert!(
+ error_msg.contains(&test.expected_error_msg),
+ "expected error message: {}, got: {}",
+ test.expected_error_msg,
+ error_msg
+ );
continue;
}
@@ -1380,6 +1585,12 @@ fn test_query_outstanding_tribute_claims() {
},
];
+ // mock liquidity deployments to make the tributes outstanding
+ let liquidity_deployments = mock_proposals
+ .iter()
+ .map(|proposal| get_nonzero_deployment_for_proposal(proposal.clone()))
+ .collect();
+
let user_vote = VoteWithPower {
prop_id: 1,
power: Decimal::from_ratio(Uint128::new(500), Uint128::one()),
@@ -1409,6 +1620,7 @@ fn test_query_outstanding_tribute_claims() {
),
],
mock_proposals.clone(),
+ liquidity_deployments,
);
deps.querier.update_wasm(move |q| mock_querier.handler(q));
From 27ffd1e1cf2d3237ad71c38673146d7440b00828 Mon Sep 17 00:00:00 2001
From: Philip Offtermatt
Date: Tue, 29 Oct 2024 12:36:00 +0100
Subject: [PATCH 05/15] Replace single-match with if let
---
contracts/tribute/src/contract.rs | 28 +++++++++++-----------------
1 file changed, 11 insertions(+), 17 deletions(-)
diff --git a/contracts/tribute/src/contract.rs b/contracts/tribute/src/contract.rs
index 8315edb..6fd3af9 100644
--- a/contracts/tribute/src/contract.rs
+++ b/contracts/tribute/src/contract.rs
@@ -412,25 +412,19 @@ fn get_top_n_proposal_info(
let liquitidy_deployment_res =
get_liquidity_deployment(deps, config, round_id, tranche_id, proposal_id);
- match liquitidy_deployment_res {
- Ok(liquidity_deployment) => {
- info.had_deployment_entered = true;
- info.received_nonzero_funds = !liquidity_deployment.deployed_funds.is_empty()
- && liquidity_deployment
- .deployed_funds
- .iter()
- .any(|coin| coin.amount > Uint128::zero());
- }
- Err(_) => {}
+ if let Ok(liquidity_deployment) = liquitidy_deployment_res {
+ info.had_deployment_entered = true;
+ info.received_nonzero_funds = !liquidity_deployment.deployed_funds.is_empty()
+ && liquidity_deployment
+ .deployed_funds
+ .iter()
+ .any(|coin| coin.amount > Uint128::zero());
}
- match get_top_n_proposal(deps, config, round_id, tranche_id, proposal_id)? {
- Some(proposal) => {
- info.top_n_proposal = Some(proposal.clone());
- info.is_above_voting_threshold =
- proposal.percentage >= config.min_prop_percent_for_claimable_tributes;
- }
- None => {}
+ if let Some(proposal) = get_top_n_proposal(deps, config, round_id, tranche_id, proposal_id)? {
+ info.top_n_proposal = Some(proposal.clone());
+ info.is_above_voting_threshold =
+ proposal.percentage >= config.min_prop_percent_for_claimable_tributes;
}
Ok(info)
From f2236d40b36c535258318710d72e2453cda96a46 Mon Sep 17 00:00:00 2001
From: Philip Offtermatt
Date: Tue, 29 Oct 2024 12:43:00 +0100
Subject: [PATCH 06/15] Add changelog entries
---
.changelog/unreleased/features/162-add-liquidity-deployments.md | 2 ++
.changelog/unreleased/features/162-minimum-export-floor.md | 2 ++
.changelog/unreleased/features/162-tribute-claim-change.md | 2 ++
3 files changed, 6 insertions(+)
create mode 100644 .changelog/unreleased/features/162-add-liquidity-deployments.md
create mode 100644 .changelog/unreleased/features/162-minimum-export-floor.md
create mode 100644 .changelog/unreleased/features/162-tribute-claim-change.md
diff --git a/.changelog/unreleased/features/162-add-liquidity-deployments.md b/.changelog/unreleased/features/162-add-liquidity-deployments.md
new file mode 100644
index 0000000..5a051d1
--- /dev/null
+++ b/.changelog/unreleased/features/162-add-liquidity-deployments.md
@@ -0,0 +1,2 @@
+- Allow whitelist admins to register performed liquidity deployments in the Hydro contract.
+ ([\#164](https://github.com/informalsystems/hydro/pull/164))
diff --git a/.changelog/unreleased/features/162-minimum-export-floor.md b/.changelog/unreleased/features/162-minimum-export-floor.md
new file mode 100644
index 0000000..353730c
--- /dev/null
+++ b/.changelog/unreleased/features/162-minimum-export-floor.md
@@ -0,0 +1,2 @@
+- Add a minimum liquidity request value to proposals.
+ ([\#164](https://github.com/informalsystems/hydro/pull/164))
diff --git a/.changelog/unreleased/features/162-tribute-claim-change.md b/.changelog/unreleased/features/162-tribute-claim-change.md
new file mode 100644
index 0000000..3946fd2
--- /dev/null
+++ b/.changelog/unreleased/features/162-tribute-claim-change.md
@@ -0,0 +1,2 @@
+- Adjusts tributes to only be claimable if their proposal received a non-zero fund deployment.
+ ([\#164](https://github.com/informalsystems/hydro/pull/164))
From c98436ab8b3ff2b18775769bb9fcfec58e3fa4f9 Mon Sep 17 00:00:00 2001
From: Philip Offtermatt
Date: Tue, 29 Oct 2024 12:43:40 +0100
Subject: [PATCH 07/15] Fix formatting
---
contracts/tribute/src/testing.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/contracts/tribute/src/testing.rs b/contracts/tribute/src/testing.rs
index 14b9a31..6de1cb5 100644
--- a/contracts/tribute/src/testing.rs
+++ b/contracts/tribute/src/testing.rs
@@ -792,7 +792,7 @@ fn claim_tribute_test() {
power: Decimal::from_ratio(Uint128::new(70), Uint128::one()),
},
)],
- mock_top_n_proposals.clone(),
+ mock_top_n_proposals.clone(),
vec![],),
},
ClaimTributeTestCase {
From dd40047dc800a597af2e37cc07ec7546481757ef Mon Sep 17 00:00:00 2001
From: Philip Offtermatt
Date: Wed, 30 Oct 2024 16:02:37 +0100
Subject: [PATCH 08/15] Remove top n and percentage threshold from tribute
contract
---
contracts/tribute/src/contract.rs | 138 +++-----
contracts/tribute/src/lib.rs | 1 -
contracts/tribute/src/migrate.rs | 42 ---
contracts/tribute/src/state.rs | 4 +-
contracts/tribute/src/testing.rs | 545 ++++++++++--------------------
docs/oversight_committee.md | 6 +-
packages/interface/src/tribute.rs | 13 +-
7 files changed, 237 insertions(+), 512 deletions(-)
delete mode 100644 contracts/tribute/src/migrate.rs
diff --git a/contracts/tribute/src/contract.rs b/contracts/tribute/src/contract.rs
index 6fd3af9..da5bb23 100644
--- a/contracts/tribute/src/contract.rs
+++ b/contracts/tribute/src/contract.rs
@@ -17,8 +17,7 @@ use crate::state::{
Config, Tribute, CONFIG, ID_TO_TRIBUTE_MAP, TRIBUTE_CLAIMS, TRIBUTE_ID, TRIBUTE_MAP,
};
use hydro::query::{
- CurrentRoundResponse, ProposalResponse, QueryMsg as HydroQueryMsg, TopNProposalsResponse,
- UserVotesResponse,
+ CurrentRoundResponse, ProposalResponse, QueryMsg as HydroQueryMsg, UserVotesResponse,
};
use hydro::state::{Proposal, VoteWithPower};
@@ -39,8 +38,6 @@ pub fn instantiate(
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
let config = Config {
hydro_contract: deps.api.addr_validate(&msg.hydro_contract)?,
- top_n_props_count: msg.top_n_props_count,
- min_prop_percent_for_claimable_tributes: msg.min_prop_percent_for_claimable_tributes,
};
CONFIG.save(deps.storage, &config)?;
@@ -196,11 +193,11 @@ fn claim_tribute(
Some(vote) => vote,
};
- // Check that the voter voted for one of the top N proposals that
- // received at least the threshold of the total voting power.
- let proposal =
- get_top_n_proposal_info(&deps.as_ref(), &config, round_id, tranche_id, vote.prop_id)?
- .are_tributes_claimable(&config)?;
+ // make sure that tributes for this proposal are claimable
+ get_proposal_tributes_info(&deps.as_ref(), &config, round_id, tranche_id, vote.prop_id)?
+ .are_tributes_claimable()?;
+
+ let proposal = get_proposal(&deps.as_ref(), &config, round_id, tranche_id, vote.prop_id)?;
let sent_coin = calculate_voter_claim_amount(tribute.funds, vote.power, proposal.power)?;
@@ -288,7 +285,7 @@ fn refund_tribute(
)));
}
- get_top_n_proposal_info(&deps.as_ref(), &config, round_id, tranche_id, proposal_id)?
+ get_proposal_tributes_info(&deps.as_ref(), &config, round_id, tranche_id, proposal_id)?
.are_tributes_refundable()?;
// Load the tribute
@@ -332,40 +329,26 @@ fn refund_tribute(
// field will be set to true if the proposal received at least the minimum voting threshold.
// If the proposal is not among the top N, the "top_n_proposal" field will be set to None,
// and "is_above_voting_threshold" field will be set to false.
-struct TopNProposalInfo {
- pub top_n_proposal: Option,
- pub is_above_voting_threshold: bool,
+struct ProposalTributesInfo {
pub had_deployment_entered: bool,
pub received_nonzero_funds: bool,
}
-impl TopNProposalInfo {
- fn are_tributes_claimable(&self, config: &Config) -> Result {
- match self.top_n_proposal.as_ref() {
- None => Err(ContractError::Std(StdError::generic_err(
- "User voted for proposal outside of top N proposals",
- ))),
- Some(proposal) => {
- if !self.is_above_voting_threshold {
- return Err(ContractError::Std(StdError::generic_err(format!(
- "Tribute not claimable: Proposal received less voting percentage than threshold: {} required, but is {}", config.min_prop_percent_for_claimable_tributes, proposal.percentage))));
- }
-
- if !self.had_deployment_entered {
- return Err(ContractError::Std(StdError::generic_err(
- "Tribute not claimable: Proposal did not have a liquidity deployment entered",
- )));
- }
-
- if !self.received_nonzero_funds {
- return Err(ContractError::Std(StdError::generic_err(
- "Tribute not claimable: Proposal did not receive a non-zero liquidity deployment",
- )));
- }
+impl ProposalTributesInfo {
+ fn are_tributes_claimable(&self) -> Result<(), ContractError> {
+ if !self.had_deployment_entered {
+ return Err(ContractError::Std(StdError::generic_err(
+ "Tribute not claimable: Proposal did not have a liquidity deployment entered",
+ )));
+ }
- Ok(proposal.clone())
- }
+ if !self.received_nonzero_funds {
+ return Err(ContractError::Std(StdError::generic_err(
+ "Tribute not claimable: Proposal did not receive a non-zero liquidity deployment",
+ )));
}
+
+ Ok(())
}
fn are_tributes_refundable(&self) -> Result<(), ContractError> {
@@ -381,29 +364,20 @@ impl TopNProposalInfo {
)));
}
- if self.top_n_proposal.is_some() && (self.is_above_voting_threshold) {
- return Err(ContractError::Std(StdError::generic_err(
- "Can't refund top N proposal that received at least the threshold of the total voting power",
- )));
- }
-
Ok(())
}
}
-// This function will query the top N proposals from the Hydro contract and determine
-// if the proposal with the given ID is among the top N. If yes, it will also determine
-// if the proposal received at least the threshold percent of the total voting power.
-fn get_top_n_proposal_info(
+// This function will return an info struct that holds information about the proposal.
+// The info struct will contain information about whether tributes on this proposal are refundable, claimable, or neither.
+fn get_proposal_tributes_info(
deps: &Deps,
config: &Config,
round_id: u64,
tranche_id: u64,
proposal_id: u64,
-) -> Result {
- let mut info = TopNProposalInfo {
- top_n_proposal: None,
- is_above_voting_threshold: false,
+) -> Result {
+ let mut info = ProposalTributesInfo {
had_deployment_entered: false,
received_nonzero_funds: false,
};
@@ -421,12 +395,6 @@ fn get_top_n_proposal_info(
.any(|coin| coin.amount > Uint128::zero());
}
- if let Some(proposal) = get_top_n_proposal(deps, config, round_id, tranche_id, proposal_id)? {
- info.top_n_proposal = Some(proposal.clone());
- info.is_above_voting_threshold =
- proposal.percentage >= config.min_prop_percent_for_claimable_tributes;
- }
-
Ok(info)
}
@@ -638,20 +606,17 @@ pub fn query_outstanding_tribute_claims(
let mut claims = vec![];
for user_vote in user_votes {
- let proposal =
- match get_top_n_proposal_info(deps, &config, round_id, tranche_id, user_vote.prop_id)
- // If the query top N failed once, it will most likely always fail, so it is safe to return error here.
- .map_err(|err| {
- StdError::generic_err(format!("Failed to get top N proposal: {}", err))
- })?
- .are_tributes_claimable(&config)
- {
- Err(_) => {
- // If the proposal wasn't in top N, or it didn't reach reach the voting threshold, we move to the next vote.
- continue;
- }
- Ok(proposal) => proposal,
- };
+ if get_proposal_tributes_info(deps, &config, round_id, tranche_id, user_vote.prop_id)
+ // If the query top N failed once, it will most likely always fail, so it is safe to return error here.
+ .map_err(|err| StdError::generic_err(format!("Failed to get proposal info: {}", err)))?
+ .are_tributes_claimable()
+ .is_err()
+ {
+ continue;
+ }
+
+ let proposal = get_proposal(deps, &config, round_id, tranche_id, user_vote.prop_id)
+ .map_err(|err| StdError::generic_err(format!("Failed to get proposal: {}", err)))?;
// get all tributes for this proposal
let tributes = TRIBUTE_MAP
@@ -716,40 +681,23 @@ pub fn query_outstanding_tribute_claims(
Ok(OutstandingTributeClaimsResponse { claims })
}
-fn get_top_n_proposal(
+fn get_proposal(
deps: &Deps,
config: &Config,
round_id: u64,
tranche_id: u64,
proposal_id: u64,
-) -> Result