diff --git a/Cargo.lock b/Cargo.lock index 19f7d4393..fd45ddb5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2314,20 +2314,10 @@ dependencies = [ "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw2 1.1.2", - "cw20 1.1.2", - "cw20-base 1.1.2", - "cw20-stake 2.5.0", - "cw4 1.1.2", - "cw4-group 1.1.2", - "cw721-base 0.18.0", "dao-hooks 2.5.0", "dao-interface 2.5.0", "dao-testing", "dao-voting 2.5.0", - "dao-voting-cw20-staked", - "dao-voting-cw4 2.5.0", - "dao-voting-cw721-staked", - "dao-voting-token-staked", "semver", "thiserror", ] diff --git a/contracts/delegation/dao-vote-delegation/Cargo.toml b/contracts/delegation/dao-vote-delegation/Cargo.toml index edb2b4e98..c082cff4c 100644 --- a/contracts/delegation/dao-vote-delegation/Cargo.toml +++ b/contracts/delegation/dao-vote-delegation/Cargo.toml @@ -19,11 +19,8 @@ library = [] cosmwasm-std = { workspace = true } cosmwasm-schema = { workspace = true } cw2 = { workspace = true } -cw4 = { workspace = true } -cw20 = { workspace = true } -cw20-base = { workspace = true, features = ["library"] } cw-controllers = { workspace = true } -cw-ownable = { workspace = true } +cw-snapshot-vector-map = { workspace = true } cw-storage-plus = { workspace = true } cw-utils = { workspace = true } dao-hooks = { workspace = true } @@ -35,11 +32,4 @@ thiserror = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } anyhow = { workspace = true } -cw20-stake = { workspace = true, features = ["library"] } -cw4-group = { workspace = true, features = ["library"] } -cw721-base = { workspace = true, features = ["library"] } -dao-voting-cw20-staked = { workspace = true, features = ["library"] } -dao-voting-cw4 = { workspace = true, features = ["library"] } -dao-voting-token-staked = { workspace = true, features = ["library"] } -dao-voting-cw721-staked = { workspace = true, features = ["library"] } dao-testing = { workspace = true } diff --git a/contracts/delegation/dao-vote-delegation/src/contract.rs b/contracts/delegation/dao-vote-delegation/src/contract.rs new file mode 100644 index 000000000..6db73ea08 --- /dev/null +++ b/contracts/delegation/dao-vote-delegation/src/contract.rs @@ -0,0 +1,383 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::{ + entry_point, to_json_binary, Binary, Decimal, Deps, DepsMut, Env, MessageInfo, Response, + StdError, StdResult, Uint128, +}; +use cw2::{get_contract_version, set_contract_version}; +use cw_utils::nonpayable; +use dao_interface::voting::InfoResponse; +use semver::Version; + +use crate::helpers::{calculate_delegated_vp, get_voting_power, is_delegate_registered}; +use crate::msg::{ + DelegationsResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, OptionalUpdate, QueryMsg, +}; +use crate::state::{ + Config, Delegate, Delegation, CONFIG, DAO, DELEGATED_VP, DELEGATED_VP_AMOUNTS, DELEGATES, + DELEGATIONS, DELEGATION_IDS, PERCENT_DELEGATED, UNVOTED_DELEGATED_VP, +}; +use crate::ContractError; + +pub(crate) const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub const DEFAULT_LIMIT: u32 = 10; +pub const MAX_LIMIT: u32 = 50; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + let dao = msg + .dao + .map(|d| deps.api.addr_validate(&d)) + .transpose()? + .unwrap_or(info.sender); + + DAO.save(deps.storage, &dao)?; + + CONFIG.save( + deps.storage, + &Config { + vp_cap_percent: msg.vp_cap_percent, + }, + )?; + + Ok(Response::new().add_attribute("dao", dao)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Register {} => execute_register(deps, env, info), + ExecuteMsg::Unregister {} => execute_unregister(deps, env, info), + ExecuteMsg::Delegate { delegate, percent } => { + execute_delegate(deps, env, info, delegate, percent) + } + ExecuteMsg::Undelegate { delegate } => execute_undelegate(deps, env, info, delegate), + ExecuteMsg::UpdateConfig { vp_cap_percent, .. } => { + execute_update_config(deps, info, vp_cap_percent) + } + ExecuteMsg::StakeChangeHook(msg) => execute_stake_changed(deps, env, info, msg), + ExecuteMsg::NftStakeChangeHook(msg) => execute_nft_stake_changed(deps, env, info, msg), + ExecuteMsg::MemberChangedHook(msg) => execute_membership_changed(deps, env, info, msg), + } +} + +fn execute_register(deps: DepsMut, env: Env, info: MessageInfo) -> Result { + nonpayable(&info)?; + + let delegate = info.sender; + + if is_delegate_registered(deps.as_ref(), &delegate, env.block.height)? { + return Err(ContractError::DelegateAlreadyRegistered {}); + } + + // ensure delegate has voting power in the DAO + let vp = get_voting_power(deps.as_ref(), &delegate, None)?; + if vp.is_zero() { + return Err(ContractError::NoVotingPower {}); + } + + DELEGATES.save(deps.storage, delegate, &Delegate {}, env.block.height)?; + + Ok(Response::new()) +} + +fn execute_unregister( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result { + nonpayable(&info)?; + + let delegate = info.sender; + + if !is_delegate_registered(deps.as_ref(), &delegate, env.block.height)? { + return Err(ContractError::DelegateNotRegistered {}); + } + + DELEGATES.remove(deps.storage, delegate, env.block.height)?; + + Ok(Response::new()) +} + +fn execute_delegate( + deps: DepsMut, + env: Env, + info: MessageInfo, + delegate: String, + percent: Decimal, +) -> Result { + nonpayable(&info)?; + + if percent <= Decimal::zero() { + return Err(ContractError::InvalidVotingPowerPercent {}); + } + + let delegator = info.sender; + + // prevent self delegation + let delegate = deps.api.addr_validate(&delegate)?; + if delegate == delegator { + return Err(ContractError::CannotDelegateToSelf {}); + } + + // ensure delegator has voting power in the DAO + let vp = get_voting_power(deps.as_ref(), &delegator, None)?; + if vp.is_zero() { + return Err(ContractError::NoVotingPower {}); + } + + // prevent duplicate delegation + let delegation_exists = DELEGATION_IDS.has(deps.storage, (&delegator, &delegate)); + if delegation_exists { + return Err(ContractError::DelegationAlreadyExists {}); + } + + // ensure delegate is registered + if !is_delegate_registered(deps.as_ref(), &delegate, env.block.height)? { + return Err(ContractError::DelegateNotRegistered {}); + } + + // ensure not delegating more than 100% + let current_percent_delegated = PERCENT_DELEGATED + .may_load(deps.storage, &delegator)? + .unwrap_or_default(); + let new_percent_delegated = current_percent_delegated.checked_add(percent)?; + if new_percent_delegated > Decimal::one() { + return Err(ContractError::CannotDelegateMoreThan100Percent { + current: current_percent_delegated + .checked_mul(Decimal::new(100u128.into()))? + .to_string(), + }); + } + + // add new delegation + let delegation_id = DELEGATIONS.push( + deps.storage, + &delegator, + &Delegation { + delegate: delegate.clone(), + percent, + }, + env.block.height, + // TODO: expiry?? + None, + )?; + + DELEGATION_IDS.save(deps.storage, (&delegator, &delegate), &delegation_id)?; + PERCENT_DELEGATED.save(deps.storage, &delegator, &new_percent_delegated)?; + + // add the delegated VP to the delegate's total delegated VP + let delegated_vp = calculate_delegated_vp(vp, percent); + DELEGATED_VP.update( + deps.storage, + &delegate, + env.block.height, + |vp| -> StdResult { + Ok(vp + .unwrap_or_default() + .checked_add(delegated_vp) + .map_err(StdError::overflow)?) + }, + )?; + DELEGATED_VP_AMOUNTS.save(deps.storage, (&delegator, &delegate), &delegated_vp)?; + + Ok(Response::new()) +} + +fn execute_undelegate( + deps: DepsMut, + env: Env, + info: MessageInfo, + delegate: String, +) -> Result { + nonpayable(&info)?; + + let delegator = info.sender; + let delegate = deps.api.addr_validate(&delegate)?; + + // ensure delegation exists + let existing_id = DELEGATION_IDS + .load(deps.storage, (&delegator, &delegate)) + .map_err(|_| ContractError::DelegationDoesNotExist {})?; + + // if delegation exists above, percent will exist + let current_percent_delegated = PERCENT_DELEGATED.load(deps.storage, &delegator)?; + + // retrieve and remove delegation + let delegation = DELEGATIONS.remove(deps.storage, &delegator, existing_id, env.block.height)?; + DELEGATION_IDS.remove(deps.storage, (&delegator, &delegate)); + + // update delegator's percent delegated + let new_percent_delegated = current_percent_delegated.checked_sub(delegation.percent)?; + PERCENT_DELEGATED.save(deps.storage, &delegator, &new_percent_delegated)?; + + // remove delegated VP from delegate's total delegated VP + let current_delegated_vp = DELEGATED_VP_AMOUNTS.load(deps.storage, (&delegator, &delegate))?; + DELEGATED_VP.update( + deps.storage, + &delegate, + env.block.height, + |vp| -> StdResult { + Ok(vp + // must exist if delegation was added in the past + .ok_or(StdError::not_found("delegate's total delegated VP"))? + .checked_sub(current_delegated_vp) + .map_err(StdError::overflow)?) + }, + )?; + DELEGATED_VP_AMOUNTS.remove(deps.storage, (&delegator, &delegate)); + + Ok(Response::new()) +} + +fn execute_update_config( + deps: DepsMut, + info: MessageInfo, + vp_cap_percent: Option>, +) -> Result { + nonpayable(&info)?; + + // only the DAO can update the config + let dao = DAO.load(deps.storage)?; + if info.sender != dao { + return Err(ContractError::Unauthorized {}); + } + + let mut config = CONFIG.load(deps.storage)?; + + if let Some(vp_cap_percent) = vp_cap_percent { + match vp_cap_percent { + OptionalUpdate::Set(vp_cap_percent) => config.vp_cap_percent = Some(vp_cap_percent), + OptionalUpdate::Clear => config.vp_cap_percent = None, + } + } + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Info {} => Ok(to_json_binary(&query_info(deps)?)?), + QueryMsg::Delegations { + delegator, + height, + offset, + limit, + } => Ok(to_json_binary(&query_delegations( + deps, env, delegator, height, offset, limit, + )?)?), + QueryMsg::UnvotedDelegatedVotingPower { + delegate, + proposal_module, + proposal_id, + height, + } => Ok(to_json_binary(&query_unvoted_delegated_vp( + deps, + delegate, + proposal_module, + proposal_id, + height, + )?)?), + } +} + +fn query_info(deps: Deps) -> StdResult { + let info = get_contract_version(deps.storage)?; + Ok(InfoResponse { info }) +} + +fn query_delegations( + deps: Deps, + env: Env, + delegator: String, + height: Option, + offset: Option, + limit: Option, +) -> StdResult { + let height = height.unwrap_or(env.block.height); + let delegator = deps.api.addr_validate(&delegator)?; + let delegations = DELEGATIONS + .load(deps.storage, &delegator, height, limit, offset)? + .into_iter() + .map(|d| d.item) + .collect(); + Ok(DelegationsResponse { + delegations, + height, + }) +} + +fn query_unvoted_delegated_vp( + deps: Deps, + delegate: String, + proposal_module: String, + proposal_id: u64, + height: u64, +) -> StdResult { + let delegate = deps.api.addr_validate(&delegate)?; + + // if delegate not registered, they have no unvoted delegated VP. + if !is_delegate_registered(deps, &delegate, height)? { + return Ok(Uint128::zero()); + } + + let proposal_module = deps.api.addr_validate(&proposal_module)?; + + // if no unvoted delegated VP exists for the proposal, use the delegate's + // total delegated VP at that height. UNVOTED_DELEGATED_VP gets set when the + // delegate or one of their delegators casts a vote. if empty, none of them + // have voted yet. + let udvp = match UNVOTED_DELEGATED_VP + .may_load(deps.storage, (&delegate, &proposal_module, proposal_id))? + { + Some(vp) => vp, + None => DELEGATED_VP + .may_load_at_height(deps.storage, &delegate, height)? + .unwrap_or_default(), + }; + + Ok(udvp) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + let contract_version = get_contract_version(deps.storage)?; + + if contract_version.contract != CONTRACT_NAME { + return Err(ContractError::MigrationErrorIncorrectContract { + expected: CONTRACT_NAME.to_string(), + actual: contract_version.contract, + }); + } + + let new_version: Version = CONTRACT_VERSION.parse()?; + let current_version: Version = contract_version.version.parse()?; + + // only allow upgrades + if new_version <= current_version { + return Err(ContractError::MigrationErrorInvalidVersion { + new: new_version.to_string(), + current: current_version.to_string(), + }); + } + + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Response::default()) +} diff --git a/contracts/delegation/dao-vote-delegation/src/error.rs b/contracts/delegation/dao-vote-delegation/src/error.rs index cfcf562c6..defd24886 100644 --- a/contracts/delegation/dao-vote-delegation/src/error.rs +++ b/contracts/delegation/dao-vote-delegation/src/error.rs @@ -7,9 +7,6 @@ pub enum ContractError { #[error(transparent)] Std(#[from] StdError), - #[error(transparent)] - Ownable(#[from] cw_ownable::OwnershipError), - #[error(transparent)] Overflow(#[from] OverflowError), @@ -21,6 +18,39 @@ pub enum ContractError { #[error("semver parsing error: {0}")] SemVer(String), + + #[error("unauthorized")] + Unauthorized {}, + + #[error("delegate already registered")] + DelegateAlreadyRegistered {}, + + #[error("delegate not registered")] + DelegateNotRegistered {}, + + #[error("no voting power to delegate")] + NoVotingPower {}, + + #[error("cannot delegate to self")] + CannotDelegateToSelf {}, + + #[error("delegation already exists")] + DelegationAlreadyExists {}, + + #[error("delegation does not exist")] + DelegationDoesNotExist {}, + + #[error("cannot delegate more than 100% (current: {current}%)")] + CannotDelegateMoreThan100Percent { current: String }, + + #[error("invalid voting power percent")] + InvalidVotingPowerPercent {}, + + #[error("migration error: incorrect contract: expected {expected}, actual {actual}")] + MigrationErrorIncorrectContract { expected: String, actual: String }, + + #[error("migration error: invalid version: new {new}, current {current}")] + MigrationErrorInvalidVersion { new: String, current: String }, } impl From for ContractError { diff --git a/contracts/delegation/dao-vote-delegation/src/helpers.rs b/contracts/delegation/dao-vote-delegation/src/helpers.rs new file mode 100644 index 000000000..8da3f46a6 --- /dev/null +++ b/contracts/delegation/dao-vote-delegation/src/helpers.rs @@ -0,0 +1,34 @@ +use cosmwasm_std::{Addr, Decimal, Deps, StdResult, Uint128}; + +use dao_interface::voting; + +use crate::state::{DAO, DELEGATES}; + +pub fn is_delegate_registered(deps: Deps, delegate: &Addr, height: u64) -> StdResult { + DELEGATES + .may_load_at_height(deps.storage, delegate.clone(), height) + .map(|d| d.is_some()) +} + +pub fn get_voting_power(deps: Deps, addr: &Addr, height: Option) -> StdResult { + let dao = DAO.load(deps.storage)?; + + let voting_power: voting::VotingPowerAtHeightResponse = deps.querier.query_wasm_smart( + &dao, + &voting::Query::VotingPowerAtHeight { + address: addr.to_string(), + height, + }, + )?; + + Ok(voting_power.power) +} + +// TODO: precision factor??? +pub fn calculate_delegated_vp(vp: Uint128, percent: Decimal) -> Uint128 { + if percent.is_zero() { + return Uint128::zero(); + } + + vp.mul_floor(percent) +} diff --git a/contracts/delegation/dao-vote-delegation/src/lib.rs b/contracts/delegation/dao-vote-delegation/src/lib.rs index d4a73c5be..c7e09cbe7 100644 --- a/contracts/delegation/dao-vote-delegation/src/lib.rs +++ b/contracts/delegation/dao-vote-delegation/src/lib.rs @@ -2,6 +2,7 @@ pub mod contract; mod error; +mod helpers; pub mod msg; pub mod state; diff --git a/contracts/delegation/dao-vote-delegation/src/msg.rs b/contracts/delegation/dao-vote-delegation/src/msg.rs index 0222ccec4..38caced02 100644 --- a/contracts/delegation/dao-vote-delegation/src/msg.rs +++ b/contracts/delegation/dao-vote-delegation/src/msg.rs @@ -1,13 +1,9 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Decimal}; +use cosmwasm_std::{Decimal, Uint128}; use cw4::MemberChangedHookMsg; -use cw_ownable::cw_ownable_execute; -use cw_utils::Duration; use dao_hooks::{nft_stake::NftStakeChangedHookMsg, stake::StakeChangedHookMsg}; use dao_interface::voting::InfoResponse; -pub use cw_ownable::Ownership; - use crate::state::Delegation; #[cw_serde] @@ -18,14 +14,39 @@ pub struct InstantiateMsg { /// they can be delegated any amount of voting power—this cap is only /// applied when casting votes. pub vp_cap_percent: Option, - /// the duration a delegation is valid for, after which it must be renewed - /// by the delegator. - pub delegation_validity: Option, + // /// the duration a delegation is valid for, after which it must be renewed + // /// by the delegator. + // pub delegation_validity: Option, } -#[cw_ownable_execute] #[cw_serde] pub enum ExecuteMsg { + /// Register as a delegate. + Register {}, + /// Unregister as a delegate. + Unregister {}, + /// Create a delegation. + Delegate { + /// the delegate to delegate to + delegate: String, + /// the percent of voting power to delegate + percent: Decimal, + }, + /// Revoke a delegation. + Undelegate { + /// the delegate to undelegate from + delegate: String, + }, + /// Updates the configuration of the delegation system. + UpdateConfig { + /// the maximum percent of voting power that a single delegate can + /// wield. they can be delegated any amount of voting power—this cap is + /// only applied when casting votes. + vp_cap_percent: Option>, + // /// the duration a delegation is valid for, after which it must be + // /// renewed by the delegator. + // delegation_validity: Option, + }, /// Called when a member is added or removed /// to a cw4-groups or cw721-roles contract. MemberChangedHook(MemberChangedHookMsg), @@ -33,16 +54,12 @@ pub enum ExecuteMsg { NftStakeChangeHook(NftStakeChangedHookMsg), /// Called when tokens are staked or unstaked. StakeChangeHook(StakeChangedHookMsg), - /// updates the configuration of the delegation system - UpdateConfig { - /// the maximum percent of voting power that a single delegate can - /// wield. they can be delegated any amount of voting power—this cap is - /// only applied when casting votes. - vp_cap_percent: Option, - /// the duration a delegation is valid for, after which it must be - /// renewed by the delegator. - delegation_validity: Option, - }, +} + +#[cw_serde] +pub enum OptionalUpdate { + Set(T), + Clear, } #[cw_serde] @@ -51,28 +68,32 @@ pub enum QueryMsg { /// Returns contract version info #[returns(InfoResponse)] Info {}, - /// Returns information about the ownership of this contract. - #[returns(Ownership)] - Ownership {}, - /// Returns the delegations by a delegator. + /// Returns the delegations by a delegator, optionally at a given height. + /// Uses the current block height if not provided. #[returns(DelegationsResponse)] - DelegatorDelegations { + Delegations { delegator: String, - start_after: Option, - limit: Option, + height: Option, + offset: Option, + limit: Option, }, - /// Returns the delegations to a delegate. - #[returns(DelegationsResponse)] - DelegateDelegations { + /// Returns the VP delegated to a delegate that has not yet been used in + /// votes cast by delegators in a specific proposal. + #[returns(Uint128)] + UnvotedDelegatedVotingPower { delegate: String, - start_after: Option, - limit: Option, + proposal_module: String, + proposal_id: u64, + height: u64, }, } #[cw_serde] pub struct DelegationsResponse { + /// The delegations. pub delegations: Vec, + /// The height at which the delegations were loaded. + pub height: u64, } #[cw_serde] diff --git a/contracts/delegation/dao-vote-delegation/src/state.rs b/contracts/delegation/dao-vote-delegation/src/state.rs index 977efaa75..dea8f52bf 100644 --- a/contracts/delegation/dao-vote-delegation/src/state.rs +++ b/contracts/delegation/dao-vote-delegation/src/state.rs @@ -1,8 +1,7 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Decimal, Uint128}; -use cw20::Expiration; +use cw_snapshot_vector_map::SnapshotVectorMap; use cw_storage_plus::{Item, Map, SnapshotMap, Strategy}; -use cw_utils::Duration; /// the configuration of the delegation system. pub const CONFIG: Item = Item::new("config"); @@ -10,9 +9,17 @@ pub const CONFIG: Item = Item::new("config"); /// the DAO this delegation system is connected to. pub const DAO: Item = Item::new("dao"); +/// the delegates. +pub const DELEGATES: SnapshotMap = SnapshotMap::new( + "delegates", + "delegates__checkpoints", + "delegates__changelog", + Strategy::EveryBlock, +); + /// the VP delegated to a delegate that has not yet been used in votes cast by -/// delegators in a specific proposal. -pub const UNVOTED_DELEGATED_VP: Map<(&Addr, u64), Uint128> = Map::new("udvp"); +/// delegators in a specific proposal (module, ID). +pub const UNVOTED_DELEGATED_VP: Map<(&Addr, &Addr, u64), Uint128> = Map::new("udvp"); /// the VP delegated to a delegate by height. pub const DELEGATED_VP: SnapshotMap<&Addr, Uint128> = SnapshotMap::new( @@ -22,26 +29,45 @@ pub const DELEGATED_VP: SnapshotMap<&Addr, Uint128> = SnapshotMap::new( Strategy::EveryBlock, ); +/// the delegations of a delegator. +pub const DELEGATIONS: SnapshotVectorMap = SnapshotVectorMap::new( + "d__items", + "d__next_ids", + "d__active", + "d__active__checkpoints", + "d__active__changelog", +); + +/// map (delegator, delegate) -> ID of the delegation in the vector map. this is +/// useful for quickly checking if a delegation already exists, and for +/// undelegating. +pub const DELEGATION_IDS: Map<(&Addr, &Addr), u64> = Map::new("dids"); + +/// map (delegator, delegate) -> calculated absolute delegated VP. +pub const DELEGATED_VP_AMOUNTS: Map<(&Addr, &Addr), Uint128> = Map::new("dvp_amounts"); + +/// map delegator -> percent delegated to all delegates. +pub const PERCENT_DELEGATED: Map<&Addr, Decimal> = Map::new("pd"); + #[cw_serde] pub struct Config { /// the maximum percent of voting power that a single delegate can wield. /// they can be delegated any amount of voting power—this cap is only /// applied when casting votes. pub vp_cap_percent: Option, - /// the duration a delegation is valid for, after which it must be renewed - /// by the delegator. - pub delegation_validity: Option, + // /// the duration a delegation is valid for, after which it must be renewed + // /// by the delegator. + // pub delegation_validity: Option, } +#[cw_serde] +pub struct Delegate {} + #[cw_serde] pub struct Delegation { - /// the delegator. - pub delegator: Addr, /// the delegate that can vote on behalf of the delegator. pub delegate: Addr, /// the percent of the delegator's voting power that is delegated to the /// delegate. pub percent: Decimal, - /// when the delegation expires. - pub expiration: Expiration, }