From 9ad11b45cf56820a0d88e623a9085126d3d36370 Mon Sep 17 00:00:00 2001 From: Noah Saso Date: Mon, 7 Oct 2024 16:51:28 -0400 Subject: [PATCH] created contract scaffold --- Cargo.lock | 31 ++++++++ Cargo.toml | 2 + contracts/README.md | 8 +- .../delegation/dao-vote-delegation/Cargo.toml | 45 +++++++++++ .../delegation/dao-vote-delegation/README.md | 54 +++++++++++++ .../dao-vote-delegation/examples/schema.rs | 11 +++ .../dao-vote-delegation/src/error.rs | 30 +++++++ .../delegation/dao-vote-delegation/src/lib.rs | 11 +++ .../delegation/dao-vote-delegation/src/msg.rs | 79 +++++++++++++++++++ .../dao-vote-delegation/src/state.rs | 47 +++++++++++ 10 files changed, 314 insertions(+), 4 deletions(-) create mode 100644 contracts/delegation/dao-vote-delegation/Cargo.toml create mode 100644 contracts/delegation/dao-vote-delegation/README.md create mode 100644 contracts/delegation/dao-vote-delegation/examples/schema.rs create mode 100644 contracts/delegation/dao-vote-delegation/src/error.rs create mode 100644 contracts/delegation/dao-vote-delegation/src/lib.rs create mode 100644 contracts/delegation/dao-vote-delegation/src/msg.rs create mode 100644 contracts/delegation/dao-vote-delegation/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index b1e7d67d1..ec5457d96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2292,6 +2292,37 @@ dependencies = [ "stake-cw20", ] +[[package]] +name = "dao-vote-delegation" +version = "2.5.0" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers 1.1.2", + "cw-multi-test", + "cw-ownable", + "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", +] + [[package]] name = "dao-voting" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 1d98d3a94..499f511b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ exclude = ["ci/configs/", "wasmvm/libwasmvm"] members = [ "contracts/dao-dao-core", "contracts/distribution/*", + "contracts/delegation/*", "contracts/external/*", "contracts/proposal/*", "contracts/pre-propose/*", @@ -117,6 +118,7 @@ dao-proposal-sudo = { path = "./contracts/test/dao-proposal-sudo", version = "2. dao-rewards-distributor = { path = "./contracts/distribution/dao-rewards-distributor", version = "2.5.0" } dao-test-custom-factory = { path = "./contracts/test/dao-test-custom-factory", version = "2.5.0" } dao-testing = { path = "./packages/dao-testing", version = "2.5.0" } +dao-vote-delegation = { path = "./contracts/delegation/dao-vote-delegation", version = "2.5.0" } dao-voting = { path = "./packages/dao-voting", version = "2.5.0" } dao-voting-cw20-balance = { path = "./contracts/test/dao-voting-cw20-balance", version = "2.5.0" } dao-voting-cw20-staked = { path = "./contracts/voting/dao-voting-cw20-staked", version = "2.5.0" } diff --git a/contracts/README.md b/contracts/README.md index 88c0f6e83..be8f48509 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -1,12 +1,12 @@ # DAO Contracts - `dao-dao-core` - the core module for DAOs. -- `external` - contracts used by DAOs that are not part of a DAO - module. +- `delegation` - delegation modules. +- `distribution` - token distribution modules. +- `external` - contracts used by DAOs that are not part of a DAO module. - `pre-propose` - pre-propose modules. - `proposal` - proposal modules. +- `staking` - cw20 staking functionality and a staking rewards system. - `voting` - voting modules. -- `staking` - cw20 staking functionality and a staking rewards - system. These contracts are used by [Wasmswap](https://github.com/Wasmswap) as well as DAO DAO. For a description of each module type, see [our wiki](https://github.com/DA0-DA0/dao-contracts/wiki/DAO-DAO-Contracts-Design). diff --git a/contracts/delegation/dao-vote-delegation/Cargo.toml b/contracts/delegation/dao-vote-delegation/Cargo.toml new file mode 100644 index 000000000..edb2b4e98 --- /dev/null +++ b/contracts/delegation/dao-vote-delegation/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "dao-vote-delegation" +authors = ["Noah "] +description = "Manages delegation of voting power for DAOs." +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +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-storage-plus = { workspace = true } +cw-utils = { workspace = true } +dao-hooks = { workspace = true } +dao-interface = { workspace = true } +dao-voting = { workspace = true } +semver = { workspace = true } +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/README.md b/contracts/delegation/dao-vote-delegation/README.md new file mode 100644 index 000000000..f73912c64 --- /dev/null +++ b/contracts/delegation/dao-vote-delegation/README.md @@ -0,0 +1,54 @@ +# DAO Vote Delegation + +[![dao-vote-delegation on +crates.io](https://img.shields.io/crates/v/dao-vote-delegation.svg?logo=rust)](https://crates.io/crates/dao-vote-delegation) +[![docs.rs](https://img.shields.io/docsrs/dao-vote-delegation?logo=docsdotrs)](https://docs.rs/dao-vote-delegation/latest/dao_vote_delegation/) + +The `dao-vote-delegation` contract allows members of a DAO to delegate their +voting power to other members of the DAO who have registered as delegates. It +works in conjunction with voting and proposal modules, as well as the rewards +distributor, to offer a comprehensive delegation system for DAOs that supports +the following features: + +- Fractional delegation of voting power on a per-proposal-module basis. +- Overridable delegate votes that can be overridden on a per-proposal basis by + the delegator +- Delegate reward commission. + +## Instantiation and Setup + +This contract must be instantiated by the DAO. + +### Hooks + +After instantiating the contract, it is VITAL to set up the required hooks for +it to work. To compute delegate voting power correctly, this contract needs to +know about both voting power changes and votes cast on proposals as soon as they +happen. + +This can be achieved using the `add_hook` method on voting/staking contracts +that support voting power changes, such as: + +- `cw4-group` +- `dao-voting-cw721-staked` +- `dao-voting-token-staked` +- `cw20-stake` + +For proposal modules, the corresponding hook is `add_vote_hook`: + +- `dao-proposal-single` +- `dao-proposal-multiple` +- `dao-proposal-condorcet` + +## Design Decisions + +### Fractional Delegation via Percentages + +In order to support fractional delegation, users assign a percentage of voting +power to each delegate. Percentages are used instead of choosing an absolute +amount of voting power (e.g. staked tokens) since voting power can change +independently of delegation. If an absolute amount were used, and a user who had +delegated all of their voting power to a few different delegates then unstaked +half of their tokens, there is no clear way to resolve what their new +delegations are. Using percentages instead allows voting power and delegation to +be decided independently. diff --git a/contracts/delegation/dao-vote-delegation/examples/schema.rs b/contracts/delegation/dao-vote-delegation/examples/schema.rs new file mode 100644 index 000000000..19120c210 --- /dev/null +++ b/contracts/delegation/dao-vote-delegation/examples/schema.rs @@ -0,0 +1,11 @@ +use cosmwasm_schema::write_api; +use dao_vote_delegation::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg, + } +} diff --git a/contracts/delegation/dao-vote-delegation/src/error.rs b/contracts/delegation/dao-vote-delegation/src/error.rs new file mode 100644 index 000000000..cfcf562c6 --- /dev/null +++ b/contracts/delegation/dao-vote-delegation/src/error.rs @@ -0,0 +1,30 @@ +use cosmwasm_std::{DivideByZeroError, OverflowError, StdError}; +use cw_utils::PaymentError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error(transparent)] + Std(#[from] StdError), + + #[error(transparent)] + Ownable(#[from] cw_ownable::OwnershipError), + + #[error(transparent)] + Overflow(#[from] OverflowError), + + #[error(transparent)] + DivideByZero(#[from] DivideByZeroError), + + #[error(transparent)] + Payment(#[from] PaymentError), + + #[error("semver parsing error: {0}")] + SemVer(String), +} + +impl From for ContractError { + fn from(err: semver::Error) -> Self { + Self::SemVer(err.to_string()) + } +} diff --git a/contracts/delegation/dao-vote-delegation/src/lib.rs b/contracts/delegation/dao-vote-delegation/src/lib.rs new file mode 100644 index 000000000..d4a73c5be --- /dev/null +++ b/contracts/delegation/dao-vote-delegation/src/lib.rs @@ -0,0 +1,11 @@ +#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] + +pub mod contract; +mod error; +pub mod msg; +pub mod state; + +#[cfg(test)] +mod testing; + +pub use crate::error::ContractError; diff --git a/contracts/delegation/dao-vote-delegation/src/msg.rs b/contracts/delegation/dao-vote-delegation/src/msg.rs new file mode 100644 index 000000000..0222ccec4 --- /dev/null +++ b/contracts/delegation/dao-vote-delegation/src/msg.rs @@ -0,0 +1,79 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Decimal}; +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] +pub struct InstantiateMsg { + /// The DAO. If not provided, the instantiator is used. + pub dao: Option, + /// 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, +} + +#[cw_ownable_execute] +#[cw_serde] +pub enum ExecuteMsg { + /// Called when a member is added or removed + /// to a cw4-groups or cw721-roles contract. + MemberChangedHook(MemberChangedHookMsg), + /// Called when NFTs are staked or unstaked. + 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] +#[derive(QueryResponses)] +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(DelegationsResponse)] + DelegatorDelegations { + delegator: String, + start_after: Option, + limit: Option, + }, + /// Returns the delegations to a delegate. + #[returns(DelegationsResponse)] + DelegateDelegations { + delegate: String, + start_after: Option, + limit: Option, + }, +} + +#[cw_serde] +pub struct DelegationsResponse { + pub delegations: Vec, +} + +#[cw_serde] +pub struct MigrateMsg {} diff --git a/contracts/delegation/dao-vote-delegation/src/state.rs b/contracts/delegation/dao-vote-delegation/src/state.rs new file mode 100644 index 000000000..977efaa75 --- /dev/null +++ b/contracts/delegation/dao-vote-delegation/src/state.rs @@ -0,0 +1,47 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Decimal, Uint128}; +use cw20::Expiration; +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"); + +/// the DAO this delegation system is connected to. +pub const DAO: Item = Item::new("dao"); + +/// 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"); + +/// the VP delegated to a delegate by height. +pub const DELEGATED_VP: SnapshotMap<&Addr, Uint128> = SnapshotMap::new( + "dvp", + "dvp__checkpoints", + "dvp__changelog", + Strategy::EveryBlock, +); + +#[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, +} + +#[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, +}