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

Reward distributor improvements #881

Merged
merged 8 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,20 @@
},
"additionalProperties": false
},
{
"description": "Used to fund the latest distribution with native tokens.",
"type": "object",
"required": [
"fund_latest"
],
"properties": {
"fund_latest": {
"type": "object",
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Claims rewards for the sender.",
"type": "object",
Expand Down Expand Up @@ -786,6 +800,30 @@
},
"additionalProperties": false
},
{
"description": "Returns the undistributed rewards for a distribution.",
"type": "object",
"required": [
"undistributed_rewards"
],
"properties": {
"undistributed_rewards": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Returns the state of the given distribution.",
"type": "object",
Expand Down Expand Up @@ -1781,6 +1819,12 @@
"type": "string"
}
}
},
"undistributed_rewards": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Uint128",
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
"type": "string"
}
}
}
94 changes: 74 additions & 20 deletions contracts/distribution/dao-rewards-distributor/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{
ensure, from_json, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response,
StdError, StdResult, Uint128, Uint256,
ensure, from_json, to_json_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Order,
Response, StdError, StdResult, Uint128, Uint256,
};
use cw2::{get_contract_version, set_contract_version};
use cw20::{Cw20ReceiveMsg, Denom};
Expand Down Expand Up @@ -68,7 +68,7 @@
ExecuteMsg::MemberChangedHook(msg) => execute_membership_changed(deps, env, info, msg),
ExecuteMsg::UpdateOwnership(action) => execute_update_owner(deps, info, env, action),
ExecuteMsg::Receive(msg) => execute_receive_cw20(deps, env, info, msg),
ExecuteMsg::Create(create_msg) => execute_create(deps, env, info, create_msg),
ExecuteMsg::Create(create_msg) => execute_create_native(deps, env, info, create_msg),
ExecuteMsg::Update {
id,
emission_rate,
Expand All @@ -86,6 +86,7 @@
withdraw_destination,
),
ExecuteMsg::Fund(FundMsg { id }) => execute_fund_native(deps, env, info, id),
ExecuteMsg::FundLatest {} => execute_fund_latest_native(deps, env, info),
ExecuteMsg::Claim { id } => execute_claim(deps, env, info, id),
ExecuteMsg::Withdraw { id } => execute_withdraw(deps, info, env, id),
}
Expand Down Expand Up @@ -119,22 +120,68 @@
}
};

execute_fund(deps, env, distribution, wrapper.amount)
}
ReceiveCw20Msg::FundLatest {} => {
let id = COUNT.load(deps.storage)?;
let distribution = DISTRIBUTIONS
.load(deps.storage, id)
.map_err(|_| ContractError::DistributionNotFound { id })?;

match &distribution.denom {
Denom::Native(_) => return Err(ContractError::InvalidFunds {}),

Check warning on line 132 in contracts/distribution/dao-rewards-distributor/src/contract.rs

View check run for this annotation

Codecov / codecov/patch

contracts/distribution/dao-rewards-distributor/src/contract.rs#L132

Added line #L132 was not covered by tests
Denom::Cw20(addr) => {
// ensure funding is coming from the cw20 we are currently
// distributing
if addr != info.sender {
return Err(ContractError::InvalidCw20 {});

Check warning on line 137 in contracts/distribution/dao-rewards-distributor/src/contract.rs

View check run for this annotation

Codecov / codecov/patch

contracts/distribution/dao-rewards-distributor/src/contract.rs#L137

Added line #L137 was not covered by tests
}
}
};

execute_fund(deps, env, distribution, wrapper.amount)
}
}
}

fn execute_create_native(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: CreateMsg,
) -> Result<Response, ContractError> {
let checked_denom = msg.denom.clone().into_checked(deps.as_ref())?;

// if native funds provided, ensure they are for this denom. if other native
// funds present, return error. if no funds, do nothing and leave registered
// denom with no funding, to be funded later.
let initial_funds = if !info.funds.is_empty() {
match &checked_denom {
Denom::Native(denom) => {
// ensures there is exactly 1 coin passed that matches the denom
Some(must_pay(&info, denom)?)
}
Denom::Cw20(_) => return Err(ContractError::NoFundsOnCw20Create {}),
}
} else {
None
};

execute_create(deps, env, info.sender, msg, initial_funds)
}

