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

Rename contracts to simply use the name Staker #94

Merged
merged 2 commits into from
Jan 15, 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
46 changes: 29 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,44 @@
***DISCLAIMER: This codebase is not yet audited***
# Staker

# Governance Staking

Governance Staking rewards a DAO's tokenholders for participating in governance. This staking system distributes rewards to tokenholders whose tokens are active in governance. Rewards generally come from the DAO, funded by protocol revenue and/or issuance of the native token from the treasury.
Staker is a flexible, configurable staking contract. Staker makes it easy to distribute onchain staking rewards for any ERC20 token.

## How it works:

- The DAO decides how stakers can be eligible for rewards. The DAO sets up an oracle to put eligiblity scores onchain.
- Tokenholders stake their tokens. There is no delay to stake or unstake. Stakers set a claimer for their reward, such as themselves.
- The DAO sends rewards into its Governance Staking.
- Governance Staking distributes the rewards over time. Each staker's reward is proportional to their staked balance over time.
### 1. Deploy and configurate a Staker
- 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

### 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
- The depositor sets a claimer who can claim the staking rewards, such as themselves or someone else.

### 3. Staker distributes rewards
- The admin sends rewards into Staker.
- Optionally, the admin sets eligibility criteria for rewards.
- Staker distributes those rewards over time.
- Each tokenholder's reward is proportional to their staked balance over time.
- Claimers can claim their accrued rewards at any time.

When Staker is used for a protocol or DAO, the rewards are generally funded by protocol revenue and/or issuance of the native token from the treasury.

## Implementation details:

Governance Staking can be deployed as an immutable contract with minimal governance. It does have some admin functions:
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

Staking is compatible with existing `ERC20Votes` governance tokens. It splits voting power by creating a surrogate contract for each delegate.
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.

Governance Staking 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. That gives everyone a chance to stake and minimizes discontinuities from flash staking.

### Governance Staking system
### Staking system

The governance 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 eligibile stakers.

