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

delegations #888

Draft
wants to merge 24 commits into
base: development
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
11 changes: 11 additions & 0 deletions .github/workflows/release-contracts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ jobs:
steps:
- uses: actions/checkout@v3

- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: false
docker-images: true
swap-storage: true

# tar is required for cargo cache
- run: apk add --no-cache tar

Expand Down
43 changes: 43 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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/*",
Expand Down Expand Up @@ -92,6 +93,7 @@ cw-fund-distributor = { path = "./contracts/distribution/cw-fund-distributor", v
cw-hooks = { path = "./packages/cw-hooks", version = "2.6.0" }
cw-paginate-storage = { path = "./packages/cw-paginate-storage", version = "2.6.0" }
cw-payroll-factory = { path = "./contracts/external/cw-payroll-factory", version = "2.6.0" }
cw-snapshot-vector-map = { path = "./packages/cw-snapshot-vector-map", version = "2.6.0" }
cw-stake-tracker = { path = "./packages/cw-stake-tracker", version = "2.6.0" }
cw-token-swap = { path = "./contracts/external/cw-token-swap", version = "2.6.0" }
cw-tokenfactory-issuer = { path = "./contracts/external/cw-tokenfactory-issuer", version = "2.6.0", default-features = false }
Expand Down Expand Up @@ -122,6 +124,7 @@ dao-proposal-sudo = { path = "./contracts/test/dao-proposal-sudo", version = "2.
dao-rewards-distributor = { path = "./contracts/distribution/dao-rewards-distributor", version = "2.6.0" }
dao-test-custom-factory = { path = "./contracts/test/dao-test-custom-factory", version = "2.6.0" }
dao-testing = { path = "./packages/dao-testing", version = "2.6.0" }
dao-vote-delegation = { path = "./contracts/delegation/dao-vote-delegation", version = "2.6.0" }
dao-voting = { path = "./packages/dao-voting", version = "2.6.0" }
dao-voting-cw20-balance = { path = "./contracts/test/dao-voting-cw20-balance", version = "2.6.0" }
dao-voting-cw20-staked = { path = "./contracts/voting/dao-voting-cw20-staked", version = "2.6.0" }
Expand Down
1 change: 1 addition & 0 deletions ci/bootstrap-env/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ fn main() -> Result<()> {
},
close_proposal_on_execution_failure: false,
veto: None,
delegation_module: None,
})?,
admin: Some(Admin::CoreModule {}),
funds: vec![],
Expand Down
1 change: 1 addition & 0 deletions ci/integration-tests/src/helpers/helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ pub fn create_dao(
},
},
veto: None,
delegation_module: None,
})?,
admin: Some(Admin::CoreModule {}),
funds: vec![],
Expand Down
8 changes: 4 additions & 4 deletions contracts/README.md
Original file line number Diff line number Diff line change
@@ -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).
48 changes: 48 additions & 0 deletions contracts/delegation/dao-vote-delegation/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
[package]
name = "dao-vote-delegation"
authors = ["Noah <[email protected]>"]
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 }
cw-controllers = { workspace = true }
cw-paginate-storage = { workspace = true }
cw-snapshot-vector-map = { workspace = true }
cw-storage-plus = { workspace = true }
cw-utils = { workspace = true }
cw-wormhole = { 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 = { workspace = true }
cw20-base = { workspace = true, features = ["library"] }
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 }
dao-proposal-sudo = { workspace = true }
106 changes: 106 additions & 0 deletions contracts/delegation/dao-vote-delegation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# 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.
- Delegate votes that can be overridden on a per-proposal basis by each
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.

## Implementation Notes

The trickiest piece of this implementation is navigating the snapshot maps,
which are the data structures used to store historical state.

Essentially, snapshot maps (and the other historical data structures based on
snapshot maps) take 1 block to reflect updates made, but only when querying
state at a specific height (typically in the past). When using the query
functions that do not accept a height, they read the updates immediately,
including those from the same block. For example, `snapshot_map.may_load`
returns the latest map values, including those changed in the same block by an
earlier transaction; on the other hand, `snapshot_map.may_load_at_height`
returns the map values as they were at the end of the previous block (due to an
implementation detail of snapshot maps that I'm not sure was intentional).

Ideally, we would just fix this discrepancy and move on. However, many other
modules have been built using SnapshotMaps, and it is important that all modules
behave consistently with respect to this issue. For example, voting power
queries in voting modules operate in this way, with updates delayed 1
block—because of this, it is crucial that we compute and store delegated voting
power in the same way. Otherwise we risk introducing off-by-one inconsistencies
in voting power calculations. Thus, for now, we will accept this behavior and
continue.

What this means for the implementation is that we must be very careful whenever
we do pretty much anything. When performing updates at the latest block, such as
when delegating or undelegating voting power, or when handling a change in
someone's voting power (in order to propagate that change to their delegates),
we need to be sure to interact with the latest delegation and voting power
state. However, when querying information from the past, we need to match the
delayed update behavior of voting power queries.

More concretely:
- when registering/unregistering a delegate, delegating/undelegating, or
handling voting power change hooks, we need to access the account's latest
voting power (by querying `latest_height + 1`), even if it was updated in the
same block. this ensures that changes to voting power right before a
registration/delegation occurs, or voting power changes right after a
delegation occurs, are taken into account. e.g. an account should not be able
to get rid of all their voting power (i.e. stop being a member) and then
become a delegate within the same block.
- when delegating/undelegating or handling voting power change hooks, in order
to update a delegate's total delegated VP, we need to query the latest
delegated VP, even if it was updated earlier in the same block, and then
effectively "re-prepare" the total that will be reflected in historical
queries starting from the next block. `snapshot_map.update` takes care of this
automatically by loading the latest value from the same block.
- when querying information from the past, such as when querying a delegate's
total unvoted delegated VP when they cast a vote, or when a vote cast hook is
triggered for a delegator, we need to use historical queries that match the
behavior of the voting module's voting power queries, i.e. delayed by 1 block.
11 changes: 11 additions & 0 deletions contracts/delegation/dao-vote-delegation/examples/schema.rs
Original file line number Diff line number Diff line change
@@ -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,
}
}
Loading
Loading