/// creates a new rewards distribution. only the owner can do this. if funds
/// provided when creating a native token distribution, will start distributing
/// rewards immediately.
fn execute_create(
deps: DepsMut,
env: Env,
info: MessageInfo,
sender: Addr,
msg: CreateMsg,
initial_funds: Option<Uint128>,
) -> Result<Response, ContractError> {
// only the owner can create a new distribution
cw_ownable::assert_owner(deps.storage, &info.sender)?;
cw_ownable::assert_owner(deps.storage, &sender)?;

// update count and use as the new distribution's ID
let id = COUNT.update(deps.storage, |count| -> StdResult<u64> { Ok(count + 1) })?;
Expand All @@ -147,7 +194,7 @@
// if withdraw destination is specified, we validate it
Some(addr) => deps.api.addr_validate(&addr)?,
// otherwise default to the owner
None => info.sender.clone(),
None => sender.clone(),
};

msg.emission_rate.validate()?;
Expand Down Expand Up @@ -185,20 +232,10 @@
.add_attribute("id", id.to_string())
.add_attribute("denom", distribution.get_denom_string());

// if native funds provided, ensure they are for this denom. if other native
// funds present, return error. if no funds, do nothing and leave registered
// denom with no funding, to be funded later.
if !info.funds.is_empty() {
match &distribution.denom {
Denom::Native(denom) => {
// ensures there is exactly 1 coin passed that matches the denom
let amount = must_pay(&info, denom)?;

execute_fund(deps, env, distribution, amount)?;

response = response.add_attribute("amount_funded", amount);
}
Denom::Cw20(_) => return Err(ContractError::NoFundsOnCw20Create {}),
if let Some(initial_funds) = initial_funds {
if !initial_funds.is_zero() {
execute_fund(deps, env, distribution, initial_funds)?;
response = response.add_attribute("amount_funded", initial_funds);
}
}

Expand Down Expand Up @@ -259,6 +296,15 @@
.add_attribute("denom", distribution.get_denom_string()))
}

fn execute_fund_latest_native(
deps: DepsMut,
env: Env,
info: MessageInfo,
) -> Result<Response, ContractError> {
let id = COUNT.load(deps.storage)?;
execute_fund_native(deps, env, info, id)
}

