Skip to content

Commit

Permalink
Initial draft of dao staking support in cw-vesting
Browse files Browse the repository at this point in the history
  • Loading branch information
JakeHartnell committed Dec 12, 2023
1 parent 2983b53 commit a73d749
Show file tree
Hide file tree
Showing 8 changed files with 284 additions and 10 deletions.
3 changes: 3 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 contracts/external/cw-vesting/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ cw-utils = { workspace = true }
cw-wormhole = { workspace = true }
cw2 = { workspace = true }
cw20 = { workspace = true }
dao-pre-propose-single = { workspace = true, features = ["library"] }
dao-proposal-single = { workspace = true, features = ["library"] }
dao-voting = { workspace = true }
dao-voting-token-staked = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
wynd-utils = { workspace = true }
Expand Down
231 changes: 228 additions & 3 deletions contracts/external/cw-vesting/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ use cosmwasm_std::entry_point;
use cosmwasm_std::{
from_json, to_json_binary, Binary, Coin, CosmosMsg, DelegationResponse, Deps, DepsMut,
DistributionMsg, Env, MessageInfo, Response, StakingMsg, StakingQuery, StdResult, Timestamp,
Uint128,
Uint128, WasmMsg,
};
use cw2::set_contract_version;
use cw20::Cw20ReceiveMsg;
use cw_denom::CheckedDenom;
use cw_ownable::OwnershipError;
use cw_utils::{must_pay, nonpayable};
use dao_voting::deposit::DepositRefundPolicy;
use dao_voting::pre_propose::ProposalCreationPolicy;
use dao_voting::{proposal::SingleChoiceProposeMsg, voting::Vote};

use crate::error::ContractError;
use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, ReceiveMsg};
use crate::state::{PAYMENT, UNBONDING_DURATION_SECONDS};
use crate::msg::{DaoActionsMsg, ExecuteMsg, InstantiateMsg, QueryMsg, ReceiveMsg};
use crate::state::{DAO_STAKING_LIMITS, PAYMENT, UNBONDING_DURATION_SECONDS};
use crate::vesting::{Status, VestInit};

const CONTRACT_NAME: &str = "crates.io:cw-vesting";
Expand Down Expand Up @@ -68,12 +71,22 @@ pub fn instantiate(
// payment receiver so that when they stake vested tokens
// they receive the rewards.
if denom.as_str() == deps.querier.query_bonded_denom()? {
// Check if dao_staking is enabled. Throw an error if it is.
if let Some(_) = msg.dao_staking {
return Err(ContractError::DaoStakingNotSupported {});
}

Some(CosmosMsg::Distribution(
DistributionMsg::SetWithdrawAddress {
address: vest.recipient.to_string(),
},
))
} else {
// Check if dao_staking is enabled, and save dao_staking limits if it is.
if let Some(dao_staking) = msg.dao_staking {
DAO_STAKING_LIMITS.save(deps.storage, &dao_staking)?;
}

None
}
}
Expand Down Expand Up @@ -125,6 +138,34 @@ pub fn execute(
amount,
during_unbonding,
} => execute_register_slash(deps, env, info, validator, time, amount, during_unbonding),
ExecuteMsg::DaoActions(action) => match action {
DaoActionsMsg::Stake {
amount,
staking_contract,
} => execute_dao_stake(deps, env, info, amount, staking_contract),
DaoActionsMsg::Unstake {
amount,
staking_contract,
} => execute_dao_unstake(deps, env, info, amount, staking_contract),
DaoActionsMsg::Vote {
proposal_module,
proposal_id,
vote,
rationale,
} => execute_dao_vote(
deps,
env,
info,
proposal_module,
proposal_id,
vote,
rationale,
),
DaoActionsMsg::Propose {
proposal,
proposal_module,
} => execute_dao_propose(deps, env, info, proposal_module, proposal),
},
}
}

Expand Down Expand Up @@ -443,6 +484,190 @@ pub fn execute_register_slash(
}
}

