Skip to content

Commit

Permalink
Handle revoting and update readme's
Browse files Browse the repository at this point in the history
  • Loading branch information
ismellike committed Feb 7, 2024
1 parent ed344a4 commit f0c494b
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 47 deletions.
29 changes: 26 additions & 3 deletions contracts/external/dao-proposal-incentives/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,31 @@
[![dao-proposal-incentives on crates.io](https://img.shields.io/crates/v/dao-proposal-incentives.svg?logo=rust)](https://crates.io/crates/dao-proposal-incentives)
[![docs.rs](https://img.shields.io/docsrs/dao-proposal-incentives?logo=docsdotrs)](https://docs.rs/dao-proposal-incentives/latest/cw_admin_factory/)

Allows for DAOs to offer incentives for making successful proposals.
This contract enables DAO's to incentivize members for making successful proposals. By integrating this contract, DAO's can automatically reward members whose proposals are successfully passed, using either native tokens or CW20 tokens.

To setup this contract, the DAO needs to add this contract as a `ProposalHook` to the `dao-voting-single` or `dao-voting-multiple` proposal module, and the DAO must be the `owner` of this contract. When someone successfully passes a proposal the specified rewards are automatically paid out.
## Instantiate

The incentives can be configured as native or cw20 tokens, and the award is determined by the configuration at the passed proposal's `start_time`.
To instantiate the contract, provide the following parameters:

- `owner`: The DAO sending this contract proposal hooks.
- `proposal_incentives`: Configuration for the incentives to be awarded for successful proposals. This should be specified using the `ProposalIncentivesUnchecked` structure.

## Setup

- This contract should be added as a `ProposalHook` to either the `dao-voting-single` or `dao-voting-multiple` proposal modules.
- The DAO must be set as the `owner` of this contract to manage incentives and ownership.

## Execute

- **ProposalHook(ProposalHookMsg)**: Triggered when a proposal's status changes. This is used to evaluate and potentially reward successful proposals.
- **UpdateOwnership(cw_ownable::Action)**: Updates the ownership of the contract. This can be used to transfer ownership or perform other ownership-related actions.
- **UpdateProposalIncentives { proposal_incentives: ProposalIncentivesUnchecked }**: Updates the incentives configuration. This allows the DAO to modify the rewards for successful proposals.
- **Receive(Cw20ReceiveMsg)**: Handles the receipt of CW20 tokens. This is necessary for managing CW20-based incentives.

## Query

- **ProposalIncentives { height: Option<u64> }**: Returns the current configuration of the proposal incentives. The `height` parameter is optional and can be used to query the incentives at a specific blockchain height, providing a snapshot of the incentives at that point in time.

## Configuration

The incentives can be adjusted at any time by the owner of the contract. The rewards are determined based on the configuration at the proposal's `start_time`. This allows for dynamic adjustment of incentives to reflect the DAO's evolving priorities and resources.
42 changes: 25 additions & 17 deletions contracts/external/dao-voting-incentives/README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
# dao-voting-incentives
This contract enables DAOs to offer incentives for voting on DAO proposals. By rewarding active voters, DAOs can encourage greater community involvement and decision-making.

[![dao-voting-incentives on crates.io](https://img.shields.io/crates/v/dao-voting-incentives.svg?logo=rust)](https://crates.io/crates/dao-voting-incentives)
[![docs.rs](https://img.shields.io/docsrs/dao-voting-incentives?logo=docsdotrs)](https://docs.rs/dao-voting-incentives/latest/cw_admin_factory/)
## Instantiate

Allows for DAOs to offer incentives for voting on DAO proposals.
To instantiate the contract, provide the following parameters:

When creating this contract, the DAO specifies an `epoch_duration` and an amount to pay out per epoch. Then, the DAO needs to add this contract as a `VoteHook` to the `dao-voting-single` or `dao-voting-multiple` proposal module. When DAO members vote, this contract keeps track of the proposals and who voted.
- `owner`: The DAO sending this contract voting hooks.
- `denom`: The denomination of the tokens to distribute as rewards.
- `expiration`: The expiration of the voting incentives period, defining how long the incentives are active.

At the end of the epoch, rewards are payable as follows:
## Configuration

``
rewards = (user vote count / prop count) / total_vote_count * voting incentives
``
- This contract should be added as a `VoteHook` to either the `dao-proposal-single` or `dao-proposal-multiple` proposal modules.
- The DAO must be set as the `owner` of this contract to manage incentives and ownership.

If no proposals happen during an epoch, no rewards are paid out.
If no votes are cast during the voting incentives period, then the contract's funds are sent to the `owner` on expiration.

## TODO
- [ ] Unit and Integration tests with a full DAO
- [ ] Make sure it works with multiple proposal modules (i.e. multiple choice and single choice)
- [ ] Make sure claiming rewards is gas effecient even if many epochs have passed.
- [ ] Support Cw20.
- [ ] Use `cw-ownable` to configure a contract owner who can update the voting incentives config.
- [ ] Add more info to the readme and delete this TODO section.
Rewards for a user are determined as such: `reward(user) = votes(user) * contract's balance / total votes`

## Execute

- **VoteHook(VoteHookMsg)**: Triggered when a new vote is cast. This is used to track voting activity and allocate rewards accordingly.
- **Claim {}**: Allows voters to claim their rewards after expiration.
- **Expire {}**: Expires the voting incentives period, allowing voters to claim rewards.
- **UpdateOwnership(cw_ownable::Action)**: Updates the ownership of the contract. This can be used to transfer ownership or perform other ownership-related actions.
- **Receive(Cw20ReceiveMsg)**: Handles the receipt of CW20 tokens. This is necessary for managing CW20-based incentives.

## Query

- **Config {}**: Returns the configuration of the voting incentives.
- **Rewards { address: String }**: Queries the claimable rewards for a specific address.
- **ExpectedRewards { address: String }**: Estimates the expected rewards for a specific address, based on current votes.
65 changes: 38 additions & 27 deletions contracts/external/dao-voting-incentives/src/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use dao_interface::{
};

use crate::{
state::{reward, CONFIG, GENERIC_PROPOSAL_INFO, USER_VOTE_COUNT},
state::{reward, CONFIG, GENERIC_PROPOSAL_INFO, USER_PROPOSAL_HAS_VOTED, USER_VOTE_COUNT},
ContractError,
};

Expand Down Expand Up @@ -113,32 +113,42 @@ pub fn vote_hook(

// Check if the vote came from a proposal at or after the start of the voting incentives
if proposal_info.start_height >= config.start_height {
// Increment counts
let user_votes = USER_VOTE_COUNT.update(
deps.storage,
&voter,
|x| -> StdResult<Uint128> {
Ok(x.unwrap_or_default().checked_add(Uint128::one())?)
},
)?;
config.total_votes = config.total_votes.checked_add(Uint128::one())?;
CONFIG.save(deps.storage, &config)?;

// Set attributes
attrs = vec![
Attribute {
key: "total_votes".to_string(),
value: config.total_votes.to_string(),
},
Attribute {
key: "user_votes".to_string(),
value: user_votes.to_string(),
},
Attribute {
key: "user".to_string(),
value: voter.to_string(),
},
];
// Check if the user has already voted for the proposal
if !USER_PROPOSAL_HAS_VOTED.has(deps.storage, (&voter, proposal_id)) {
// Increment counts
let user_votes = USER_VOTE_COUNT.update(
deps.storage,
&voter,
|x| -> StdResult<Uint128> {
Ok(x.unwrap_or_default().checked_add(Uint128::one())?)
},
)?;
config.total_votes = config.total_votes.checked_add(Uint128::one())?;
CONFIG.save(deps.storage, &config)?;

// Set has voted
USER_PROPOSAL_HAS_VOTED.save(
deps.storage,
(&voter, proposal_id),
&true,
)?;

// Set attributes
attrs = vec![
Attribute {
key: "total_votes".to_string(),
value: config.total_votes.to_string(),
},
Attribute {
key: "user_votes".to_string(),
value: user_votes.to_string(),
},
Attribute {
key: "user".to_string(),
value: voter.to_string(),
},
];
}
}
}
}
Expand Down Expand Up @@ -187,6 +197,7 @@ pub fn expire(deps: DepsMut, env: Env, _info: MessageInfo) -> Result<Response, C

// Clean state
GENERIC_PROPOSAL_INFO.clear(deps.storage);
USER_PROPOSAL_HAS_VOTED.clear(deps.storage);

Ok(Response::new()
.add_attribute("action", "expire")
Expand Down
3 changes: 3 additions & 0 deletions contracts/external/dao-voting-incentives/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ pub struct Config {

/// A map of user address to vote count
pub const USER_VOTE_COUNT: Map<&Addr, Uint128> = Map::new("user_vote_count");
/// A map of user address with proposal id to has voted value
/// This map is useful for cases where a proposal module allows revoting, so users cannot spam votes for more rewards
pub const USER_PROPOSAL_HAS_VOTED: Map<(&Addr, u64), bool> = Map::new("user_proposal_has_voted");
/// The voting incentives config
pub const CONFIG: Item<Config> = Item::new("config");
/// A cache of generic proposal information (proposal_module, proposal_id)
Expand Down

0 comments on commit f0c494b

Please sign in to comment.