fn execute_fund_native(
deps: DepsMut,
env: Env,
Expand Down Expand Up @@ -561,6 +607,14 @@
start_after,
limit,
)?)?),
QueryMsg::UndistributedRewards { id } => {
let state = DISTRIBUTIONS.load(deps.storage, id)?;
Ok(to_json_binary(
&state
.get_undistributed_rewards(&env.block)
.map_err(|e| StdError::generic_err(e.to_string()))?,
)?)

Check warning on line 616 in contracts/distribution/dao-rewards-distributor/src/contract.rs

View check run for this annotation

Codecov / codecov/patch

contracts/distribution/dao-rewards-distributor/src/contract.rs#L616

Added line #L616 was not covered by tests
}
QueryMsg::Distribution { id } => {
let state = DISTRIBUTIONS.load(deps.storage, id)?;
Ok(to_json_binary(&state)?)
Expand Down
14 changes: 14 additions & 0 deletions contracts/distribution/dao-rewards-distributor/src/msg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
use cw4::MemberChangedHookMsg;
use cw_ownable::cw_ownable_execute;
use dao_hooks::{nft_stake::NftStakeChangedHookMsg, stake::StakeChangedHookMsg};
use dao_interface::voting::InfoResponse;

Check warning on line 7 in contracts/distribution/dao-rewards-distributor/src/msg.rs

View workflow job for this annotation

GitHub Actions / Test Suite

unused import: `dao_interface::voting::InfoResponse`

// so that consumers don't need a cw_ownable or cw_controllers dependency
// to consume this contract's queries.
Expand Down Expand Up @@ -50,6 +50,8 @@
Receive(Cw20ReceiveMsg),
/// Used to fund this contract with native tokens.
Fund(FundMsg),
/// Used to fund the latest distribution with native tokens.
FundLatest {},
/// Claims rewards for the sender.
Claim { id: u64 },
/// withdraws the undistributed rewards for a distribution. members can
Expand Down Expand Up @@ -83,6 +85,15 @@
pub enum ReceiveCw20Msg {
/// Used to fund this contract with cw20 tokens.
Fund(FundMsg),
/// Used to fund the latest distribution with cw20 tokens. We can't verify
/// the sender of CW20 token send contract executions; since the create
/// function is restricted to the contract owner, we cannot allow creating
/// new distributions and funding with CW20 tokens in one message (like we
/// can with native tokens via the funds field). To prevent DAOs from having
/// to submit two proposals to create+fund a CW20 distribution, we allow
/// creating and funding a distribution in one transaction via this message
/// that funds the latest distribution without knowing the ID ahead of time.
FundLatest {},
}

#[cw_serde]
Expand All @@ -101,6 +112,9 @@
start_after: Option<u64>,
limit: Option<u32>,
},
/// Returns the undistributed rewards for a distribution.
#[returns(Uint128)]
UndistributedRewards { id: u64 },
/// Returns the state of the given distribution.
#[returns(DistributionState)]
Distribution { id: u64 },
Expand Down
44 changes: 38 additions & 6 deletions contracts/distribution/dao-rewards-distributor/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,40 @@
}
}

// get the undistributed rewards based on the active epoch's emission rate
pub fn get_undistributed_rewards(
&self,
current_block: &BlockInfo,
) -> Result<Uint128, ContractError> {
match self.active_epoch.emission_rate {
// if paused, all rewards are undistributed
EmissionRate::Paused {} => Ok(self.funded_amount),
// if immediate, no rewards are distributed
EmissionRate::Immediate {} => Ok(Uint128::zero()),

Check warning on line 262 in contracts/distribution/dao-rewards-distributor/src/state.rs

View check run for this annotation

Codecov / codecov/patch

contracts/distribution/dao-rewards-distributor/src/state.rs#L262

Added line #L262 was not covered by tests
// if linear, the undistributed rewards are the portion of the
// funded amount that hasn't been distributed yet
EmissionRate::Linear {
amount, duration, ..
} => {
// get last time rewards were distributed
let last_time_rewards_distributed =
self.get_latest_reward_distribution_time(current_block);

let epoch_duration =
last_time_rewards_distributed.duration_since(&self.active_epoch.started_at)?;

// count total intervals of the rewards emission that have
// passed based on the start and last distribution times
let complete_distribution_periods = epoch_duration.checked_div(&duration)?;

let distributed = amount.checked_mul(complete_distribution_periods)?;
let undistributed = self.funded_amount.checked_sub(distributed)?;

Ok(undistributed)
}
}
}

/// Finish current epoch early and start a new one with a new emission rate.
pub fn transition_epoch(
&mut self,
Expand All @@ -262,14 +296,12 @@
return Ok(());
}

// 1. finish current epoch by updating rewards and setting end to now
// 1. finish current epoch by updating rewards and setting end to the
// last time rewards were distributed (which is either the end date
// or the current block)
self.active_epoch.total_earned_puvp =
get_active_total_earned_puvp(deps, current_block, self)?;
self.active_epoch.ends_at = match self.active_epoch.started_at {
Expiration::Never {} => Expiration::Never {},
Expiration::AtHeight(_) => Expiration::AtHeight(current_block.height),
Expiration::AtTime(_) => Expiration::AtTime(current_block.time),
};
self.active_epoch.ends_at = self.get_latest_reward_distribution_time(current_block);

// 2. add current epoch rewards earned to historical rewards
self.historical_earned_puvp = self
Expand Down
Loading
Loading