Skip to content

Commit

Permalink
Proposal Incentives
Browse files Browse the repository at this point in the history
  • Loading branch information
ismellike committed Jan 31, 2024
1 parent 21c488b commit b7b01cb
Show file tree
Hide file tree
Showing 30 changed files with 923 additions and 607 deletions.
405 changes: 202 additions & 203 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ dao-pre-propose-multiple = { path = "./contracts/pre-propose/dao-pre-propose-mul
dao-pre-propose-single = { path = "./contracts/pre-propose/dao-pre-propose-single", version = "2.4.0" }
dao-proposal-condorcet = { path = "./contracts/proposal/dao-proposal-condorcet", version = "2.4.0" }
dao-proposal-hook-counter = { path = "./contracts/test/dao-proposal-hook-counter", version = "2.4.0" }
dao-proposal-incentives = { path = "./contracts/external/dao-proposal-incentives", version = "2.4.0" }
dao-proposal-multiple = { path = "./contracts/proposal/dao-proposal-multiple", version = "2.4.0" }
dao-proposal-single = { path = "./contracts/proposal/dao-proposal-single", version = "2.4.0" }
dao-proposal-sudo = { path = "./contracts/test/dao-proposal-sudo", version = "2.4.0" }
Expand Down
8 changes: 8 additions & 0 deletions contracts/dao-dao-core/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
QueryMsg::ProposalModules { start_after, limit } => {
query_proposal_modules(deps, start_after, limit)
}
QueryMsg::ProposalModule { address } => query_proposal_module(deps, address),
QueryMsg::ProposalModuleCount {} => query_proposal_module_count(deps),
QueryMsg::TotalPowerAtHeight { height } => query_total_power_at_height(deps, height),
QueryMsg::VotingModule {} => query_voting_module(deps),
Expand All @@ -577,6 +578,13 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
}
}

pub fn query_proposal_module(deps: Deps, address: String) -> StdResult<Binary> {
let address = deps.api.addr_validate(&address)?;
let proposal_module = &PROPOSAL_MODULES.load(deps.storage, address)?;

to_json_binary(&proposal_module)
}