/// Stake tokens in a DAO
pub fn execute_dao_stake(
deps: DepsMut,
_env: Env,
info: MessageInfo,
amount: Uint128,
staking_contract: String,
) -> Result<Response, ContractError> {
// Validate staking contract address
deps.api.addr_validate(&staking_contract)?;

// Load vest and check status, only recipients can stake
let vest = PAYMENT.get_vest(deps.storage)?;
match vest.status {
Status::Unfunded => return Err(ContractError::NotFunded),
Status::Funded => {
if info.sender != vest.recipient {
return Err(ContractError::NotReceiver);
}
}
Status::Canceled { .. } => return Err(ContractError::Cancelled),
}

// Validate staking contract is on the allowist
// Otherwise staking might be abused to get tokens out of this contract
let dao_staking = DAO_STAKING_LIMITS.load(deps.storage)?;
if !dao_staking
.staking_contract_allowlist
.contains(&staking_contract)
{
return Err(ContractError::NotOnAllowlist);
}

// TODO limitations on how much can be staked?

// Construct stake message
let msg = WasmMsg::Execute {
contract_addr: staking_contract.clone(),
msg: to_json_binary(&dao_voting_token_staked::msg::ExecuteMsg::Stake {})?,
funds: vec![Coin {
denom: vest.denom.to_string(),
amount: amount,
}],
};

Ok(Response::default()
.add_message(msg)
.add_attribute("method", "execute_dao_stake")
.add_attribute("staking_contract", staking_contract)
.add_attribute("amount", amount))
}

/// Unstake tokens in a DAO
pub fn execute_dao_unstake(
deps: DepsMut,
_env: Env,
info: MessageInfo,
amount: Uint128,
staking_contract: String,
) -> Result<Response, ContractError> {
// Validate staking contract address
deps.api.addr_validate(&staking_contract)?;

// Load vest and check status, only recipients can unstake
let vest = PAYMENT.get_vest(deps.storage)?;
if info.sender != vest.recipient {
return Err(ContractError::NotReceiver);
};

// Construct unstake message
let msg = WasmMsg::Execute {
contract_addr: staking_contract.clone(),
msg: to_json_binary(&dao_voting_token_staked::msg::ExecuteMsg::Unstake { amount })?,
funds: vec![],
};

Ok(Response::default()
.add_message(msg)
.add_attribute("method", "execute_dao_unstake")
.add_attribute("staking_contract", staking_contract)
.add_attribute("amount", amount))
}

/// Vote in a DAO
pub fn execute_dao_vote(
deps: DepsMut,
_env: Env,
info: MessageInfo,
proposal_module: String,
proposal_id: u64,
vote: Vote,
rationale: Option<String>,
) -> Result<Response, ContractError> {
// Validate proposal module contract address
deps.api.addr_validate(&proposal_module)?;

// Check sender is the recipient of the vesting contract
let vest = PAYMENT.get_vest(deps.storage)?;
if info.sender != vest.recipient {
return Err(ContractError::NotReceiver);
};

// Construct voting message
let msg = WasmMsg::Execute {
contract_addr: proposal_module.clone(),
msg: to_json_binary(&dao_proposal_single::msg::ExecuteMsg::Vote {
proposal_id,
vote,
rationale: rationale.clone(),
})?,
funds: vec![],
};

Ok(Response::default()
.add_message(msg)
.add_attribute("method", "execute_dao_vote")
.add_attribute("proposal_module", proposal_module)
.add_attribute("proposal_id", proposal_id.to_string())
.add_attribute("vote", vote.to_string())
.add_attribute("rationale", rationale.unwrap_or_default()))
}

