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

Updates and improvements to natspec and README #120

Merged
merged 1 commit into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright (c) 2024 Tally
Copyright (c) 2025 Tally
info@tally.xyz

GNU AFFERO GENERAL PUBLIC LICENSE
Expand Down
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# Staker

Staker is a flexible, configurable staking contract. Staker makes it easy to distribute onchain staking rewards for any ERC20 token.
Staker is a flexible, configurable staking contract. Staker makes it easy to distribute onchain staking rewards for any ERC20 token, including DAO governance tokens.

## How it works:

### 1. Deploy and configure a Staker
- Staker is deployed with a single staking token
- Staker is deployed with a single staking token.
- Staker is deployed with an admin, such as a DAO.
- Staker is configured to distribute one or more reward tokens
- Staker is configured to collect and distribute reward tokens.

### 2. Tokenholders stake
- Tokenholders of the staking token can deposit those tokens in Staker.
- There is no delay to deposit or withdraw.
- If the staking token is a governance token, depositors can delegate their staked tokens' voting power to themselves or someone else
- If the staking token is a governance token, depositors can delegate their staked tokens' voting power to themselves or someone else.
- The depositor sets a claimer who can claim the staking rewards, such as themselves or someone else.

### 3. Staker distributes rewards
Expand All @@ -28,17 +28,17 @@ When Staker is used for a protocol or DAO, the rewards are generally funded by p

Staker can be deployed as an immutable contract with minimal governance. It does have some admin functions:

- Adding a new source of rewards
- Changing the eligibility oracle or the emergency pause guardian
- Overriding eligibility for a particular address
- Adding a new source of rewards.
- Changing the eligibility oracle or the emergency pause guardian.
- Overriding eligibility for a particular address.

The staking token can be an `ERC20` token, including `ERC20Votes` governance tokens. Staker splits up all voting power in Staker by creating a surrogate contract for each delegate.

Staker distributes rewards over a fixed period of time. That gives everyone a chance to stake and minimizes discontinuities from flash staking.
Staker distributes rewards over a fixed period of time. This minimizes discontinuities from flash staking, and prevents frontrunning attacks, aimed at gaining a disproportionate share of rewards, ahead of reward distributions.

### Staking system

The staking system accepts user stake, delegates their voting power, and distributes rewards for eligibile stakers.
The staking system accepts user stake, delegates their voting power, and distributes rewards for eligible stakers.

```mermaid

Expand Down Expand Up @@ -83,7 +83,7 @@ stateDiagram-v2

### Earning Power Calculator

The earning power calculator determines which stakers are eligible for a reward. This implementation uses an oracle. An oracle is needed because eligibility depends on off-chain behavior.
The earning power calculator determines which depositors are eligible for rewards—and the rate at which those rewards are earned—based on their stake and their governance delegatee. The calculator is a modular component of the staker, which can be customized and updated by owner of the Staker, such as a DAO. One provided implementation uses an oracle. An oracle is needed because eligibility depends on the off-chain behavior of DAO delegates.

```mermaid
stateDiagram-v2
Expand Down Expand Up @@ -165,4 +165,4 @@ This command will use the names of the contract's unit tests to generate a human

The code in this repository is licensed under the [GNU Affero General Public License](LICENSE) unless otherwise indicated.

