Skip to content

Commit

Permalink
Allow voting with locks whose voted-for-proposal did not receive any …
Browse files Browse the repository at this point in the history
…funds (#231)

* Allow locks with zero funds deployments to vote again even during deployment duration

* Fix query and refactor methods

* Add changelog entry

* Regenerate contracts and fix clippy

* Fix check for round 0 in loop

* Recompile contracts

* Recompile contracts]

* Recompile again

* Filter out tranche properly by returning None

* Regenerate contract
  • Loading branch information
p-offtermatt authored Feb 25, 2025
1 parent 6363e8c commit c0e6de5
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 15 deletions.
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

0 comments on commit c0e6de5

Please sign in to comment.