// TODO how to handle if a deposit is slashed?
// TODO maybe we just don't handle proposals?
// Makes this contract much easier... as we don't have to worry about deposits?
// Cheeper audit too...
/// Create a proposal in a DAO
pub fn execute_dao_propose(
deps: DepsMut,
_env: Env,
info: MessageInfo,
proposal_module: String,
proposal: SingleChoiceProposeMsg,
) -> Result<Response, ContractError> {
// Check sender is the recipient of the vesting contract
let vest = PAYMENT.get_vest(deps.storage)?;
if info.sender != vest.recipient {
return Err(ContractError::NotReceiver);
};

// Validate proposal module contract address
let proposal_contract = deps.api.addr_validate(&proposal_module)?;

// Get proposal creation policy
let policy = deps.querier.query_wasm_smart(
proposal_contract,
&dao_proposal_single::msg::QueryMsg::ProposalCreationPolicy {},
)?;

// Match policy, if anyone proceed to make proposal directly
// otherwise check deposits?
match policy {
ProposalCreationPolicy::Anyone {} => {
// Construct message to submit proposal directly
let msg = WasmMsg::Execute {
contract_addr: proposal_module.clone(),
msg: to_json_binary(&dao_proposal_single::msg::ExecuteMsg::Propose(proposal))?,
funds: vec![],
};

Ok(Response::default()
.add_message(msg)
.add_attribute("method", "execute_dao_propose")
.add_attribute("proposal_module", proposal_module))
}
ProposalCreationPolicy::Module { addr } => {
// Get the config for the pre-propose module
let config: dao_pre_propose_single::Config = deps
.querier
.query_wasm_smart(addr, &dao_pre_propose_single::QueryMsg::Config {})?;

if let Some(deposit_info) = config.deposit_info {
match deposit_info.refund_policy {
DepositRefundPolicy::Always {} => unimplemented!(),
DepositRefundPolicy::Never {} => unimplemented!(),
DepositRefundPolicy::OnlyPassed {} => unimplemented!(),
}
}

unimplemented!();
}
}
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
Expand Down
8 changes: 8 additions & 0 deletions contracts/external/cw-vesting/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ pub enum ContractError {
#[error("payment is cancelled")]
Cancelled,

// TODO better error message / name
#[error("DAO staking is not supported for the native staking token")]
DaoStakingNotSupported,

// TODO better error message / name
#[error("staking contract is not on dao staking allowlist")]
NotOnAllowlist,

#[error("payment is not cancelled")]
NotCancelled,

Expand Down
36 changes: 29 additions & 7 deletions contracts/external/cw-vesting/src/msg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use cw_ownable::cw_ownable_execute;
use cw_stake_tracker::StakeTrackerQuery;
use dao_voting::{proposal::SingleChoiceProposeMsg, voting::Vote};

use crate::vesting::Schedule;
use crate::{state::DaoStakingLimits, vesting::Schedule};

#[cw_serde]
pub struct InstantiateMsg {
Expand All @@ -27,6 +27,11 @@ pub struct InstantiateMsg {
/// The type and denom of token being vested.
pub denom: UncheckedDenom,

/// TODO support limits for native staked tokens as well?
/// Optionally enabling this vesting contract to stake in token DAOs.
/// Set to None if vesting a native token.
pub dao_staking: Option<DaoStakingLimits>,

/// The vesting schedule, can be either `SaturatingLinear` vesting
/// (which vests evenly over time), or `PiecewiseLinear` which can
/// represent a more complicated vesting schedule.
Expand Down Expand Up @@ -70,6 +75,8 @@ pub enum ExecuteMsg {
/// Anyone may call this method so long as the contract has not
/// yet been funded.
Receive(Cw20ReceiveMsg),
/// TODO we need something for things like airdrops? Should we have a general
/// execute method that can be used for this?
/// Distribute vested tokens to the vest receiver. Anyone may call
/// this method.
Distribute {
Expand Down Expand Up @@ -184,21 +191,26 @@ pub enum ExecuteMsg {
DaoActions(DaoActionsMsg),
}

// TODO we may need to pass in staking contract address?
#[cw_serde]
pub enum DaoActionsMsg {
/// Stake to a DAO
Stake {
/// The amount to stake. Default is full amount.
amount: Option<Uint128>,
/// The staking contract address
staking_contract: String,
/// The amount to stake.
amount: Uint128,
},
/// Unstake from a DAO
Unstake {
/// The amount to unstake. Default is full amount.
amount: Option<Uint128>,
/// The staking contract address
staking_contract: String,
/// The amount to unstake.
amount: Uint128,
},
/// Vote on single choice proposal
Vote {
/// The address of the proposal module you are voting on.
proposal_module: String,
/// The ID of the proposal to vote on.
proposal_id: u64,
/// The senders position on the proposal.
Expand All @@ -208,9 +220,19 @@ pub enum DaoActionsMsg {
/// the vote.
rationale: Option<String>,
},
// TODO update dao_staking config
// UpdateConfig {
// /// Contracts the vesting contract is allowed to stake with.
// staking_contract_allowlist: Option<Vec<String>>,
// },
/// TODO need to figure out how to handle this, need to know the right proposal module...
/// Create a new proposal... TODO how to handle deposit?
Propose(SingleChoiceProposeMsg),
Propose {
// The address of the proposal module you are voting on.
proposal_module: String,
/// The proposal to create.
proposal: SingleChoiceProposeMsg,
},
// // TODO support multiple choice voting and proposals
// /// Vote on multiple choice proposal
// VoteMultipleChoice {
Expand Down
Loading

0 comments on commit a73d749

Please sign in to comment.