Skip to content

Commit 4c7ffe1

Browse files
authored
Merge branch 'development' into feature/cw-orch-interface
2 parents 866d498 + c8ae2f0 commit 4c7ffe1

File tree

46 files changed

+990
-713
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+990
-713
lines changed

.github/workflows/release-contracts.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ permissions:
66
on:
77
push:
88
tags:
9-
- 'v*'
9+
- "v*"
1010
branches:
1111
- main
1212
- ci/release-contracts
1313

1414
jobs:
1515
release:
1616
runs-on: ubuntu-latest
17-
container: cosmwasm/workspace-optimizer:0.14.0
17+
container: cosmwasm/optimizer:0.16.0
1818
steps:
1919
- uses: actions/checkout@v3
2020

@@ -29,12 +29,12 @@ jobs:
2929
~/.cargo/registry/index/
3030
~/.cargo/registry/cache/
3131
~/.cargo/git/db/
32-
target/
32+
target/
3333
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
3434

3535
- name: Compile contracts
3636
timeout-minutes: 30
37-
run: optimize_workspace.sh .
37+
run: optimize.sh .
3838

3939
- name: Upload contracts
4040
uses: actions/upload-artifact@v3

Cargo.lock

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

ci/bootstrap-env/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,8 @@ fn main() -> Result<()> {
102102
}),
103103
submission_policy: PreProposeSubmissionPolicy::Specific {
104104
dao_members: true,
105-
allowlist: None,
106-
denylist: None,
105+
allowlist: vec![],
106+
denylist: vec![],
107107
},
108108
extension: Empty::default(),
109109
})

ci/integration-tests/src/helpers/helper.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@ pub fn create_dao(
8686
}),
8787
submission_policy: PreProposeSubmissionPolicy::Specific {
8888
dao_members: true,
89-
allowlist: None,
90-
denylist: None,
89+
allowlist: vec![],
90+
denylist: vec![],
9191
},
9292
extension: Empty::default(),
9393
})

