Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce compounder cap #220

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
390 changes: 285 additions & 105 deletions contracts/hydro/src/contract.rs

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion contracts/hydro/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use cosmwasm_std::{OverflowError, StdError};
use cosmwasm_std::{CheckedFromRatioError, OverflowError, StdError};
use cw_utils::PaymentError;
use neutron_sdk::NeutronError;
use thiserror::Error;
Expand All @@ -11,6 +11,9 @@ pub enum ContractError {
#[error("{0}")]
OverflowError(#[from] OverflowError),

#[error("{0}")]
CheckedFromRatioError(#[from] CheckedFromRatioError),

#[error("Unauthorized")]
Unauthorized,

Expand Down
7 changes: 7 additions & 0 deletions contracts/hydro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod msg;
pub mod query;
pub mod score_keeper;
pub mod state;
pub mod utils;
pub mod validators_icqs;

#[cfg(test)]
Expand All @@ -28,3 +29,9 @@ mod testing_fractional_voting;

#[cfg(test)]
mod testing_deployments;

#[cfg(test)]
mod testing_utils;

#[cfg(test)]
mod testing_compounder_cap;
134 changes: 113 additions & 21 deletions contracts/hydro/src/lsm_integration.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
use cosmwasm_std::{Decimal, Deps, Env, Order, StdError, StdResult, Storage};
use cosmwasm_std::{Decimal, Deps, Order, StdError, StdResult, Storage, Uint128};

use neutron_sdk::bindings::query::NeutronQuery;
use neutron_std::types::ibc::applications::transfer::v1::{DenomTrace, TransferQuerier};

use crate::state::{
ValidatorInfo, SCALED_ROUND_POWER_SHARES_MAP, VALIDATORS_INFO, VALIDATORS_PER_ROUND,
VALIDATORS_STORE_INITIALIZED,
ValidatorInfo, SCALED_ROUND_POWER_SHARES_MAP, TOTAL_VOTING_POWER_PER_ROUND, VALIDATORS_INFO,
VALIDATORS_PER_ROUND, VALIDATORS_STORE_INITIALIZED,
};
use crate::{
contract::compute_current_round_id,
score_keeper::{get_total_power_for_proposal, update_power_ratio_for_proposal},
state::{Constants, Proposal, PROPOSAL_MAP, PROPS_BY_SCORE, TRANCHE_MAP},
};
Expand All @@ -22,16 +21,15 @@ pub const COSMOS_VALIDATOR_ADDR_LENGTH: usize = 52; // e.g. cosmosvaloper15w6ra6

// Returns OK if the denom is a valid IBC denom representing LSM
// tokenized share transferred directly from the Cosmos Hub
// of a validator that is also currently among the top
// max_validators validators, and returns the address of that validator.
// of a validator that is also among the top max_validators validators
// for the given round, and returns the address of that validator.
pub fn validate_denom(
deps: Deps<NeutronQuery>,
env: Env,
round_id: u64,
constants: &Constants,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to support denom resolution for the past rounds. We need to pass in the round_id because sometimes this function gets executed in current round, but we are interested for past rounds (e.g. for compounder cap to determine users voting power in previous round).

denom: String,
) -> StdResult<String> {
let validator = resolve_validator_from_denom(&deps, constants, denom)?;
let round_id = compute_current_round_id(&env, constants)?;
let max_validators = constants.max_validator_shares_participating;

if is_active_round_validator(deps.storage, round_id, &validator) {
Expand Down Expand Up @@ -133,6 +131,35 @@ fn query_ibc_denom_trace(deps: &Deps<NeutronQuery>, denom: String) -> StdResult<
.ok_or(StdError::generic_err("Failed to obtain IBC denom trace"))
}

/// Updates all the required stores each time some validator's power ratio is changed
pub fn update_stores_due_to_power_ratio_change(
storage: &mut dyn Storage,
current_height: u64,
validator: &str,
current_round_id: u64,
old_power_ratio: Decimal,
new_power_ratio: Decimal,
) -> StdResult<()> {
update_scores_due_to_power_ratio_change(
storage,
validator,
current_round_id,
old_power_ratio,
new_power_ratio,
)?;

update_total_power_due_to_power_ratio_change(
storage,
current_height,
validator,
current_round_id,
old_power_ratio,
new_power_ratio,
)?;

Ok(())
}

// Applies the new power ratio for the validator to score keepers.
// It updates:
// * all proposals of that round
Expand Down Expand Up @@ -215,31 +242,96 @@ pub fn update_scores_due_to_power_ratio_change(
Ok(())
}

pub fn get_total_power_for_round(deps: Deps<NeutronQuery>, round_id: u64) -> StdResult<Decimal> {
// get the current validators for that round
let validators = get_round_validators(deps, round_id);

// compute the total power
let mut total = Decimal::zero();
for validator in validators {
let shares = SCALED_ROUND_POWER_SHARES_MAP
.may_load(deps.storage, (round_id, validator.address.clone()))?
.unwrap_or(Decimal::zero());
total += shares * validator.power_ratio;
// Updates the total voting power for the current and future rounds when the given validator power ratio changes.
pub fn update_total_power_due_to_power_ratio_change(
storage: &mut dyn Storage,
current_height: u64,
validator: &str,
current_round_id: u64,
old_power_ratio: Decimal,
new_power_ratio: Decimal,
) -> StdResult<()> {
let mut round_id = current_round_id;

// Try to update the total voting power starting from the current round id and moving to next rounds until
// we reach the round for which there is no entry in the TOTAL_VOTING_POWER_PER_ROUND. This implies the first
// round in which no lock entry gives voting power, which also must be true for all rounds after that round,
// so we break the loop at that point.
loop {
let old_total_voting_power =
match TOTAL_VOTING_POWER_PER_ROUND.may_load(storage, round_id)? {
None => break,
Some(total_voting_power) => Decimal::from_ratio(total_voting_power, Uint128::one()),
};

let validator_shares =
get_validator_shares_for_round(storage, round_id, validator.to_owned())?;
if validator_shares == Decimal::zero() {
// If we encounter a round that doesn't have this validator shares, then no subsequent
// round could also have its shares, so break early to save some gas.
break;
}

let old_validator_shares_power = validator_shares * old_power_ratio;
let new_validator_shares_power = validator_shares * new_power_ratio;

let new_total_voting_power = old_total_voting_power
.checked_add(new_validator_shares_power)?
.checked_sub(old_validator_shares_power)?;

TOTAL_VOTING_POWER_PER_ROUND.save(
storage,
round_id,
&new_total_voting_power.to_uint_floor(),
current_height,
)?;

round_id += 1;
}

Ok(total)
Ok(())
}

pub fn get_total_power_for_round(deps: Deps<NeutronQuery>, round_id: u64) -> StdResult<Decimal> {
Ok(
match TOTAL_VOTING_POWER_PER_ROUND.may_load(deps.storage, round_id)? {
None => Decimal::zero(),
Some(total_voting_power) => Decimal::from_ratio(total_voting_power, Uint128::one()),
},
)
}

pub fn add_validator_shares_to_round_total(
storage: &mut dyn Storage,
current_height: u64,
round_id: u64,
validator: String,
val_power_ratio: Decimal,
num_shares: Decimal,
) -> StdResult<()> {
// Update validator shares for the round
let current_shares = get_validator_shares_for_round(storage, round_id, validator.clone())?;
let new_shares = current_shares + num_shares;
SCALED_ROUND_POWER_SHARES_MAP.save(storage, (round_id, validator), &new_shares)
SCALED_ROUND_POWER_SHARES_MAP.save(storage, (round_id, validator.clone()), &new_shares)?;

// Update total voting power for the round
TOTAL_VOTING_POWER_PER_ROUND.update(
storage,
round_id,
current_height,
|total_power_before| -> Result<Uint128, StdError> {
let total_power_before = match total_power_before {
None => Decimal::zero(),
Some(total_power_before) => Decimal::from_ratio(total_power_before, Uint128::one()),
};

Ok(total_power_before
.checked_add(num_shares.checked_mul(val_power_ratio)?)?
.to_uint_floor())
},
)?;

Ok(())
}

pub fn get_validator_shares_for_round(
Expand Down
7 changes: 4 additions & 3 deletions contracts/hydro/src/migration/migrate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use cw2::{get_contract_version, set_contract_version};
use neutron_sdk::bindings::msg::NeutronMsg;
use neutron_sdk::bindings::query::NeutronQuery;

use super::unreleased::migrate_v3_0_0_to_unreleased;
use super::v3_0_0::MigrateMsgV3_0_0;

pub const CONTRACT_VERSION_V1_1_0: &str = "1.1.0";
Expand All @@ -18,8 +19,8 @@ pub const CONTRACT_VERSION_UNRELEASED: &str = "4.0.0";

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(
deps: DepsMut<NeutronQuery>,
_env: Env,
mut deps: DepsMut<NeutronQuery>,
env: Env,
_msg: MigrateMsgV3_0_0,
) -> Result<Response<NeutronMsg>, ContractError> {
let contract_version = get_contract_version(deps.storage)?;
Expand All @@ -30,7 +31,7 @@ pub fn migrate(
)));
}

// no migration necessary from 2.1.0 to 3.0.0
migrate_v3_0_0_to_unreleased(&mut deps, env)?;

set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;

Expand Down
1 change: 1 addition & 0 deletions contracts/hydro/src/migration/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod migrate;
pub mod unreleased;
pub mod v3_0_0;
20 changes: 20 additions & 0 deletions contracts/hydro/src/migration/unreleased.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use cosmwasm_std::{DepsMut, Env};
use neutron_sdk::bindings::query::NeutronQuery;

use crate::error::ContractError;

pub fn migrate_v3_0_0_to_unreleased(
_deps: &mut DepsMut<NeutronQuery>,
_env: Env,
) -> Result<(), ContractError> {
// TODO:
// 1) Migrate Constants from Item to Map; Make sure that the queries for past rounds keep working.
// 2) TOTAL_VOTING_POWER_PER_ROUND needs to be correctly populated regardless of the point in time
// we do the migration. Needs to be populated for future rounds as well. If we populate it for
// the past rounds as well, we can use that in our queries instead of on-the-fly computation
// e.g. query_round_total_power(), query_top_n_proposals().
// 3) LOCKS_MAP needs to be migrated to SnapshotMap.
// 4) Populate USER_LOCKS for existing lockups.
// 4) Populate ROUND_TO_HEIGHT_RANGE and HEIGHT_TO_ROUND for previous rounds?
Ok(())
}
2 changes: 2 additions & 0 deletions contracts/hydro/src/msg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ pub enum ExecuteMsg {
address: String,
},
UpdateConfig {
activate_at: Timestamp,
max_locked_tokens: Option<u128>,
current_users_extra_cap: Option<u128>,
max_deployment_duration: Option<u64>,
},
Pause {},
Expand Down
70 changes: 67 additions & 3 deletions contracts/hydro/src/state.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Coin, Decimal, Timestamp, Uint128};
use cw_storage_plus::{Item, Map};
use cw_storage_plus::{Item, Map, SnapshotMap, Strategy};

use crate::msg::LiquidityDeployment;

pub const CONSTANTS: Item<Constants> = Item::new("constants");
// CONSTANTS: key(activation_timestamp) -> Constants
pub const CONSTANTS: Map<u64, Constants> = Map::new("constants");

#[cw_serde]
pub struct LockPowerEntry {
Expand Down Expand Up @@ -58,7 +59,15 @@ pub struct Constants {
pub round_length: u64,
pub lock_epoch_length: u64,
pub first_round_start: Timestamp,
// The maximum number of tokens that can be locked by any users (currently known and the future ones)
pub max_locked_tokens: u128,
// The maximum number of tokens (out of the max_locked_tokens) that is reserved for locking only
// for currently known users. This field is intended to be set to some value greater than zero at
// the begining of the round, and such Constants would apply only for predefined period of time.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// the begining of the round, and such Constants would apply only for predefined period of time.
// the begining of the round, and such Constants would apply only for a predefined period of time.

// After this period has expired, a new Constants would be activated that would set this value to
// zero, which would allow any user to lock any amount that possibly wasn't filled, but was reserved
// for this cap.
pub current_users_extra_cap: u128,
pub max_validator_shares_participating: u64,
pub hub_connection_id: String,
pub hub_transfer_channel_id: String,
Expand All @@ -71,14 +80,30 @@ pub struct Constants {
// the total number of tokens locked in the contract
pub const LOCKED_TOKENS: Item<u128> = Item::new("locked_tokens");

// Tracks the total number of tokens locked in extra cap, for the given round
// EXTRA_LOCKED_TOKENS_ROUND_TOTAL: key(round_id) -> uint128
pub const EXTRA_LOCKED_TOKENS_ROUND_TOTAL: Map<u64, u128> =
Map::new("extra_locked_tokens_round_total");

// Tracks the number of tokens locked in extra cap by specific user, for the given round
// EXTRA_LOCKED_TOKENS_CURRENT_USERS: key(round_id, sender_address) -> uint128
pub const EXTRA_LOCKED_TOKENS_CURRENT_USERS: Map<(u64, Addr), u128> =
Map::new("extra_locked_tokens_current_users");

pub const LOCK_ID: Item<u64> = Item::new("lock_id");

// stores the current PROP_ID, in order to ensure that each proposal has a unique ID
// this is incremented every time a new proposal is created
pub const PROP_ID: Item<u64> = Item::new("prop_id");

// LOCKS_MAP: key(sender_address, lock_id) -> LockEntry
pub const LOCKS_MAP: Map<(Addr, u64), LockEntry> = Map::new("locks_map");
pub const LOCKS_MAP: SnapshotMap<(Addr, u64), LockEntry> = SnapshotMap::new(
"locks_map",
"locks_map__checkpoints",
"locks_map__changelog",
Strategy::EveryBlock,
);

#[cw_serde]
pub struct LockEntry {
pub lock_id: u64,
Expand All @@ -87,6 +112,25 @@ pub struct LockEntry {
pub lock_end: Timestamp,
}

// Stores the lockup IDs that belong to a user. Snapshoted so that we can determine which lockups
// user had at a given height and use this info to compute users voting power at that height.
// USER_LOCKS: key(user_address) -> Vec<lock_ids>
pub const USER_LOCKS: SnapshotMap<Addr, Vec<u64>> = SnapshotMap::new(
"user_locks",
"user_locks__checkpoints",
"user_locks__changelog",
Strategy::EveryBlock,
);

// This is the total voting power of all users combined.
// TOTAL_VOTING_POWER_PER_ROUND: key(round_id) -> total_voting_power
pub const TOTAL_VOTING_POWER_PER_ROUND: SnapshotMap<u64, Uint128> = SnapshotMap::new(
"total_voting_power_per_round",
"total_voting_power_per_round__checkpoints",
"total_voting_power_per_round__changelog",
Strategy::EveryBlock,
);

// PROPOSAL_MAP: key(round_id, tranche_id, prop_id) -> Proposal
pub const PROPOSAL_MAP: Map<(u64, u64, u64), Proposal> = Map::new("prop_map");
#[cw_serde]
Expand Down Expand Up @@ -228,3 +272,23 @@ impl ValidatorInfo {
// 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");

// Stores the mapping between the round_id and the range of known block heights for that round.
// The lowest_known_height is the height at which the first transaction was executed, and the
// highest_known_height is the height at which the last transaction was executed against the smart
// contract in the given round.
// Notice that the round could span beyond these boundaries, but we don't have a way to know that.
// Besides, the info we store here is sufficient for our needs.
// ROUND_TO_HEIGHT_RANGE: key(round_id) -> HeightRange
pub const ROUND_TO_HEIGHT_RANGE: Map<u64, HeightRange> = Map::new("round_to_height_range");

// Stores the mapping between the block height and round. It gets populated
// each time a transaction is executed against the smart contract.
// HEIGHT_TO_ROUND: key(block_height) -> round_id
pub const HEIGHT_TO_ROUND: Map<u64, u64> = Map::new("height_to_round");

#[cw_serde]
pub struct HeightRange {
pub lowest_known_height: u64,
pub highest_known_height: u64,
}
Loading
Loading