Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Allow voting with locks that voted for a proposal which did not receive any funds in its deployment
([\#231](https://github.com/informalsystems/hydro/pull/231))
4 changes: 2 additions & 2 deletions artifacts/checksums.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
5774e9ab9b8c54b8304d27111209b271a610ef31c50e6f539ad44a5a202ab3b0 dao_voting_adapter.wasm
b62a691c948def77d79d7f0eca6c4d2e3b27aa3c8d90db2c474ab26b049f3ac5 hydro.wasm
7c6834be989d327bce530307d76f3a4ee11f04eadb7a396eed393efa0c776d96 tribute.wasm
8249ace08e35c0341257c9e730f63cc475e1f0e9711140f78e2f949edf1c180b hydro.wasm
79d6187269733a5281b25a9ab3e2f25d4484e1c1e6ac3e829ea7035b3ed18fc7 tribute.wasm
Binary file modified artifacts/hydro.wasm
Binary file not shown.
Binary file modified artifacts/tribute.wasm
Binary file not shown.
32 changes: 30 additions & 2 deletions contracts/hydro/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ use crate::state::{
VALIDATOR_TO_QUERY_ID, VOTE_MAP, VOTING_ALLOWED_ROUND, WHITELIST, WHITELIST_ADMINS,
};
use crate::utils::{
get_current_user_voting_power, get_lock_time_weighted_shares,
find_deployment_for_voted_lock, get_current_user_voting_power, get_lock_time_weighted_shares,
load_constants_active_at_timestamp, load_current_constants, run_on_each_transaction,
scale_lockup_power, to_lockup_with_power, update_locked_tokens_info,
validate_locked_tokens_caps,
Expand Down Expand Up @@ -1827,7 +1827,35 @@ fn enrich_lockups_with_tranche_infos(
return None;
}

let next_round_voting_allowed = next_round_voting_allowed_res.unwrap();
let mut next_round_voting_allowed = next_round_voting_allowed_res.unwrap();

// if the next round voting allowed is greater than the current round,
// meaning the lockup has voted on a proposal in some previous round,
// check whether there is a deployment associated with that proposal
if next_round_voting_allowed > current_round_id {
let deployment_res = find_deployment_for_voted_lock(
deps,
current_round_id,
*tranche_id,
&converted_addr,
lock.lock_entry.lock_id,
);

// if there was an error in the store while loading the deployment,
// we filter out the tranche by returning None
if deployment_res.is_err() {
return None;
}

let deployment = deployment_res.unwrap();

// If the deployment for the proposals exists, and has zero funds, we ignore next_round_voting_allowed - the lockup can vote
if deployment.is_some() && !(deployment.unwrap().has_nonzero_funds()) {
next_round_voting_allowed = current_round_id;
}

// otherwise, next_round_voting_allowed stays unmodified
}

// return the info for this tranche
Some(PerTrancheLockupInfo {
Expand Down
111 changes: 111 additions & 0 deletions contracts/hydro/src/testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3652,3 +3652,114 @@ fn test_get_vote_for_update() {
}
}
}

#[test]
fn test_cannot_vote_while_long_deployment_ongoing() {
let user_address = "addr0000";
let user_token = Coin::new(1000u64, IBC_DENOM_1.to_string());

let grpc_query = denom_trace_grpc_query_mock(
"transfer/channel-0".to_string(),
HashMap::from([(IBC_DENOM_1.to_string(), VALIDATOR_1_LST_DENOM_1.to_string())]),
);

let (mut deps, mut env) = (mock_dependencies(grpc_query), mock_env());
let info = get_message_info(&deps.api, user_address, &[user_token.clone()]);

// Initialize with 1 month round length
let mut msg = get_default_instantiate_msg(&deps.api);
msg.round_length = ONE_MONTH_IN_NANO_SECONDS;
msg.whitelist_admins = vec![get_address_as_str(&deps.api, "admin")];

let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg.clone());
assert!(res.is_ok());

// Setup validator for round 0
set_validator_infos_for_round(&mut deps.storage, 0, vec![VALIDATOR_1.to_string()]).unwrap();

// Lock tokens for 3 months to be able to vote on long proposals
let msg = ExecuteMsg::LockTokens {
lock_duration: THREE_MONTHS_IN_NANO_SECONDS,
};
let res = execute(deps.as_mut(), env.clone(), info.clone(), msg);
assert!(res.is_ok());

// Create proposal with 3 month deployment duration
let long_proposal_msg = ExecuteMsg::CreateProposal {
round_id: None,
tranche_id: 1,
title: "long proposal".to_string(),
description: "3 month deployment".to_string(),
deployment_duration: 3,
minimum_atom_liquidity_request: Uint128::zero(),
};
let res = execute(deps.as_mut(), env.clone(), info.clone(), long_proposal_msg);
assert!(res.is_ok());

// Vote on long proposal
let msg = ExecuteMsg::Vote {
tranche_id: 1,
proposals_votes: vec![ProposalToLockups {
proposal_id: 0,
lock_ids: vec![0],
}],
};
let res = execute(deps.as_mut(), env.clone(), info.clone(), msg);
assert!(res.is_ok());

// Advance to next round
env.block.time = env.block.time.plus_nanos(ONE_MONTH_IN_NANO_SECONDS + 1);

// Setup validator for round 1
set_validator_infos_for_round(&mut deps.storage, 1, vec![VALIDATOR_1.to_string()]).unwrap();

// Create new proposal in round 1
let new_proposal_msg = ExecuteMsg::CreateProposal {
round_id: None,
tranche_id: 1,
title: "new proposal".to_string(),
description: "1 month deployment".to_string(),
deployment_duration: 1,
minimum_atom_liquidity_request: Uint128::zero(),
};
let res = execute(deps.as_mut(), env.clone(), info.clone(), new_proposal_msg);
assert!(res.is_ok());

// Try to vote on new proposal - should fail because user already voted on long proposal
let vote_msg = ExecuteMsg::Vote {
tranche_id: 1,
proposals_votes: vec![ProposalToLockups {
proposal_id: 1,
lock_ids: vec![0],
}],
};
let res = execute(deps.as_mut(), env.clone(), info.clone(), vote_msg.clone());

// Verify that voting fails
assert!(res.is_err());
let error = res.unwrap_err().to_string();
assert!(
error.contains("Cannot vote again with this lock_id until round"),
"Error: {}",
error
);

// Add zero liquidity deployment to first proposal
let msg = ExecuteMsg::AddLiquidityDeployment {
round_id: 0,
tranche_id: 1,
proposal_id: 0,
destinations: vec!["destination1".to_string()],
deployed_funds: vec![],
funds_before_deployment: vec![],
total_rounds: 3,
remaining_rounds: 3,
};
let admin_info = get_message_info(&deps.api, "admin", &[]);
let res = execute(deps.as_mut(), env.clone(), admin_info, msg);
assert!(res.is_ok(), "error: {:?}", res);

// now, voting should be possible
let res = execute(deps.as_mut(), env.clone(), info.clone(), vote_msg);
assert!(res.is_ok());
}
71 changes: 70 additions & 1 deletion contracts/hydro/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ use crate::{
get_total_power_for_round, get_validator_power_ratio_for_round, initialize_validator_store,
validate_denom,
},
msg::LiquidityDeployment,
query::LockEntryWithPower,
state::{
Constants, HeightRange, LockEntry, RoundLockPowerSchedule, CONSTANTS,
EXTRA_LOCKED_TOKENS_CURRENT_USERS, EXTRA_LOCKED_TOKENS_ROUND_TOTAL, HEIGHT_TO_ROUND,
LOCKED_TOKENS, LOCKS_MAP, ROUND_TO_HEIGHT_RANGE, SNAPSHOTS_ACTIVATION_HEIGHT, USER_LOCKS,
LIQUIDITY_DEPLOYMENTS_MAP, LOCKED_TOKENS, LOCKS_MAP, PROPOSAL_MAP, ROUND_TO_HEIGHT_RANGE,
SNAPSHOTS_ACTIVATION_HEIGHT, USER_LOCKS, VOTE_MAP,
},
};

Expand Down Expand Up @@ -543,3 +545,70 @@ pub struct LockingInfo {
pub lock_in_public_cap: Option<u128>,
pub lock_in_known_users_cap: Option<u128>,
}

// Finds the deployment for the last proposal the given lock has voted for.
// This will return None if there is no deployment for the proposal.
// It will return an error if the lock has not voted for any proposal,
// or if the store entry for the proposals deployment cannot be parsed.
pub fn find_deployment_for_voted_lock(
deps: &Deps<NeutronQuery>,
current_round_id: u64,
tranche_id: u64,
lock_voter: &Addr,
lock_id: u64,
) -> Result<Option<LiquidityDeployment>, ContractError> {
if current_round_id == 0 {
return Err(ContractError::Std(StdError::generic_err(
"Cannot find deployment for lock in round 0.",
)));
}

let mut check_round = current_round_id - 1;
loop {
if let Some(prev_vote) = VOTE_MAP.may_load(
deps.storage,
((check_round, tranche_id), lock_voter.clone(), lock_id),
)? {
// Found a vote, so get the proposal and its deployment
let prev_proposal =
PROPOSAL_MAP.load(deps.storage, (check_round, tranche_id, prev_vote.prop_id))?;

// load the deployment for the prev_proposal
return LIQUIDITY_DEPLOYMENTS_MAP
.may_load(
deps.storage,
(
prev_proposal.round_id,
prev_proposal.tranche_id,
prev_proposal.proposal_id,
),
)
.map_err(|_| {
// if we cannot read the store, there is an error
ContractError::Std(StdError::generic_err(format!(
"Could not read deployment store for proposal {} in tranche {} and round {}",
prev_proposal.proposal_id, prev_proposal.tranche_id, prev_proposal.round_id
)))
});
}
// If we reached the beginning of the tranche, there is an error
if check_round == 0 {
return Err(ContractError::Std(StdError::generic_err(format!(
"Could not find previous vote for lock_id {} in tranche {}.",
lock_id, tranche_id,
))));
}

check_round -= 1;
}
}

impl LiquidityDeployment {
pub fn has_nonzero_funds(&self) -> bool {
!self.deployed_funds.is_empty()
&& self
.deployed_funds
.iter()
.any(|coin| coin.amount > Uint128::zero())
}
}
21 changes: 16 additions & 5 deletions contracts/hydro/src/vote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::score_keeper::ProposalPowerUpdate;
use crate::state::{
Constants, LockEntry, Vote, LOCKS_MAP, PROPOSAL_MAP, VOTE_MAP, VOTING_ALLOWED_ROUND,
};
use crate::utils::get_lock_time_weighted_shares;
use crate::utils::{find_deployment_for_voted_lock, get_lock_time_weighted_shares};
use cosmwasm_std::{Addr, Decimal, DepsMut, Env, SignedDecimal, StdError, Storage, Uint128};
use neutron_sdk::bindings::query::NeutronQuery;
use std::collections::{HashMap, HashSet};
Expand Down Expand Up @@ -205,10 +205,21 @@ pub fn process_votes(

if let Some(voting_allowed_round) = voting_allowed_round {
if voting_allowed_round > context.round_id {
return Err(ContractError::Std(StdError::generic_err(format!(
"Not allowed to vote with lock_id {} in tranche {}. Cannot vote again with this lock_id until round {}.",
lock_id, context.tranche_id, voting_allowed_round
))));
let deployment = find_deployment_for_voted_lock(
&deps.as_ref(),
context.round_id,
context.tranche_id,
context.sender,
lock_id,
)?;

// If there is no deployment for this proposal yet, or it has non-zero funds, then should error out
if deployment.is_none() || deployment.unwrap().has_nonzero_funds() {
return Err(ContractError::Std(StdError::generic_err(format!(
"Not allowed to vote with lock_id {} in tranche {}. Cannot vote again with this lock_id until round {}.",
lock_id, context.tranche_id, voting_allowed_round
))));
}
}
}

Expand Down
6 changes: 1 addition & 5 deletions contracts/tribute/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -386,11 +386,7 @@ fn get_proposal_tributes_info(

if let Ok(liquidity_deployment) = liquidity_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());
info.received_nonzero_funds = liquidity_deployment.has_nonzero_funds();
}

Ok(info)
Expand Down