Copyright (C) 2024 Tally
Copyright (C) 2025 Tally
45 changes: 29 additions & 16 deletions src/Staker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,18 @@ import {SafeCast} from "openzeppelin/utils/math/SafeCast.sol";
/// received, the reward duration restarts, and the rate at which rewards are streamed is updated
/// to include the newly received rewards along with any remaining rewards that have finished
/// streaming since the last time a reward was received.
///
/// The rate at which a depositor earns rewards is proportional to their earning power. Earning
/// power is based on the amount the depositor has staked and the activity of their delegatee.
/// The calculation of earning power is handled by a separate module called the earning power
/// calculator. This module is set by the owner, and can be updated by the owner. If the owner of
/// the Staker contract is a DAO, which is the expected common case, this means the DAO has
/// the ability to define and iterate on its own definition of active, aligned participation,
/// and to decide how to reward it.
abstract contract Staker is INotifiableRewardReceiver, Multicall {
using SafeCast for uint256;

/// @notice A unique identifier assigned to each deposit.
type DepositIdentifier is uint256;

/// @notice Emitted when stake is deposited by a depositor, either to a new deposit or one that
Expand Down Expand Up @@ -119,8 +128,8 @@ abstract contract Staker is INotifiableRewardReceiver, Multicall {
/// duration.
error Staker__InsufficientRewardBalance();

/// @notice Thrown if the unclaimed rewards are insufficient to cover a bumpers requested tip or
/// in the case of an earning power decrease the tip of a subsequent earning power increase.
/// @notice Thrown if the unclaimed rewards are insufficient to cover a bumper's requested tip,
/// or in the case of an earning power decrease the tip of a subsequent earning power increase.
error Staker__InsufficientUnclaimedRewards();

/// @notice Thrown if a caller attempts to specify address zero for certain designated addresses.
Expand All @@ -139,6 +148,7 @@ abstract contract Staker is INotifiableRewardReceiver, Multicall {
error Staker__InvalidSignature();

/// @notice Thrown if an earning power update is unqualified to be bumped.
/// @param score The would-be new earning power which did not qualify.
error Staker__Unqualified(uint256 score);

/// @notice Metadata associated with a discrete staking deposit.
Expand Down Expand Up @@ -171,7 +181,7 @@ abstract contract Staker is INotifiableRewardReceiver, Multicall {

/// @notice Parameters associated with the fee assessed when rewards are claimed.
/// @param feeAmount The absolute amount of the reward token that is taken as a fee when rewards
/// claimed for a given deposit.
/// are claimed for a given deposit.
/// @param feeCollector The address to which reward token fees are sent.
struct ClaimFeeParameters {
uint96 feeAmount;
Expand Down Expand Up @@ -199,7 +209,8 @@ abstract contract Staker is INotifiableRewardReceiver, Multicall {
/// @dev Unique identifier that will be used for the next deposit.
DepositIdentifier private nextDepositId;

/// @notice Permissioned actor that can enable/disable `rewardNotifier` addresses.
/// @notice Permissioned actor that can enable/disable `rewardNotifier` addresses, set the max
/// bump tip, set the claim fee parameters, and update the earning power calculator.
address public admin;

/// @notice Maximum tip a bumper can request.
Expand All @@ -212,7 +223,7 @@ abstract contract Staker is INotifiableRewardReceiver, Multicall {
uint256 public totalEarningPower;

/// @notice Contract that determines a deposit's earning power based on their delegatee.
/// @dev An earning power calculator should take into account that a deposit's earning power is an
/// @dev An earning power calculator should take into account that a deposit's earning power is a
/// uint96. There may be overflow issues within governance staker if this is not taken into
/// account. Also, there should be some mechanism to prevent the deposit from frequently being
/// bumpable: if earning power changes frequently, this will eat into a users unclaimed rewards.
Expand Down Expand Up @@ -250,7 +261,8 @@ abstract contract Staker is INotifiableRewardReceiver, Multicall {
/// @param _stakeToken Delegable governance token which users will stake to earn rewards.
/// @param _earningPowerCalculator The contract that will serve as the initial calculator of
/// earning power for the staker system.
/// @param _admin Address which will have permission to manage rewardNotifiers.
/// @param _admin Address which will have permission to manage reward notifiers, claim fee
/// parameters, the max bump tip, and the reward calculator.
constructor(
IERC20 _rewardToken,
IERC20 _stakeToken,
Expand Down Expand Up @@ -305,12 +317,12 @@ abstract contract Staker is INotifiableRewardReceiver, Multicall {
_setClaimFeeParameters(_params);
}

/// @notice A method to get a delegation surrogate contract for a given delegate.
/// @param _delegatee The address the delegation surrogate is delegating voting power.
/// @notice A method to get the delegation surrogate contract for a given delegate.
/// @param _delegatee The address to which the delegation surrogate is delegating voting power.
/// @return The delegation surrogate.
/// @dev A concrete implementation should return a delegate surrogate address for a given
/// delegatee. In practice this may be as simple as returning an address stored in a mapping or
/// computing it's create2 address.
/// computing its create2 address.
function surrogates(address _delegatee) public view virtual returns (DelegationSurrogate);

/// @notice Timestamp representing the last time at which rewards have been distributed, which is
Expand Down Expand Up @@ -367,7 +379,7 @@ abstract contract Staker is INotifiableRewardReceiver, Multicall {
/// contract to spend at least the would-be staked amount of the token.
/// @param _amount Quantity of the staking token to stake.
/// @param _delegatee Address to assign the governance voting weight of the staked tokens.
/// @param _claimer Address that will accrue rewards for this stake.
/// @param _claimer Address that will have the right to claim rewards for this stake.
/// @return _depositId Unique identifier for this deposit.
/// @dev Neither the delegatee nor the claimer may be the zero address. The deposit will be
/// owned by the message sender.
Expand Down Expand Up @@ -427,7 +439,7 @@ abstract contract Staker is INotifiableRewardReceiver, Multicall {
}

/// @notice Claim reward tokens earned by a given deposit. Message sender must be the claimer
/// address of the deposit. Tokens are sent to the claimer address.
/// address of the deposit or the owner of the deposit. Tokens are sent to the caller.
/// @param _depositId Identifier of the deposit from which accrued rewards will be claimed.
/// @return Amount of reward tokens claimed, after the fee has been assessed.
function claimReward(DepositIdentifier _depositId) external virtual returns (uint256) {
Expand Down Expand Up @@ -825,6 +837,8 @@ abstract contract Staker is INotifiableRewardReceiver, Multicall {
maxBumpTip = _newMaxTip;
}

/// @notice Internal helper method which sets the claim fee parameters.
/// @param _params The new fee parameters.
function _setClaimFeeParameters(ClaimFeeParameters memory _params) internal virtual {
if (
_params.feeAmount > MAX_CLAIM_FEE
Expand All @@ -848,12 +862,11 @@ abstract contract Staker is INotifiableRewardReceiver, Multicall {
}

/// @notice Internal helper method which reverts Staker__Unauthorized if the alleged
/// owner is
/// not the true owner of the deposit.
/// owner is not the true owner of the deposit.
/// @param deposit Deposit to validate.
/// @param owner Alleged owner of deposit.
function _revertIfNotDepositOwner(Deposit storage deposit, address owner) internal view virtual {
if (owner != deposit.owner) revert Staker__Unauthorized("not owner", owner);
/// @param _owner Alleged owner of deposit.
function _revertIfNotDepositOwner(Deposit storage deposit, address _owner) internal view virtual {
if (_owner != deposit.owner) revert Staker__Unauthorized("not owner", _owner);
}

/// @notice Internal helper method which reverts with Staker__InvalidAddress if the
Expand Down
24 changes: 11 additions & 13 deletions src/calculators/BinaryEligibilityOracleEarningPowerCalculator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ import {IEarningPowerCalculator} from "src/interfaces/IEarningPowerCalculator.so
/// @title BinaryEligibilityOracleEarningPowerCalculator
/// @author [ScopeLift](https://scopelift.co)
/// @notice This contract calculates the earning power of a staker based on their delegatee's score.
/// The score is provided by a permissioned oracle, that can be updated by the contract owner. The
/// oracle can also be paused by a permissioned pauser role. The contract ensures the oracle
/// remains fresh. The contract allows the owner to set a threshold score over which staker's
/// receive earning power equal to the amount they've staked, and below which they receive no
/// earning power at all. The contract enforces a grace period before which a staker's earning
/// power does not qualify for being bumped. The contract also allows the owner to override a
/// score for a given delegatee. Note that, in practice, it is expected that the owner will be the
/// DAO itself.
contract BinaryEligibilityOracleEarningPowerCalculator is Ownable, IEarningPowerCalculator {
/// @notice Emitted when a delegatee's score is updated.
/// @param delegatee The address of the delegatee whose score was updated.
Expand Down Expand Up @@ -99,7 +107,6 @@ contract BinaryEligibilityOracleEarningPowerCalculator is Ownable, IEarningPower
/// @notice Mapping to store the lock status of delegate scores.
mapping(address delegate => bool isLocked) public delegateeScoreLockStatus;

/// @notice Initializes the EarningPowerCalculator contract.
/// @param _owner The DAO governor address.
/// @param _scoreOracle The address of the trusted oracle address.
/// @param _delegateeScoreEligibilityThreshold The threshold for delegatee score eligibility to
Expand All @@ -122,11 +129,7 @@ contract BinaryEligibilityOracleEarningPowerCalculator is Ownable, IEarningPower
lastOracleUpdateTime = block.timestamp;
}

/// @notice Calculates the earning power for a given delegatee and staking amount.
/// @param _amountStaked The amount of tokens staked.
/// @param /* _staker */ The address of the staker.
/// @param _delegatee The address of the delegatee.
/// @return The calculated earning power.
/// @inheritdoc IEarningPowerCalculator
function getEarningPower(uint256 _amountStaked, address, /* _staker */ address _delegatee)
external
view
Expand All @@ -136,13 +139,7 @@ contract BinaryEligibilityOracleEarningPowerCalculator is Ownable, IEarningPower
return _isDelegateeEligible(_delegatee) ? _amountStaked : 0;
}

/// @notice Calculates the new earning power and determines if it qualifies for an update.`
/// @param _amountStaked The amount of tokens staked.
/// @param /* _staker */ The address of the staker.
/// @param _delegatee The address of the delegatee.
/// @param /* _oldEarningPower */ The previous earning power value.
/// @return The newly calculated earning power.
/// @return Boolean indicating if the new earning power qualifies for an update.
/// @inheritdoc IEarningPowerCalculator
function getNewEarningPower(
uint256 _amountStaked,
address, /* _staker */
Expand All @@ -163,6 +160,7 @@ contract BinaryEligibilityOracleEarningPowerCalculator is Ownable, IEarningPower
/// @notice Updates the eligibility score of a delegatee.
/// @dev This function can only be called by the authorized `scoreOracle` address.
/// @dev If the delegatee's score is locked, the update will be reverted.
/// @dev If the oracle is paused, the update will be reverted.
/// @param _delegatee The address of the delegatee whose score is being updated.
/// @param _newScore The new score to be assigned to the delegatee.
function updateDelegateeScore(address _delegatee, uint256 _newScore) public {
Expand Down
4 changes: 3 additions & 1 deletion src/extensions/StakerDelegateSurrogateVotes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ abstract contract StakerDelegateSurrogateVotes is Staker {
/// the staked tokens from deposits which assign voting weight to said delegate.
mapping(address delegatee => DelegationSurrogate surrogate) private storedSurrogates;

/// @notice Thrown if an inheritor uses a seperate staking token.
/// @notice Thrown if an inheritor misconfigures the staking token on deployment.
error StakerDelegateSurrogateVotes__UnauthorizedToken();

/// @param _votingToken The token that is used for voting, which must be the same as the parent
/// Staker's STAKE_TOKEN.
constructor(IERC20Delegates _votingToken) {
if (address(STAKE_TOKEN) != address(_votingToken)) {
revert StakerDelegateSurrogateVotes__UnauthorizedToken();
Expand Down
Loading
Loading