```mermaid
Expand All @@ -35,7 +47,7 @@ stateDiagram-v2
User --> CUF: Stakes tokens
state GovernanceStaker {
state Staker {
state "Key User Functions" as CUF {
stake --> claimReward
claimReward --> withdraw
Expand Down Expand Up @@ -63,8 +75,8 @@ stateDiagram-v2
DelegationSurrogate --> Delegatee: Delegates voting power
Admin --> CAF: e.g. governance
RewardNotifier --> GovernanceStaker: Tells Staker about new rewards
EarningPowerCalculator --> GovernanceStaker: Calculates eligibility
RewardNotifier --> Staker: Tells Staker about new rewards
EarningPowerCalculator --> Staker: Calculates eligibility
```
Expand Down Expand Up @@ -102,7 +114,7 @@ stateDiagram-v2
ScoreOracle --> SOF: Updates scores
Owner --> OF: Admin controls
Guardian --> GF: Emergency pause
PF --> GovernanceStaker: Returns earning power to staking system
PF --> Staker: Returns earning power to staking system
```

## Development
Expand Down
11 changes: 5 additions & 6 deletions script/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";
import {Script} from "forge-std/Script.sol";

import {DeployInput} from "script/DeployInput.sol";
import {GovernanceStakerHarness} from "test/harnesses/GovernanceStakerHarness.sol";
import {StakerHarness} from "test/harnesses/StakerHarness.sol";
import {IERC20Staking} from "src/interfaces/IERC20Staking.sol";
import {INotifiableRewardReceiver} from "src/interfaces/INotifiableRewardReceiver.sol";
import {IEarningPowerCalculator} from "src/interfaces/IEarningPowerCalculator.sol";
Expand All @@ -22,20 +22,19 @@ contract Deploy is Script, DeployInput {
);
}

function run() public returns (GovernanceStakerHarness) {
function run() public returns (StakerHarness) {
vm.startBroadcast(deployerPrivateKey);
// Deploy the staking contract
// TODO: Replace with the `ArbitrumStaker` contract once it is developed
GovernanceStakerHarness govStaker = new GovernanceStakerHarness(
StakerHarness govStaker = new StakerHarness(
IERC20(PAYOUT_TOKEN_ADDRESS),
IERC20Staking(STAKE_TOKEN_ADDRESS),
IEarningPowerCalculator(address(0)),
MAX_BUMP_TIP,
vm.addr(deployerPrivateKey),
"GovernanceStakerHarness"
"StakerHarness"
);

// Change GovernanceStaker admin from `msg.sender` to the Governor timelock
// Change Staker admin from `msg.sender` to the Governor timelock
govStaker.setAdmin(GOVERNOR_TIMELOCK);
vm.stopBroadcast();

Expand Down
58 changes: 28 additions & 30 deletions src/GovernanceStaker.sol → src/Staker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {Nonces} from "openzeppelin/utils/Nonces.sol";
import {SignatureChecker} from "openzeppelin/utils/cryptography/SignatureChecker.sol";
import {EIP712} from "openzeppelin/utils/cryptography/EIP712.sol";

/// @title GovernanceStaker
/// @title Staker
/// @author [ScopeLift](https://scopelift.co)
/// @notice This contract manages the distribution of rewards to stakers. Rewards are denominated
/// in an ERC20 token and sent to the contract by authorized reward notifiers. To stake means to
Expand All @@ -29,7 +29,7 @@ import {EIP712} from "openzeppelin/utils/cryptography/EIP712.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.
abstract contract GovernanceStaker is INotifiableRewardReceiver, Multicall {
abstract contract Staker is INotifiableRewardReceiver, Multicall {
using SafeCast for uint256;

type DepositIdentifier is uint256;
Expand Down Expand Up @@ -83,37 +83,37 @@ abstract contract GovernanceStaker is INotifiableRewardReceiver, Multicall {
/// @notice Thrown when an account attempts a call for which it lacks appropriate permission.
/// @param reason Human readable code explaining why the call is unauthorized.
/// @param caller The address that attempted the unauthorized call.
error GovernanceStaker__Unauthorized(bytes32 reason, address caller);
error Staker__Unauthorized(bytes32 reason, address caller);

/// @notice Thrown if the new rate after a reward notification would be zero.
error GovernanceStaker__InvalidRewardRate();
error Staker__InvalidRewardRate();

/// @notice Thrown if the following invariant is broken after a new reward: the contract should
/// always have a reward balance sufficient to distribute at the reward rate across the reward
/// duration.
error GovernanceStaker__InsufficientRewardBalance();
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.
error GovernanceStaker__InsufficientUnclaimedRewards();
error Staker__InsufficientUnclaimedRewards();

/// @notice Thrown if a caller attempts to specify address zero for certain designated addresses.
error GovernanceStaker__InvalidAddress();
error Staker__InvalidAddress();

/// @notice Thrown if a bumper's requested tip is invalid.
error GovernanceStaker__InvalidTip();
error Staker__InvalidTip();

/// @notice Thrown if the claim fee parameters are outside permitted bounds.
error GovernanceStaker__InvalidClaimFeeParameters();
error Staker__InvalidClaimFeeParameters();

/// @notice Thrown when an onBehalf method is called with a deadline that has expired.
error GovernanceStaker__ExpiredDeadline();
error Staker__ExpiredDeadline();

/// @notice Thrown if a caller supplies an invalid signature to a method that requires one.
error GovernanceStaker__InvalidSignature();
error Staker__InvalidSignature();

/// @notice Thrown if an earning power update is unqualified to be bumped.
error GovernanceStaker__Unqualified(uint256 score);
error Staker__Unqualified(uint256 score);

/// @notice Metadata associated with a discrete staking deposit.
/// @param balance The deposit's staked balance.
Expand Down Expand Up @@ -167,7 +167,7 @@ abstract contract GovernanceStaker is INotifiableRewardReceiver, Multicall {

/// @notice The maximum value to which the claim fee can be set.
/// @dev For anything other than a zero value, this immutable parameter should be set in the
/// constructor of a concrete implementation inheriting from GovernanceStaker.
/// constructor of a concrete implementation inheriting from Staker.
uint256 public immutable MAX_CLAIM_FEE;

/// @dev Unique identifier that will be used for the next deposit.
Expand Down Expand Up @@ -407,7 +407,7 @@ abstract contract GovernanceStaker is INotifiableRewardReceiver, Multicall {
function claimReward(DepositIdentifier _depositId) external virtual returns (uint256) {
Deposit storage deposit = deposits[_depositId];
if (deposit.claimer != msg.sender && deposit.owner != msg.sender) {
revert GovernanceStaker__Unauthorized("not claimer or owner", msg.sender);
revert Staker__Unauthorized("not claimer or owner", msg.sender);
}
return _claimReward(_depositId, deposit, msg.sender);
}
Expand All @@ -428,9 +428,7 @@ abstract contract GovernanceStaker is INotifiableRewardReceiver, Multicall {
/// required that a notifier contract always transfers the `_amount` to this contract before
/// calling this method.
function notifyRewardAmount(uint256 _amount) external virtual {
if (!isRewardNotifier[msg.sender]) {
revert GovernanceStaker__Unauthorized("not notifier", msg.sender);
}
if (!isRewardNotifier[msg.sender]) revert Staker__Unauthorized("not notifier", msg.sender);

// We checkpoint the accumulator without updating the timestamp at which it was updated,
// because that second operation will be done after updating the reward rate.
Expand All @@ -446,7 +444,7 @@ abstract contract GovernanceStaker is INotifiableRewardReceiver, Multicall {
rewardEndTime = block.timestamp + REWARD_DURATION;
lastCheckpointTime = block.timestamp;

if ((scaledRewardRate / SCALE_FACTOR) == 0) revert GovernanceStaker__InvalidRewardRate();
if ((scaledRewardRate / SCALE_FACTOR) == 0) revert Staker__InvalidRewardRate();

// This check cannot _guarantee_ sufficient rewards have been transferred to the contract,
// because it cannot isolate the unclaimed rewards owed to stakers left in the balance. While
Expand All @@ -455,7 +453,7 @@ abstract contract GovernanceStaker is INotifiableRewardReceiver, Multicall {
// admin.
if (
(scaledRewardRate * REWARD_DURATION) > (REWARD_TOKEN.balanceOf(address(this)) * SCALE_FACTOR)
) revert GovernanceStaker__InsufficientRewardBalance();
) revert Staker__InsufficientRewardBalance();

emit RewardNotified(_amount, msg.sender);
}
Expand All @@ -473,7 +471,7 @@ abstract contract GovernanceStaker is INotifiableRewardReceiver, Multicall {
address _tipReceiver,
uint256 _requestedTip
) external virtual {
if (_requestedTip > maxBumpTip) revert GovernanceStaker__InvalidTip();
if (_requestedTip > maxBumpTip) revert Staker__InvalidTip();

Deposit storage deposit = deposits[_depositId];

Expand All @@ -486,17 +484,17 @@ abstract contract GovernanceStaker is INotifiableRewardReceiver, Multicall {
deposit.balance, deposit.owner, deposit.delegatee, deposit.earningPower
);
if (!_isQualifiedForBump || _newEarningPower == deposit.earningPower) {
revert GovernanceStaker__Unqualified(_newEarningPower);
revert Staker__Unqualified(_newEarningPower);
}

if (_newEarningPower > deposit.earningPower && _unclaimedRewards < _requestedTip) {
revert GovernanceStaker__InsufficientUnclaimedRewards();
revert Staker__InsufficientUnclaimedRewards();
}

// Note: underflow causes a revert if the requested tip is more than unclaimed rewards
if (_newEarningPower < deposit.earningPower && (_unclaimedRewards - _requestedTip) < maxBumpTip)
{
revert GovernanceStaker__InsufficientUnclaimedRewards();
revert Staker__InsufficientUnclaimedRewards();
}

// Update global earning power & deposit earning power based on this bump
Expand Down Expand Up @@ -800,7 +798,7 @@ abstract contract GovernanceStaker is INotifiableRewardReceiver, Multicall {
if (
_params.feeAmount > MAX_CLAIM_FEE
|| (_params.feeCollector == address(0) && _params.feeAmount > 0)
) revert GovernanceStaker__InvalidClaimFeeParameters();
) revert Staker__InvalidClaimFeeParameters();

emit ClaimFeeParametersSet(
claimFeeParameters.feeAmount,
Expand All @@ -812,25 +810,25 @@ abstract contract GovernanceStaker is INotifiableRewardReceiver, Multicall {
claimFeeParameters = _params;
}

/// @notice Internal helper method which reverts GovernanceStaker__Unauthorized if the message
/// @notice Internal helper method which reverts Staker__Unauthorized if the message
/// sender is not the admin.
function _revertIfNotAdmin() internal view virtual {
if (msg.sender != admin) revert GovernanceStaker__Unauthorized("not admin", msg.sender);
if (msg.sender != admin) revert Staker__Unauthorized("not admin", msg.sender);
}

/// @notice Internal helper method which reverts GovernanceStaker__Unauthorized if the alleged
/// @notice Internal helper method which reverts Staker__Unauthorized if the alleged
/// 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 GovernanceStaker__Unauthorized("not owner", owner);
if (owner != deposit.owner) revert Staker__Unauthorized("not owner", owner);
}

/// @notice Internal helper method which reverts with GovernanceStaker__InvalidAddress if the
/// @notice Internal helper method which reverts with Staker__InvalidAddress if the
/// account in question is address zero.
/// @param _account Account to verify.
function _revertIfAddressZero(address _account) internal pure {
if (_account == address(0)) revert GovernanceStaker__InvalidAddress();
if (_account == address(0)) revert Staker__InvalidAddress();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ pragma solidity ^0.8.23;

import {DelegationSurrogate} from "src/DelegationSurrogate.sol";
import {DelegationSurrogateVotes} from "src/DelegationSurrogateVotes.sol";
import {GovernanceStaker} from "src/GovernanceStaker.sol";
import {Staker} from "src/Staker.sol";
import {IERC20Delegates} from "src/interfaces/IERC20Delegates.sol";

/// @title GovernanceStakerDelegateSurrogateVotes
/// @title StakerDelegateSurrogateVotes
/// @author [ScopeLift](https://scopelift.co)
/// @notice This contract extension adds delegation surrogates to the GovernanceStaker base
/// @notice This contract extension adds delegation surrogates to the Staker base
/// contract, allowing staked tokens to be delegated to a specific delegate.
abstract contract GovernanceStakerDelegateSurrogateVotes is GovernanceStaker {
abstract contract StakerDelegateSurrogateVotes is Staker {
/// @notice Emitted when a surrogate contract is deployed.
event SurrogateDeployed(address indexed delegatee, address indexed surrogate);

Expand All @@ -19,20 +19,20 @@ abstract contract GovernanceStakerDelegateSurrogateVotes is GovernanceStaker {
mapping(address delegatee => DelegationSurrogate surrogate) private storedSurrogates;

/// @notice Thrown if an inheritor uses a seperate staking token.
error GovernanceStakerDelegateSurrogateVotes__UnauthorizedToken();
error StakerDelegateSurrogateVotes__UnauthorizedToken();

constructor(IERC20Delegates _votingToken) {
if (address(STAKE_TOKEN) != address(_votingToken)) {
revert GovernanceStakerDelegateSurrogateVotes__UnauthorizedToken();
revert StakerDelegateSurrogateVotes__UnauthorizedToken();
}
}

/// @inheritdoc GovernanceStaker
/// @inheritdoc Staker
function surrogates(address _delegatee) public view override returns (DelegationSurrogate) {
return storedSurrogates[_delegatee];
}

/// @inheritdoc GovernanceStaker
/// @inheritdoc Staker
function _fetchOrDeploySurrogate(address _delegatee)
internal
virtual
Expand Down
Loading
Loading