diff --git a/contracts/external/dao-proposal-incentives/README.md b/contracts/external/dao-proposal-incentives/README.md index 106344544..f63d7b00a 100644 --- a/contracts/external/dao-proposal-incentives/README.md +++ b/contracts/external/dao-proposal-incentives/README.md @@ -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`. \ No newline at end of file +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 }**: 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. \ No newline at end of file diff --git a/contracts/external/dao-voting-incentives/README.md b/contracts/external/dao-voting-incentives/README.md index df486cb0c..dbb0da0e9 100644 --- a/contracts/external/dao-voting-incentives/README.md +++ b/contracts/external/dao-voting-incentives/README.md @@ -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. diff --git a/contracts/external/dao-voting-incentives/src/execute.rs b/contracts/external/dao-voting-incentives/src/execute.rs index df5a75205..bed6fe22f 100644 --- a/contracts/external/dao-voting-incentives/src/execute.rs +++ b/contracts/external/dao-voting-incentives/src/execute.rs @@ -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, }; @@ -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 { - 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 { + 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(), + }, + ]; + } } } } @@ -187,6 +197,7 @@ pub fn expire(deps: DepsMut, env: Env, _info: MessageInfo) -> Result = 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 = Item::new("config"); /// A cache of generic proposal information (proposal_module, proposal_id)