contracts/distribution/dao-rewards-distributor/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ cw-utils = { workspace = true }
3434
dao-hooks = { workspace = true }
3535
dao-interface = { workspace = true }
3636
dao-voting = { workspace = true }
37+
semver = { workspace = true }
3738
thiserror = { workspace = true }
3839
cw-orch.workspace = true
3940

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -847,8 +847,8 @@
847847
"migrate": {
848848
"$schema": "http://json-schema.org/draft-07/schema#",
849849
"title": "MigrateMsg",
850-
"type": "string",
851-
"enum": []
850+
"type": "object",
851+
"additionalProperties": false
852852
},
853853
"sudo": null,
854854
"responses": {

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

Lines changed: 123 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
use cosmwasm_std::entry_point;
33
use cosmwasm_std::{
44
ensure, from_json, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response,
5-
StdResult, Uint128, Uint256,
5+
StdError, StdResult, Uint128, Uint256,
66
};
77
use cw2::{get_contract_version, set_contract_version};
88
use cw20::{Cw20ReceiveMsg, Denom};
99
use cw_storage_plus::Bound;
1010
use cw_utils::{must_pay, nonpayable, Duration, Expiration};
1111
use dao_interface::voting::InfoResponse;
12+
use semver::Version;
1213

1314
use std::ops::Add;
1415

@@ -27,8 +28,8 @@ use crate::rewards::{
2728
use crate::state::{DistributionState, EmissionRate, Epoch, COUNT, DISTRIBUTIONS, USER_REWARDS};
2829
use crate::ContractError;
2930

30-
const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME");
31-
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
31+
pub(crate) const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME");
32+
pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
3233

3334
pub const DEFAULT_LIMIT: u32 = 10;
3435
pub const MAX_LIMIT: u32 = 50;
@@ -279,36 +280,106 @@ fn execute_fund_native(
279280
}
280281

281282
fn execute_fund(
283+
deps: DepsMut,
284+
env: Env,
285+
distribution: DistributionState,
286+
amount: Uint128,
287+
) -> Result<Response, ContractError> {
288+
match distribution.active_epoch.emission_rate {
289+
EmissionRate::Paused {} => execute_fund_paused(deps, distribution, amount),
290+
EmissionRate::Immediate {} => execute_fund_immediate(deps, env, distribution, amount),
291+
EmissionRate::Linear { .. } => execute_fund_linear(deps, env, distribution, amount),
292+
}
293+
}
294+
295+
/// funding a paused distribution simply increases the funded amount.
296+
fn execute_fund_paused(
297+
deps: DepsMut,
298+
mut distribution: DistributionState,
299+
amount: Uint128,
300+
) -> Result<Response, ContractError> {
301+
distribution.funded_amount += amount;
302+
303+
DISTRIBUTIONS.save(deps.storage, distribution.id, &distribution)?;
304+
305+
Ok(Response::new()
306+
.add_attribute("action", "fund")
307+
.add_attribute("id", distribution.id.to_string())
308+
.add_attribute("denom", distribution.get_denom_string())
309+
.add_attribute("amount_funded", amount))
310+
}
311+
312+
/// funding an immediate distribution instantly distributes the new amount.
313+
fn execute_fund_immediate(
314+
deps: DepsMut,
315+
env: Env,
316+
mut distribution: DistributionState,
317+
amount: Uint128,
318+
) -> Result<Response, ContractError> {
319+
distribution.funded_amount += amount;
320+
321+
// for immediate distribution, update total_earned_puvp instantly since we
322+
// need to know the change in funded_amount to calculate the new
323+
// total_earned_puvp.
324+
distribution.update_immediate_emission_total_earned_puvp(deps.as_ref(), &env.block, amount)?;
325+
326+
DISTRIBUTIONS.save(deps.storage, distribution.id, &distribution)?;
327+
328+
Ok(Response::new()
329+
.add_attribute("action", "fund")
330+
.add_attribute("id", distribution.id.to_string())
331+
.add_attribute("denom", distribution.get_denom_string())
332+
.add_attribute("amount_funded", amount))
333+
}
334+
335+
/// funding a linear distribution requires some complex logic based on whether
336+
/// or not the distribution is continuous and whether or not it's expired.
337+
///
338+
/// expired continuous distributions experience backfill with the new funds,
339+
/// whereas expired discontinuous distributions begin anew (and all past rewards
340+
/// must be taken into account before restarting distribution).
341+
fn execute_fund_linear(
282342
deps: DepsMut,
283343
env: Env,
284344
mut distribution: DistributionState,
285345
amount: Uint128,
286346
) -> Result<Response, ContractError> {
287-
// will only be true if emission rate is linear and continuous is true
288347
let continuous =
289348
if let EmissionRate::Linear { continuous, .. } = distribution.active_epoch.emission_rate {
290349
continuous
291350
} else {
292351
false
293352
};
353+
let previously_funded = !distribution.funded_amount.is_zero();
354+
let was_expired = distribution.active_epoch.ends_at.is_expired(&env.block);
355+
let discontinuous_expired = !continuous && was_expired;
294356

295357
// restart the distribution from the current block if it hasn't yet started
296-
// (i.e. never been funded), or if it's expired (i.e. all funds have been
297-
// distributed) and not continuous. if it is continuous, treat it as if it
298-
// weren't expired by simply adding the new funds and recomputing the end
299-
// date, keeping start date the same, effectively backfilling rewards.
300-
let restart_distribution = if distribution.funded_amount.is_zero() {
301-
true
302-
} else {
303-
!continuous && distribution.active_epoch.ends_at.is_expired(&env.block)
304-
};
358+
// (i.e. never been funded) OR if it's both discontinuous and expired (i.e.
359+
// all past funds should have been distributed and we're effectively
360+
// beginning a new distribution with new funds). this ensures the new funds
361+
// start being distributed from now instead of from the past.
362+
//
363+
// if already funded and continuous or not expired (else block), just add
364+
// the new funds and leave start date the same, backfilling rewards.
365+
if !previously_funded || discontinuous_expired {
366+
// if funding an expired discontinuous distribution that's previously
367+
// been funded, ensure all rewards are taken into account before
368+
// restarting, in case users haven't claimed yet, by adding the final
369+
// total rewards earned to the historical value.
370+
if discontinuous_expired && previously_funded {
371+
let final_total_earned_puvp =
372+
get_active_total_earned_puvp(deps.as_ref(), &env.block, &distribution)?;
373+
distribution.historical_earned_puvp = distribution
374+
.historical_earned_puvp
375+
.checked_add(final_total_earned_puvp)
376+
.map_err(|err| ContractError::DistributionHistoryTooLarge {
377+
err: err.to_string(),
378+
})?;
379+
// last updated block is reset to the new start block below
380+
}
305381

306-
// if necessary, restart the distribution from the current block so that the
307-
// new funds start being distributed from now instead of from the past, and
308-
// reset funded_amount to the new amount since we're effectively starting a
309-
// new distribution. otherwise, just add the new amount to the existing
310-
// funded_amount
311-
if restart_distribution {
382+
// reset all starting fields since a new distribution is starting
312383
distribution.funded_amount = amount;
313384
distribution.active_epoch.started_at = match distribution.active_epoch.emission_rate {
314385
EmissionRate::Paused {} => Expiration::Never {},
@@ -318,10 +389,14 @@ fn execute_fund(
318389
Duration::Time(_) => Expiration::AtTime(env.block.time),
319390
},
320391
};
392+
distribution.active_epoch.total_earned_puvp = Uint256::zero();
393+
distribution.active_epoch.last_updated_total_earned_puvp =
394+
distribution.active_epoch.started_at;
321395
} else {
322396
distribution.funded_amount += amount;
323397
}
324398

399+
// update the end block based on the new funds and potentially updated start
325400
let new_funded_duration = distribution
326401
.active_epoch
327402
.emission_rate
@@ -331,26 +406,16 @@ fn execute_fund(
331406
None => Expiration::Never {},
332407
};
333408

334-
// if immediate distribution, update total_earned_puvp instantly since we
335-
// need to know the delta in funding_amount to calculate the new
336-
// total_earned_puvp.
337-
if (distribution.active_epoch.emission_rate == EmissionRate::Immediate {}) {
338-
distribution.update_immediate_emission_total_earned_puvp(
339-
deps.as_ref(),
340-
&env.block,
341-
amount,
342-
)?;
343-
344-
// if continuous, meaning rewards should have been distributed in the past
345-
// but were not due to lack of sufficient funding, ensure the total rewards
346-
// earned puvp is up to date.
347-
} else if !restart_distribution && continuous {
409+
// if continuous, funds existed in the past, and the distribution was
410+
// expired, some rewards may not have been distributed due to lack of
411+
// sufficient funding. ensure the total rewards earned puvp is up to date
412+
// based on the original start block and the newly updated end block.
413+
if continuous && previously_funded && was_expired {
348414
distribution.active_epoch.total_earned_puvp =
349415
get_active_total_earned_puvp(deps.as_ref(), &env.block, &distribution)?;
416+
distribution.active_epoch.bump_last_updated(&env.block);
350417
}
351418

352-
distribution.active_epoch.bump_last_updated(&env.block);
353-
354419
DISTRIBUTIONS.save(deps.storage, distribution.id, &distribution)?;
355420

356421
Ok(Response::new()
@@ -542,7 +607,8 @@ fn query_pending_rewards(
542607
for (id, distribution) in distributions {
543608
// first we get the active epoch earned puvp value
544609
let active_total_earned_puvp =
545-
get_active_total_earned_puvp(deps, &env.block, &distribution)?;
610+
get_active_total_earned_puvp(deps, &env.block, &distribution)
611+
.map_err(|e| StdError::generic_err(e.to_string()))?;
546612

547613
// then we add that to the historical rewards earned puvp
548614
let total_earned_puvp =
@@ -592,6 +658,27 @@ fn query_distributions(
592658

593659
#[cfg_attr(not(feature = "library"), entry_point)]
594660
pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
661+
let contract_version = get_contract_version(deps.storage)?;
662+
663+
if contract_version.contract != CONTRACT_NAME {
664+
return Err(ContractError::MigrationErrorIncorrectContract {
665+
expected: CONTRACT_NAME.to_string(),
666+
actual: contract_version.contract,
667+
});
668+
}
669+
670+
let new_version: Version = CONTRACT_VERSION.parse()?;
671+
let current_version: Version = contract_version.version.parse()?;
672+
673+
// only allow upgrades
674+
if new_version <= current_version {
675+
return Err(ContractError::MigrationErrorInvalidVersion {
676+
new: new_version.to_string(),
677+
current: current_version.to_string(),
678+
});
679+
}
680+
595681
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
682+
596683
Ok(Response::default())
597684
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ pub enum ContractError {
2222
#[error(transparent)]
2323
Payment(#[from] PaymentError),
2424

25+
#[error("semver parsing error: {0}")]
26+
SemVer(String),
27+
2528
#[error("Invalid CW20")]
2629
InvalidCw20 {},
2730

@@ -54,4 +57,16 @@ pub enum ContractError {
5457

5558
#[error("Cannot update emission rate because this distribution has accumulated the maximum rewards. Start a new distribution with the new emission rate instead. (Overflow: {err})")]
5659
DistributionHistoryTooLarge { err: String },
60+
61+
#[error("Invalid version migration. {new} is not newer than {current}.")]
62+
MigrationErrorInvalidVersion { new: String, current: String },
63+
64+
#[error("Expected to migrate from contract {expected}. Got {actual}.")]
65+
MigrationErrorIncorrectContract { expected: String, actual: String },
66+
}
67+
68+
impl From<semver::Error> for ContractError {
69+
fn from(err: semver::Error) -> Self {
70+
Self::SemVer(err.to_string())
71+
}
5772
}

0 commit comments

Comments
 (0)