pub fn query_admin(deps: Deps) -> StdResult<Binary> {
let admin = ADMIN.load(deps.storage)?;
to_json_binary(&admin)
Expand Down
8 changes: 7 additions & 1 deletion contracts/external/dao-proposal-incentives/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name ="dao-proposal-incentives"
authors = ["Jake Hartnell <[email protected]>"]
authors = ["Jake Hartnell <[email protected]>", "ismellike"]
description = "A contract that implements incentives for voting in a DAO."
edition = { workspace = true }
license = { workspace = true }
Expand All @@ -26,9 +26,15 @@ dao-interface = { workspace = true }
dao-voting = { workspace = true }
thiserror = { workspace = true }
cw-utils = { workspace = true }
cw-denom = { workspace = true }
cw-ownable = { workspace = true }
cw20 = { workspace = true }

[dev-dependencies]
cosmwasm-schema = { workspace = true }
cw-multi-test = { workspace = true }
dao-dao-core = { workspace = true, features = ["library"] }
cw20-base = { workspace = true, features = ["library"] }
dao-testing = { workspace = true }
dao-proposal-single = { workspace = true, features = ["library"] }
cw-hooks = { workspace = true }
8 changes: 2 additions & 6 deletions contracts/external/dao-proposal-incentives/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@

Allows for DAOs to offer incentives for making successful proposals.

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. When someone successfully passes a proposal the specified rewards are automatically paid out.
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.

## TODO
- [ ] Unit and Integration tests with a full DAO
- [ ] Support Cw20
- [ ] Use `cw-ownable` to configure a contract owner who can update the proposal incentives config.
- [ ] Add more info to the readme and delete this TODO section
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`.
113 changes: 26 additions & 87 deletions contracts/external/dao-proposal-incentives/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,51 +1,39 @@
#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{
to_json_binary, BankMsg, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult,
SubMsg,
};
use cosmwasm_std::{to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
use cw2::set_contract_version;
use cw_utils::must_pay;
use dao_hooks::proposal::ProposalHookMsg;
use dao_voting::status::Status;
use cw_ownable::get_ownership;

use crate::error::ContractError;
use crate::msg::{ConfigResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
use crate::state::{DAO, PROPOSAL_INCENTIVES};
use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
use crate::state::PROPOSAL_INCENTIVES;
use crate::{execute, query, ContractError};

pub(crate) const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME");
pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");

pub const REPLY_PROPOSAL_HOOK_ID: u64 = 1;

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
_env: Env,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;

// Save DAO, assumes the sender is the DAO
DAO.save(deps.storage, &deps.api.addr_validate(&msg.dao)?)?;
// Save ownership
let ownership = cw_ownable::initialize_owner(deps.storage, deps.api, Some(&msg.owner))?;

// Save proposal incentives config
PROPOSAL_INCENTIVES.save(deps.storage, &msg.proposal_incentives)?;
// Validate proposal incentives
let proposal_incentives = msg.proposal_incentives.into_checked(deps.as_ref())?;

// Check initial deposit contains enough funds to pay out rewards
// for at least one proposal
let amount = must_pay(&info, &msg.proposal_incentives.rewards_per_proposal.denom)?;
if amount < msg.proposal_incentives.rewards_per_proposal.amount {
return Err(ContractError::InsufficientInitialDeposit {
expected: msg.proposal_incentives.rewards_per_proposal.amount,
actual: amount,
});
};
// Save proposal incentives config
PROPOSAL_INCENTIVES.save(deps.storage, &proposal_incentives, env.block.height)?;

Ok(Response::new()
.add_attribute("method", "instantiate")
.add_attribute("creator", info.sender))
.add_attribute("creator", info.sender)
.add_attributes(ownership.into_attributes())
.add_attributes(proposal_incentives.into_attributes()))
}

#[cfg_attr(not(feature = "library"), entry_point)]
Expand All @@ -56,79 +44,30 @@ pub fn execute(
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::ProposalHook(msg) => execute_proposal_hook(deps, env, info, msg),
}
}

// TODO support cw20 tokens
pub fn execute_proposal_hook(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: ProposalHookMsg,
) -> Result<Response, ContractError> {
let mut payout_msgs: Vec<SubMsg> = vec![];

// Check prop status and type of hook
match msg {
ProposalHookMsg::ProposalStatusChanged { new_status, .. } => {
// If prop status is success, add message to pay out rewards
// Otherwise, do nothing
if new_status == Status::Passed.to_string() {
// Load proposal incentives config
let proposal_incentives = PROPOSAL_INCENTIVES.load(deps.storage)?;

// We handle payout messages in a SubMsg so the error be caught
// if need be. This is to prevent running out of funds locking the DAO.
payout_msgs.push(SubMsg::reply_on_error(
BankMsg::Send {
to_address: info.sender.to_string(),
amount: vec![proposal_incentives.rewards_per_proposal],
},
REPLY_PROPOSAL_HOOK_ID,
));
}
ExecuteMsg::ProposalHook(msg) => execute::proposal_hook(deps, env, info, msg),
ExecuteMsg::UpdateOwnership(action) => execute::update_ownership(deps, env, info, action),
ExecuteMsg::UpdateProposalIncentives {
proposal_incentives,
} => execute::update_proposal_incentives(deps, env, info, proposal_incentives),
ExecuteMsg::Receive(cw20_receive_msg) => {
execute::receive_cw20(deps, env, info, cw20_receive_msg)
}
_ => {}
}

Ok(Response::default()
.add_attribute("action", "proposal_hook")
.add_submessages(payout_msgs))
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::Config {} => query_config(deps),
QueryMsg::ProposalIncentives { height } => {
to_json_binary(&query::proposal_incentives(deps, height)?)
}
QueryMsg::Ownership {} => to_json_binary(&get_ownership(deps.storage)?),
}
}

pub fn query_config(deps: Deps) -> StdResult<Binary> {
let dao = DAO.load(deps.storage)?;
let proposal_incentives = PROPOSAL_INCENTIVES.load(deps.storage)?;

to_json_binary(&ConfigResponse {
dao: dao.to_string(),
proposal_incentives,
})
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
// Set contract to version to latest
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
Ok(Response::default())
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn reply(_deps: DepsMut, _env: Env, msg: Reply) -> Result<Response, ContractError> {
match msg.id {
REPLY_PROPOSAL_HOOK_ID => {
// If an error occurred with payout, we still return an ok response
// because we don't want to fail the proposal hook and lock the DAO.
Ok(Response::default())
}
_ => Err(ContractError::UnknownReplyID {}),
}
}
14 changes: 11 additions & 3 deletions contracts/external/dao-proposal-incentives/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use cosmwasm_std::{StdError, Uint128};
use cosmwasm_std::StdError;
use cw_denom::DenomError;
use cw_ownable::OwnershipError;
use cw_utils::{ParseReplyError, PaymentError};
use thiserror::Error;

Expand All @@ -10,15 +12,21 @@ pub enum ContractError {
#[error("{0}")]
PaymentError(#[from] PaymentError),

#[error("You need to deposit enough incentives for at least one epoch of incentives. Expected {expected}, got {actual}.")]
InsufficientInitialDeposit { expected: Uint128, actual: Uint128 },
#[error("{0}")]
DenomError(#[from] DenomError),

#[error("{0}")]
ParseReplyError(#[from] ParseReplyError),

#[error("{0}")]
OwnershipError(#[from] OwnershipError),

#[error("Unauthorized")]
Unauthorized {},

#[error("An unknown reply ID was received.")]
UnknownReplyID {},

#[error("No reward per proposal given")]
NoRewardPerProposal {},
}
112 changes: 112 additions & 0 deletions contracts/external/dao-proposal-incentives/src/execute.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use cosmwasm_std::{Attribute, CosmosMsg, DepsMut, Env, MessageInfo, Response};
use cw20::Cw20ReceiveMsg;
use cw_ownable::{assert_owner, get_ownership};
use dao_hooks::proposal::ProposalHookMsg;
use dao_interface::{proposal::GenericProposalInfo, state::ProposalModule};
use dao_voting::status::Status;

use crate::{msg::ProposalIncentivesUnchecked, state::PROPOSAL_INCENTIVES, ContractError};

pub fn proposal_hook(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: ProposalHookMsg,
) -> Result<Response, ContractError> {
let mut msgs: Vec<CosmosMsg> = vec![];
let mut attrs: Vec<Attribute> = vec![];

// Get ownership
let ownership = get_ownership(deps.storage)?;

if let Some(owner) = ownership.owner {
// Validate the message is coming from a proposal module of the owner (DAO)
deps.querier.query_wasm_smart::<ProposalModule>(
owner,
&dao_interface::msg::QueryMsg::ProposalModule {
address: info.sender.to_string(),
},
)?;

// Check prop status and type of hook
match msg {

Check failure on line 32 in contracts/external/dao-proposal-incentives/src/execute.rs

View workflow job for this annotation

GitHub Actions / Lints

you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`
ProposalHookMsg::ProposalStatusChanged { id, new_status, .. } => {
// If prop status is success, add message to pay out rewards
// Otherwise, do nothing
if new_status == Status::Passed.to_string() {
// Query for the proposal
let proposal_info: GenericProposalInfo = deps.querier.query_wasm_smart(
info.sender,
&dao_interface::proposal::Query::GenericProposalInfo { proposal_id: id },
)?;

// Load proposal incentives config
let proposal_incentives = PROPOSAL_INCENTIVES
.may_load_at_height(deps.storage, proposal_info.start_height)?;

// Append the message if found
if let Some(proposal_incentives) = proposal_incentives {
msgs.push(proposal_incentives.denom.get_transfer_to_message(
&proposal_info.proposer,
proposal_incentives.rewards_per_proposal,
)?);
attrs = proposal_incentives.into_attributes();
attrs.push(Attribute {
key: "proposer".to_string(),
value: proposal_info.proposer.to_string(),
});
}
}
}
_ => {}
}
}

