Skip to content

Commit 83e9a98

Browse files
authored
fix reward distributor interval delay bug (#891)
1 parent c05a6c8 commit 83e9a98

File tree

37 files changed

+534
-331
lines changed

37 files changed

+534
-331
lines changed

Cargo.lock

Lines changed: 223 additions & 223 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 46 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ resolver = "2"
1818
edition = "2021"
1919
license = "BSD-3-Clause"
2020
repository = "https://github.com/DA0-DA0/dao-contracts"
21-
version = "2.5.0"
21+
version = "2.5.1"
2222

2323
[profile.release]
2424
codegen-units = 1
@@ -85,51 +85,51 @@ wynd-utils = "0.4"
8585
# optional owner.
8686
cw-ownable = "0.5"
8787

88-
btsg-ft-factory = { path = "./contracts/external/btsg-ft-factory", version = "2.5.0" }
89-
cw-admin-factory = { path = "./contracts/external/cw-admin-factory", version = "2.5.0" }
90-
cw-denom = { path = "./packages/cw-denom", version = "2.5.0" }
91-
cw-fund-distributor = { path = "./contracts/distribution/cw-fund-distributor", version = "2.5.0" }
92-
cw-hooks = { path = "./packages/cw-hooks", version = "2.5.0" }
93-
cw-paginate-storage = { path = "./packages/cw-paginate-storage", version = "2.5.0" }
94-
cw-payroll-factory = { path = "./contracts/external/cw-payroll-factory", version = "2.5.0" }
95-
cw-stake-tracker = { path = "./packages/cw-stake-tracker", version = "2.5.0" }
96-
cw-token-swap = { path = "./contracts/external/cw-token-swap", version = "2.5.0" }
97-
cw-tokenfactory-issuer = { path = "./contracts/external/cw-tokenfactory-issuer", version = "2.5.0", default-features = false }
98-
cw-tokenfactory-types = { path = "./packages/cw-tokenfactory-types", version = "2.5.0", default-features = false }
99-
cw-vesting = { path = "./contracts/external/cw-vesting", version = "2.5.0" }
100-
cw-wormhole = { path = "./packages/cw-wormhole", version = "2.5.0" }
101-
cw20-stake = { path = "./contracts/staking/cw20-stake", version = "2.5.0" }
102-
cw20-stake-external-rewards = { path = "./contracts/staking/cw20-stake-external-rewards", version = "2.5.0" }
103-
cw20-stake-reward-distributor = { path = "./contracts/staking/cw20-stake-reward-distributor", version = "2.5.0" }
104-
cw721-controllers = { path = "./packages/cw721-controllers", version = "2.5.0" }
105-
cw721-roles = { path = "./contracts/external/cw721-roles", version = "2.5.0" }
106-
dao-cw721-extensions = { path = "./packages/dao-cw721-extensions", version = "2.5.0" }
107-
dao-dao-core = { path = "./contracts/dao-dao-core", version = "2.5.0" }
108-
dao-dao-macros = { path = "./packages/dao-dao-macros", version = "2.5.0" }
109-
dao-hooks = { path = "./packages/dao-hooks", version = "2.5.0" }
110-
dao-interface = { path = "./packages/dao-interface", version = "2.5.0" }
111-
dao-migrator = { path = "./contracts/external/dao-migrator", version = "2.5.0" }
112-
dao-pre-propose-approval-single = { path = "./contracts/pre-propose/dao-pre-propose-approval-single", version = "2.5.0" }
113-
dao-pre-propose-approver = { path = "./contracts/pre-propose/dao-pre-propose-approver", version = "2.5.0" }
114-
dao-pre-propose-base = { path = "./packages/dao-pre-propose-base", version = "2.5.0" }
115-
dao-pre-propose-multiple = { path = "./contracts/pre-propose/dao-pre-propose-multiple", version = "2.5.0" }
116-
dao-pre-propose-single = { path = "./contracts/pre-propose/dao-pre-propose-single", version = "2.5.0" }
117-
dao-proposal-condorcet = { path = "./contracts/proposal/dao-proposal-condorcet", version = "2.5.0" }
118-
dao-proposal-hook-counter = { path = "./contracts/test/dao-proposal-hook-counter", version = "2.5.0" }
119-
dao-proposal-multiple = { path = "./contracts/proposal/dao-proposal-multiple", version = "2.5.0" }
120-
dao-proposal-single = { path = "./contracts/proposal/dao-proposal-single", version = "2.5.0" }
121-
dao-proposal-sudo = { path = "./contracts/test/dao-proposal-sudo", version = "2.5.0" }
122-
dao-rewards-distributor = { path = "./contracts/distribution/dao-rewards-distributor", version = "2.5.0" }
123-
dao-test-custom-factory = { path = "./contracts/test/dao-test-custom-factory", version = "2.5.0" }
124-
dao-testing = { path = "./packages/dao-testing", version = "2.5.0" }
125-
dao-voting = { path = "./packages/dao-voting", version = "2.5.0" }
126-
dao-voting-cw20-balance = { path = "./contracts/test/dao-voting-cw20-balance", version = "2.5.0" }
127-
dao-voting-cw20-staked = { path = "./contracts/voting/dao-voting-cw20-staked", version = "2.5.0" }
128-
dao-voting-cw4 = { path = "./contracts/voting/dao-voting-cw4", version = "2.5.0" }
129-
dao-voting-cw721-roles = { path = "./contracts/voting/dao-voting-cw721-roles", version = "2.5.0" }
130-
dao-voting-cw721-staked = { path = "./contracts/voting/dao-voting-cw721-staked", version = "2.5.0" }
131-
dao-voting-onft-staked = { path = "./contracts/voting/dao-voting-onft-staked", version = "2.5.0" }
132-
dao-voting-token-staked = { path = "./contracts/voting/dao-voting-token-staked", version = "2.5.0" }
88+
btsg-ft-factory = { path = "./contracts/external/btsg-ft-factory", version = "2.5.1" }
89+
cw-admin-factory = { path = "./contracts/external/cw-admin-factory", version = "2.5.1" }
90+
cw-denom = { path = "./packages/cw-denom", version = "2.5.1" }
91+
cw-fund-distributor = { path = "./contracts/distribution/cw-fund-distributor", version = "2.5.1" }
92+
cw-hooks = { path = "./packages/cw-hooks", version = "2.5.1" }
93+
cw-paginate-storage = { path = "./packages/cw-paginate-storage", version = "2.5.1" }
94+
cw-payroll-factory = { path = "./contracts/external/cw-payroll-factory", version = "2.5.1" }
95+
cw-stake-tracker = { path = "./packages/cw-stake-tracker", version = "2.5.1" }
96+
cw-token-swap = { path = "./contracts/external/cw-token-swap", version = "2.5.1" }
97+
cw-tokenfactory-issuer = { path = "./contracts/external/cw-tokenfactory-issuer", version = "2.5.1", default-features = false }
98+
cw-tokenfactory-types = { path = "./packages/cw-tokenfactory-types", version = "2.5.1", default-features = false }
99+
cw-vesting = { path = "./contracts/external/cw-vesting", version = "2.5.1" }
100+
cw-wormhole = { path = "./packages/cw-wormhole", version = "2.5.1" }
101+
cw20-stake = { path = "./contracts/staking/cw20-stake", version = "2.5.1" }
102+
cw20-stake-external-rewards = { path = "./contracts/staking/cw20-stake-external-rewards", version = "2.5.1" }
103+
cw20-stake-reward-distributor = { path = "./contracts/staking/cw20-stake-reward-distributor", version = "2.5.1" }
104+
cw721-controllers = { path = "./packages/cw721-controllers", version = "2.5.1" }
105+
cw721-roles = { path = "./contracts/external/cw721-roles", version = "2.5.1" }
106+
dao-cw721-extensions = { path = "./packages/dao-cw721-extensions", version = "2.5.1" }
107+
dao-dao-core = { path = "./contracts/dao-dao-core", version = "2.5.1" }
108+
dao-dao-macros = { path = "./packages/dao-dao-macros", version = "2.5.1" }
109+
dao-hooks = { path = "./packages/dao-hooks", version = "2.5.1" }
110+
dao-interface = { path = "./packages/dao-interface", version = "2.5.1" }
111+
dao-migrator = { path = "./contracts/external/dao-migrator", version = "2.5.1" }
112+
dao-pre-propose-approval-single = { path = "./contracts/pre-propose/dao-pre-propose-approval-single", version = "2.5.1" }
113+
dao-pre-propose-approver = { path = "./contracts/pre-propose/dao-pre-propose-approver", version = "2.5.1" }
114+
dao-pre-propose-base = { path = "./packages/dao-pre-propose-base", version = "2.5.1" }
115+
dao-pre-propose-multiple = { path = "./contracts/pre-propose/dao-pre-propose-multiple", version = "2.5.1" }
116+
dao-pre-propose-single = { path = "./contracts/pre-propose/dao-pre-propose-single", version = "2.5.1" }
117+
dao-proposal-condorcet = { path = "./contracts/proposal/dao-proposal-condorcet", version = "2.5.1" }
118+
dao-proposal-hook-counter = { path = "./contracts/test/dao-proposal-hook-counter", version = "2.5.1" }
119+
dao-proposal-multiple = { path = "./contracts/proposal/dao-proposal-multiple", version = "2.5.1" }
120+
dao-proposal-single = { path = "./contracts/proposal/dao-proposal-single", version = "2.5.1" }
121+
dao-proposal-sudo = { path = "./contracts/test/dao-proposal-sudo", version = "2.5.1" }
122+
dao-rewards-distributor = { path = "./contracts/distribution/dao-rewards-distributor", version = "2.5.1" }
123+
dao-test-custom-factory = { path = "./contracts/test/dao-test-custom-factory", version = "2.5.1" }
124+
dao-testing = { path = "./packages/dao-testing", version = "2.5.1" }
125+
dao-voting = { path = "./packages/dao-voting", version = "2.5.1" }
126+
dao-voting-cw20-balance = { path = "./contracts/test/dao-voting-cw20-balance", version = "2.5.1" }
127+
dao-voting-cw20-staked = { path = "./contracts/voting/dao-voting-cw20-staked", version = "2.5.1" }
128+
dao-voting-cw4 = { path = "./contracts/voting/dao-voting-cw4", version = "2.5.1" }
129+
dao-voting-cw721-roles = { path = "./contracts/voting/dao-voting-cw721-roles", version = "2.5.1" }
130+
dao-voting-cw721-staked = { path = "./contracts/voting/dao-voting-cw721-staked", version = "2.5.1" }
131+
dao-voting-onft-staked = { path = "./contracts/voting/dao-voting-onft-staked", version = "2.5.1" }
132+
dao-voting-token-staked = { path = "./contracts/voting/dao-voting-token-staked", version = "2.5.1" }
133133

134134
# v1 dependencies. used for state migrations.
135135
cw-core-v1 = { package = "cw-core", version = "0.1.0" }

contracts/dao-dao-core/schema/dao-dao-core.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"contract_name": "dao-dao-core",
3-
"contract_version": "2.5.0",
3+
"contract_version": "2.5.1",
44
"idl_version": "1.0.0",
55
"instantiate": {
66
"$schema": "http://json-schema.org/draft-07/schema#",

contracts/distribution/cw-fund-distributor/schema/cw-fund-distributor.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"contract_name": "cw-fund-distributor",
3-
"contract_version": "2.5.0",
3+
"contract_version": "2.5.1",
44
"idl_version": "1.0.0",
55
"instantiate": {
66
"$schema": "http://json-schema.org/draft-07/schema#",

contracts/distribution/dao-rewards-distributor/schema/dao-rewards-distributor.json

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"contract_name": "dao-rewards-distributor",
3-
"contract_version": "2.5.0",
3+
"contract_version": "2.5.1",
44
"idl_version": "1.0.0",
55
"instantiate": {
66
"$schema": "http://json-schema.org/draft-07/schema#",
@@ -225,6 +225,28 @@
225225
},
226226
"additionalProperties": false
227227
},
228+
{
229+
"description": "forcibly withdraw funds from the contract. this is unsafe and should only be used to recover funds that are stuck in the contract.",
230+
"type": "object",
231+
"required": [
232+
"unsafe_force_withdraw"
233+
],
234+
"properties": {
235+
"unsafe_force_withdraw": {
236+
"type": "object",
237+
"required": [
238+
"amount"
239+
],
240+
"properties": {
241+
"amount": {
242+
"$ref": "#/definitions/Coin"
243+
}
244+
},
245+
"additionalProperties": false
246+
}
247+
},
248+
"additionalProperties": false
249+
},
228250
{
229251
"description": "Update the contract's ownership. The `action` to be provided can be either to propose transferring ownership to an account, accept a pending ownership transfer, or renounce the ownership permanently.",
230252
"type": "object",
@@ -299,6 +321,21 @@
299321
"description": "Binary is a wrapper around Vec<u8> to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec<u8>. See also <https://github.com/CosmWasm/cosmwasm/blob/main/docs/MESSAGE_TYPES.md>.",
300322
"type": "string"
301323
},
324+
"Coin": {
325+
"type": "object",
326+
"required": [
327+
"amount",
328+
"denom"
329+
],
330+
"properties": {
331+
"amount": {
332+
"$ref": "#/definitions/Uint128"
333+
},
334+
"denom": {
335+
"type": "string"
336+
}
337+
}
338+
},
302339
"CreateMsg": {
303340
"type": "object",
304341
"required": [

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

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#[cfg(not(feature = "library"))]
22
use cosmwasm_std::entry_point;
33
use cosmwasm_std::{
4-
ensure, from_json, to_json_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Order,
5-
Response, StdError, StdResult, Uint128, Uint256,
4+
ensure, from_json, to_json_binary, Addr, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env,
5+
MessageInfo, Order, Response, StdError, StdResult, Uint128, Uint256,
66
};
77
use cw2::{get_contract_version, set_contract_version};
88
use cw20::{Cw20ReceiveMsg, Denom};
@@ -91,6 +91,9 @@ pub fn execute(
9191
ExecuteMsg::FundLatest {} => execute_fund_latest_native(deps, env, info),
9292
ExecuteMsg::Claim { id } => execute_claim(deps, env, info, id),
9393
ExecuteMsg::Withdraw { id } => execute_withdraw(deps, info, env, id),
94+
ExecuteMsg::UnsafeForceWithdraw { amount } => {
95+
execute_unsafe_force_withdraw(deps, info, amount)
96+
}
9497
}
9598
}
9699

@@ -594,6 +597,27 @@ fn execute_update_owner(
594597
Ok(Response::new().add_attributes(ownership.into_attributes()))
595598
}
596599

600+
fn execute_unsafe_force_withdraw(
601+
deps: DepsMut,
602+
info: MessageInfo,
603+
amount: Coin,
604+
) -> Result<Response, ContractError> {
605+
nonpayable(&info)?;
606+
607+
// only the owner can initiate a force withdraw
608+
cw_ownable::assert_owner(deps.storage, &info.sender)?;
609+
610+
let send = CosmosMsg::Bank(BankMsg::Send {
611+
to_address: info.sender.to_string(),
612+
amount: vec![amount.clone()],
613+
});
614+
615+
Ok(Response::new()
616+
.add_message(send)
617+
.add_attribute("action", "unsafe_force_withdraw")
618+
.add_attribute("amount", amount.to_string()))
619+
}
620+
597621
#[cfg_attr(not(feature = "library"), entry_point)]
598622
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
599623
match msg {
@@ -737,7 +761,7 @@ pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response, C
737761

738762
// only allow upgrades
739763
if new_version <= current_version {
740-
return Err(ContractError::MigrationErrorInvalidVersion {
764+
return Err(ContractError::MigrationErrorInvalidVersionNotNewer {
741765
new: new_version.to_string(),
742766
current: current_version.to_string(),
743767
});

contracts/distribution/dao-rewards-distributor/src/error.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use cosmwasm_std::{DivideByZeroError, OverflowError, StdError};
1+
use cosmwasm_std::{
2+
CheckedFromRatioError, CheckedMultiplyFractionError, DivideByZeroError, OverflowError, StdError,
3+
};
24
use cw_utils::PaymentError;
35
use thiserror::Error;
46

@@ -19,6 +21,12 @@ pub enum ContractError {
1921
#[error(transparent)]
2022
DivideByZero(#[from] DivideByZeroError),
2123

24+
#[error(transparent)]
25+
CheckedFromRatio(#[from] CheckedFromRatioError),
26+
27+
#[error(transparent)]
28+
CheckedMultiplyFraction(#[from] CheckedMultiplyFractionError),
29+
2230
#[error(transparent)]
2331
Payment(#[from] PaymentError),
2432

@@ -59,7 +67,7 @@ pub enum ContractError {
5967
DistributionHistoryTooLarge { err: String },
6068

6169
#[error("Invalid version migration. {new} is not newer than {current}.")]
62-
MigrationErrorInvalidVersion { new: String, current: String },
70+
MigrationErrorInvalidVersionNotNewer { new: String, current: String },
6371

6472
#[error("Expected to migrate from contract {expected}. Got {actual}.")]
6573
MigrationErrorIncorrectContract { expected: String, actual: String },

contracts/distribution/dao-rewards-distributor/src/helpers.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use cosmwasm_std::{
2-
coins, to_json_binary, Addr, BankMsg, BlockInfo, CosmosMsg, Deps, DepsMut, StdError, StdResult,
3-
Uint128, Uint256, WasmMsg,
2+
coins, to_json_binary, Addr, BankMsg, BlockInfo, CosmosMsg, Decimal, Deps, DepsMut, StdError,
3+
StdResult, Uint128, Uint256, WasmMsg,
44
};
55
use cw20::{Denom, Expiration};
66
use cw_utils::Duration;
@@ -117,9 +117,9 @@ pub trait DurationExt {
117117
/// Returns true if the duration is 0 blocks or 0 seconds.
118118
fn is_zero(&self) -> bool;
119119

120-
/// Perform checked integer division between two durations, erroring if the
121-
/// units do not match or denominator is 0.
122-
fn checked_div(&self, denominator: &Self) -> Result<Uint128, ContractError>;
120+
/// Returns the ratio between the two durations (numerator / denominator) as
121+
/// a Decimal, erroring if the units do not match.
122+
fn ratio(&self, denominator: &Self) -> Result<Decimal, ContractError>;
123123
}
124124

125125
impl DurationExt for Duration {
@@ -130,13 +130,13 @@ impl DurationExt for Duration {
130130
}
131131
}
132132

133-
fn checked_div(&self, denominator: &Self) -> Result<Uint128, ContractError> {
133+
fn ratio(&self, denominator: &Self) -> Result<Decimal, ContractError> {
134134
match (self, denominator) {
135135
(Duration::Height(numerator), Duration::Height(denominator)) => {
136-
Ok(Uint128::from(*numerator).checked_div(Uint128::from(*denominator))?)
136+
Ok(Decimal::checked_from_ratio(*numerator, *denominator)?)
137137
}
138138
(Duration::Time(numerator), Duration::Time(denominator)) => {
139-
Ok(Uint128::from(*numerator).checked_div(Uint128::from(*denominator))?)
139+
Ok(Decimal::checked_from_ratio(*numerator, *denominator)?)
140140
}
141141
_ => Err(ContractError::Std(StdError::generic_err(format!(
142142
"incompatible durations: got numerator {:?} and denominator {:?}",

contracts/distribution/dao-rewards-distributor/src/msg.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use cosmwasm_schema::{cw_serde, QueryResponses};
2-
use cosmwasm_std::Uint128;
2+
use cosmwasm_std::{Coin, Uint128};
33
use cw20::{Cw20ReceiveMsg, Denom, UncheckedDenom};
44
use cw4::MemberChangedHookMsg;
55
use cw_ownable::cw_ownable_execute;
@@ -60,6 +60,9 @@ pub enum ExecuteMsg {
6060
/// claim whatever they earned until this point. this is effectively an
6161
/// inverse to fund and does not affect any already-distributed rewards.
6262
Withdraw { id: u64 },
63+
/// forcibly withdraw funds from the contract. this is unsafe and should
64+
/// only be used to recover funds that are stuck in the contract.
65+
UnsafeForceWithdraw { amount: Coin },
6366
}
6467

6568
#[cw_serde]

contracts/distribution/dao-rewards-distributor/src/rewards.rs

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -121,17 +121,13 @@ pub fn get_active_total_earned_puvp(
121121
if total_power.is_zero() {
122122
Ok(curr)
123123
} else {
124-
// count intervals of the rewards emission that have passed
125-
// since the last update which need to be distributed
124+
// count (partial) intervals of the rewards emission that have
125+
// passed since the last update which need to be distributed
126126
let complete_distribution_periods =
127-
new_reward_distribution_duration.checked_div(&duration)?;
128-
129-
// It is impossible for this to overflow as total rewards can
130-
// never exceed max value of Uint128 as total tokens in
131-
// existence cannot exceed Uint128 (because the bank module Coin
132-
// type uses Uint128).
133-
let new_rewards_distributed = amount
134-
.full_mul(complete_distribution_periods)
127+
new_reward_distribution_duration.ratio(&duration)?;
128+
129+
let new_rewards_distributed = Uint256::from(amount)
130+
.checked_mul_floor(complete_distribution_periods)?
135131
.checked_mul(scale_factor())?;
136132

137133
// the new rewards per unit voting power that have been

0 commit comments

Comments
 (0)