Skip to content

Commit

Permalink
update readme; cleanup RewardEmissionRate impl
Browse files Browse the repository at this point in the history
  • Loading branch information
bekauz authored and NoahSaso committed Jul 18, 2024
1 parent e9c7871 commit d741f11
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 31 deletions.
72 changes: 64 additions & 8 deletions contracts/distribution/dao-rewards-distributor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
[![dao-rewards-distributor on crates.io](https://img.shields.io/crates/v/dao-rewards-distributor.svg?logo=rust)](https://crates.io/crates/dao-rewards-distributor)
[![docs.rs](https://img.shields.io/docsrs/dao-rewards-distributor?logo=docsdotrs)](https://docs.rs/dao-rewards-distributor/latest/cw20_stake_external_rewards/)

The `dao-rewards-distributor` works in conjuction with DAO voting modules to provide rewards over time for DAO members. The contract supports both cw20 and native Cosmos SDK tokens. The following voting power modules are supported:
The `dao-rewards-distributor` works in conjuction with DAO voting modules to provide rewards over time for DAO members. The contract supports both cw20 and native Cosmos SDK tokens. The following voting power modules are supported for deriving staking reward allocations:

- `dao-voting-cw4`: for membership or group based DAOs
- `dao-voting-cw20-staked`: for cw20 token based DAOs.
- `dao-voting-cw721-staked`: for NFT based DAOs.
Expand All @@ -13,19 +14,74 @@ NOTE: this contract is NOT AUDITED and is _experimental_. USE AT YOUR OWN RISK.

## Instantiation and Setup

The contract is instantiated with a number of parameters:
- `owner`: The owner of the contract. Is able to fund the contract and update the reward duration.
- `vp_contract`: A DAO DAO voting power module contract address, used to determine membership in the DAO over time.
- `hook_caller`: An optional contract that is allowed to call voting power change hooks. Often, as in `dao-voting-token-staked` and `dao-voting-cw721-staked` the vp_contract calls hooks for power change events, but sometimes they are separate. For example, the `cw4-group` contract is separate from the `dao-voting-cw4` contract and since the `cw4-group` contract fires the membership change events, it's address would be used as the `hook_caller`.
- `reward_denom`: the denomination of the reward token, can be either a cw20 or native token.
- `reward_duration`: the time period over which rewards are to be paid out in blocks.
The contract is instantiated with a very minimal state.
An optional `owner` can be specified. If it is not, the owner is set
to be the address instantiating the contract.

### Hooks setup

After instantiating the contract it is VITAL to setup the required hooks for it to work. This is because to pay out rewards accurately, this contract needs to know about staking or voting power changes in the DAO.

This can be achieved using the `add_hook` method on contracts that support voting power changes, which are:

- `cw4-group`
- `dao-voting-cw721-staked`
- `dao-voting-token-staked`
- `cw20-stake`

Finally, the contract needs to be funded with a token matching the denom specified in the `reward_denom` field during instantiation. This can be achieved by calling the `fund` method on the `dao-rewards-distributor` smart contract, and sending along the appropriate funds.
### Registering a new reward denom

Only the `owner` can register new denoms for distribution.

Registering a denom for distribution expects the following config:

- `denom`, which can either be `Cw20` or `Native`
- `emission_rate`, which determines the `amount` of that denom to be distributed to all applicable addresses per `duration` of time. duration here may be declared in either time (seconds) or blocks. some example configurations may be:
- `1000udenom` per 500 blocks
- `1000udenom` per 24 hours
- `0udenom` per any duration which effectively pauses the rewards
- `vp_contract` address, which will be used to determine the total and relative address voting power for allocating the rewards in a pro-rata basis
- `hook_caller` address, which will be authorized to call back into this contract with any voting power event changes. Example of such events may be:
- user staking tokens
- user unstaking tokens
- user cw-721 state change event
- cw-4 membership change event
- optional `withdraw_destination` address to be used in cases where after shutting down the denom reward distribution unallocated tokens would be sent to. One example use case of this may be some subDAO.

A denom being registered does not mean that any rewards will be distributed. Instead, it enables that to happen by enabling the registered reward denom to be funded.

Currently, a single denom can only have one active distribution configuration.

### Funding the denom to be distributed

Anyone can fund a denom to be distributed as long as that denom
is registered.

If a denom is not registered and someone attempts to fund it, an error will be thrown.

Otherwise, the funded denom state is updated in a few ways.

First, the funded period duration is calculated based on the amount of tokens sent and the configured emission rate. For instance, if 100_000udenom were funded, and the configured emission rate is 1_000udenom per 100 blocks, we derive that there are 100_000/1_000 = 100 epochs funded, each of which contain 100 blocks. We therefore funded 10_000 blocks of rewards.

Then the active epoch end date is re-evaluated, depending on its current value:

- If the active epoch never expires, meaning no rewards are being distributed, we take the funded period duration and add it to the current block.
- If the active epoch expires in the future, then we extend the current deadline with the funded period duration.
- If the active epoch had already expired, then we re-start the rewards distribution by adding the funded period duration to the current block.

### Updating denom reward emission rate

Only the `owner` can update the reward emission rate.

Updating the denom reward emission rate archives the active reward epoch and starts a new one.

First, the currently active epoch is evaluated. We find the amount of tokens that were earned to this point per unit of voting power and save that in the current epoch as its total earned rewards per unit of voting power.
We then bump the last update with that of the current block, and transition into the new epoch.

Active reward epoch is moved into the `historic_epoch_configs`. This is a list of previously active reward emission schedules, along with their finalized amounts.

### Shutting down denom distribution

Only the `owner` can shutdown denom distribution.

Shutdown stops the denom from being distributed, calculates the amount of rewards that was allocated (and may or may not had been claimed yet), and claws that back to the `withdraw_address`.
38 changes: 15 additions & 23 deletions contracts/distribution/dao-rewards-distributor/src/msg.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::collections::HashMap;

use cosmwasm_schema::{cw_serde, QueryResponses};
use cosmwasm_std::{StdError, StdResult, Uint128, Uint256};
use cosmwasm_std::{Decimal, StdError, StdResult, Uint128, Uint64};
use cw20::{Cw20ReceiveMsg, UncheckedDenom};
use cw4::MemberChangedHookMsg;
use cw_ownable::cw_ownable_execute;
Expand Down Expand Up @@ -71,41 +71,33 @@ pub struct RewardEmissionRate {
impl RewardEmissionRate {
// find the duration of the funded period given emission config and funded amount
pub fn get_funded_period_duration(&self, funded_amount: Uint128) -> StdResult<Duration> {
let funded_amount_u256 = Uint256::from(funded_amount);
let amount_u256 = Uint256::from(self.amount);

// if amount being distributed is 0 (rewards are paused), we return the max duration
if amount_u256.is_zero() {
if self.amount.is_zero() {
return match self.duration {
Duration::Height(_) => Ok(Duration::Height(u64::MAX)),
Duration::Time(_) => Ok(Duration::Time(u64::MAX)),
};
}
let amount_to_emission_rate_ratio = funded_amount_u256.checked_div(amount_u256)?;

let ratio_str = amount_to_emission_rate_ratio.to_string();
let ratio = ratio_str
.parse::<u64>()
.map_err(|e| StdError::generic_err(e.to_string()))?;
let amount_to_emission_rate_ratio = Decimal::from_ratio(funded_amount, self.amount);

let funded_period_duration = match self.duration {
let funded_duration = match self.duration {
Duration::Height(h) => {
let duration_height = match ratio.checked_mul(h) {
Some(duration) => duration,
None => return Err(StdError::generic_err("overflow")),
};
Duration::Height(duration_height)
let duration_height = Uint128::from(h)
.checked_mul_floor(amount_to_emission_rate_ratio)
.map_err(|e| StdError::generic_err(e.to_string()))?;
let duration = Uint64::try_from(duration_height)?.u64();
Duration::Height(duration)
}
Duration::Time(t) => {
let duration_time = match ratio.checked_mul(t) {
Some(duration) => duration,
None => return Err(StdError::generic_err("overflow")),
};
Duration::Time(duration_time)
let duration_time = Uint128::from(t)
.checked_mul_floor(amount_to_emission_rate_ratio)
.map_err(|e| StdError::generic_err(e.to_string()))?;
let duration = Uint64::try_from(duration_time)?.u64();
Duration::Time(duration)
}
};

Ok(funded_period_duration)
Ok(funded_duration)
}
}

Expand Down

0 comments on commit d741f11

Please sign in to comment.