Ok(Response::default()
.add_attribute("action", "proposal_hook")
.add_attributes(attrs)
.add_messages(msgs))
}

pub fn update_ownership(
deps: DepsMut,
env: Env,
info: MessageInfo,
action: cw_ownable::Action,
) -> Result<Response, ContractError> {
let ownership = cw_ownable::update_ownership(deps, &env.block, &info.sender, action)?;

Ok(Response::new()
.add_attribute("action", "update_ownership")
.add_attributes(ownership.into_attributes()))
}

pub fn update_proposal_incentives(
deps: DepsMut,
env: Env,
info: MessageInfo,
proposal_incentives: ProposalIncentivesUnchecked,
) -> Result<Response, ContractError> {
assert_owner(deps.storage, &info.sender)?;

// Validate proposal incentives
let proposal_incentives = proposal_incentives.into_checked(deps.as_ref())?;

// Save the new proposal incentives
PROPOSAL_INCENTIVES.save(deps.storage, &proposal_incentives, env.block.height)?;

Ok(Response::new()
.add_attribute("action", "update_proposal_incentives")
.add_attributes(proposal_incentives.into_attributes()))
}

pub fn receive_cw20(
_deps: DepsMut,
_env: Env,
info: MessageInfo,
_cw20_receive_msg: Cw20ReceiveMsg,
) -> Result<Response, ContractError> {
Ok(Response::new()
.add_attribute("action", "receive_cw20")
.add_attribute("cw20", info.sender))
}
Loading

0 comments on commit b7b01cb

Please sign in to comment.