Skip to content

Commit

Permalink
Rename files and variables from UniStaker to GovernanceStaker
Browse files Browse the repository at this point in the history
Also makes minor adjustments to CI, natspec, and other minor config changes.
  • Loading branch information
apbendi committed Sep 20, 2024
1 parent c9ec75d commit ca7001f
Show file tree
Hide file tree
Showing 15 changed files with 785 additions and 744 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ jobs:

coverage:
runs-on: ubuntu-latest
env:
FOUNDRY_PROFILE: coverage
steps:
- uses: actions/checkout@v3

Expand Down
File renamed without changes.
File renamed without changes.
10 changes: 5 additions & 5 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@
evm_version = "paris"
optimizer = true
optimizer_runs = 10_000_000
remappings = [
"openzeppelin/=lib/openzeppelin-contracts/contracts",
"uniswap-periphery/=lib/v3-periphery/contracts",
"@uniswap/v3-core=lib/v3-core",
]
remappings = ["openzeppelin/=lib/openzeppelin-contracts/contracts"]
solc_version = "0.8.26"
verbosity = 3

[profile.ci]
fuzz = { runs = 5000 }
invariant = { runs = 1000 }

[profile.coverage]
fuzz = { runs = 100 }
invariant = { runs = 0 }

[profile.lite]
fuzz = { runs = 50 }
invariant = { runs = 10 }
Expand Down
12 changes: 6 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 {UniStaker} from "src/UniStaker.sol";
import {GovernanceStaker} from "src/GovernanceStaker.sol";
import {IERC20Delegates} from "src/interfaces/IERC20Delegates.sol";
import {INotifiableRewardReceiver} from "src/interfaces/INotifiableRewardReceiver.sol";

Expand All @@ -21,19 +21,19 @@ contract Deploy is Script, DeployInput {
);
}

function run() public returns (UniStaker) {
function run() public returns (GovernanceStaker) {
vm.startBroadcast(deployerPrivateKey);
// Deploy the staking contract
UniStaker uniStaker = new UniStaker(
GovernanceStaker govStaker = new GovernanceStaker(
IERC20(PAYOUT_TOKEN_ADDRESS),
IERC20Delegates(STAKE_TOKEN_ADDRESS),
vm.addr(deployerPrivateKey)
);

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

return uniStaker;
return govStaker;
}
}
4 changes: 1 addition & 3 deletions script/DeployInput.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
pragma solidity ^0.8.23;

contract DeployInput {
address constant UNISWAP_GOVERNOR = 0x408ED6354d4973f66138C91495F2f2FCbd8724C3;
address constant UNISWAP_GOVERNOR_TIMELOCK = 0x1a9C8182C09F50C8318d769245beA52c32BE35BC;
address constant UNISWAP_V3_FACTORY_ADDRESS = 0x1F98431c8aD98523631AE4a59f267346ea31F984;
address constant GOVERNOR_TIMELOCK = 0x1a9C8182C09F50C8318d769245beA52c32BE35BC;
address constant PAYOUT_TOKEN_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; // WETH
uint256 constant PAYOUT_AMOUNT = 10e18; // 10 (WETH)
address constant STAKE_TOKEN_ADDRESS = 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984; // UNI
Expand Down
2 changes: 1 addition & 1 deletion src/DelegationSurrogate.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity ^0.8.23;
import {IERC20Delegates} from "src/interfaces/IERC20Delegates.sol";

/// @title DelegationSurrogate
/// @author ScopeLift
/// @author [ScopeLift](https://scopelift.co)
/// @notice A dead-simple contract whose only purpose is to hold governance tokens on behalf of
/// users while delegating voting power to one specific delegatee. This is needed because a single
/// address can only delegate its (full) token weight to a single address at a time. Thus, when a
Expand Down
53 changes: 28 additions & 25 deletions src/UniStaker.sol → src/GovernanceStaker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import {Nonces} from "openzeppelin/utils/Nonces.sol";
import {SignatureChecker} from "openzeppelin/utils/cryptography/SignatureChecker.sol";
import {EIP712} from "openzeppelin/utils/cryptography/EIP712.sol";

/// @title UniStaker
/// @author ScopeLift
/// @title GovernanceStaker
/// @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
/// deposit a designated, delegable ERC20 governance token and leave it over a period of time.
Expand All @@ -28,7 +28,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.
contract UniStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonces {
contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonces {
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 @@ -70,24 +70,24 @@ contract UniStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonces {
/// @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 UniStaker__Unauthorized(bytes32 reason, address caller);
error GovernanceStaker__Unauthorized(bytes32 reason, address caller);

/// @notice Thrown if the new rate after a reward notification would be zero.
error UniStaker__InvalidRewardRate();
error GovernanceStaker__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 UniStaker__InsufficientRewardBalance();
error GovernanceStaker__InsufficientRewardBalance();

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

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

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

/// @notice Metadata associated with a discrete staking deposit.
/// @param balance The deposit's staked balance.
Expand Down Expand Up @@ -193,7 +193,7 @@ contract UniStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonces {
/// @param _stakeToken Delegable governance token which users will stake to earn rewards.
/// @param _admin Address which will have permission to manage rewardNotifiers.
constructor(IERC20 _rewardToken, IERC20Delegates _stakeToken, address _admin)
EIP712("UniStaker", "1")
EIP712("GovernanceStaker", "1")
{
REWARD_TOKEN = _rewardToken;
STAKE_TOKEN = _stakeToken;
Expand Down Expand Up @@ -608,7 +608,9 @@ contract UniStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonces {
/// required that a notifier contract always transfers the `_amount` to this contract before
/// calling this method.
function notifyRewardAmount(uint256 _amount) external {
if (!isRewardNotifier[msg.sender]) revert UniStaker__Unauthorized("not notifier", msg.sender);
if (!isRewardNotifier[msg.sender]) {
revert GovernanceStaker__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 @@ -624,7 +626,7 @@ contract UniStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonces {
rewardEndTime = block.timestamp + REWARD_DURATION;
lastCheckpointTime = block.timestamp;

if ((scaledRewardRate / SCALE_FACTOR) == 0) revert UniStaker__InvalidRewardRate();
if ((scaledRewardRate / SCALE_FACTOR) == 0) revert GovernanceStaker__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 @@ -633,7 +635,7 @@ contract UniStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonces {
// admin.
if (
(scaledRewardRate * REWARD_DURATION) > (REWARD_TOKEN.balanceOf(address(this)) * SCALE_FACTOR)
) revert UniStaker__InsufficientRewardBalance();
) revert GovernanceStaker__InsufficientRewardBalance();

emit RewardNotified(_amount, msg.sender);
}
Expand Down Expand Up @@ -839,33 +841,34 @@ contract UniStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonces {
admin = _newAdmin;
}

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

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

/// @notice Internal helper method which reverts with UniStaker__InvalidAddress if the account in
/// question is address zero.
/// @notice Internal helper method which reverts with GovernanceStaker__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 UniStaker__InvalidAddress();
if (_account == address(0)) revert GovernanceStaker__InvalidAddress();
}

function _revertIfPastDeadline(uint256 _deadline) internal view {
if (block.timestamp > _deadline) revert UniStaker__ExpiredDeadline();
if (block.timestamp > _deadline) revert GovernanceStaker__ExpiredDeadline();
}

/// @notice Internal helper method which reverts with UniStaker__InvalidSignature if the signature
/// is invalid.
/// @notice Internal helper method which reverts with GovernanceStaker__InvalidSignature if the
/// signature is invalid.
/// @param _signer Address of the signer.
/// @param _hash Hash of the message.
/// @param _signature Signature to validate.
Expand All @@ -874,6 +877,6 @@ contract UniStaker is INotifiableRewardReceiver, Multicall, EIP712, Nonces {
view
{
bool _isValid = SignatureChecker.isValidSignatureNow(_signer, _hash, _signature);
if (!_isValid) revert UniStaker__InvalidSignature();
if (!_isValid) revert GovernanceStaker__InvalidSignature();
}
}
10 changes: 5 additions & 5 deletions src/interfaces/INotifiableRewardReceiver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
pragma solidity ^0.8.23;

/// @title INotifiableRewardReceiver
/// @author ScopeLift
/// @notice The communication interface between the V3FactoryOwner contract and the UniStaker
/// contract. In particular, the V3FactoryOwner only needs to know the latter implements the
/// specified method in order to forward payouts to the UniStaker contract. The UniStaker contract
/// receives the rewards and abstracts the distribution mechanics
/// @author [ScopeLift](https://scopelift.co)
/// @notice The communication interface between contracts that distribute rewards and the
/// GovernanceStaker contract. In particular, said contracts only need to know the staker
/// implements the specified method in order to forward payouts to the staker contract. The
/// GovernanceStaker contract receives the rewards and abstracts the distribution mechanics.
interface INotifiableRewardReceiver {
/// @notice Method called to notify a reward receiver it has received a reward.
/// @param _amount The amount of reward.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ pragma solidity ^0.8.23;
import {Test} from "forge-std/Test.sol";
import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";

import {UniStaker} from "src/UniStaker.sol";
import {UniStakerHandler} from "test/helpers/UniStaker.handler.sol";
import {GovernanceStaker} from "src/GovernanceStaker.sol";
import {GovernanceStakerHandler} from "test/helpers/GovernanceStaker.handler.sol";
import {ERC20VotesMock} from "test/mocks/MockERC20Votes.sol";
import {ERC20Fake} from "test/fakes/ERC20Fake.sol";

contract UniStakerInvariants is Test {
UniStakerHandler public handler;
UniStaker public uniStaker;
contract GovernanceStakerInvariants is Test {
GovernanceStakerHandler public handler;
GovernanceStaker public govStaker;
ERC20Fake rewardToken;
ERC20VotesMock govToken;
address rewardsNotifier;
Expand All @@ -25,17 +25,17 @@ contract UniStakerInvariants is Test {

rewardsNotifier = address(0xaffab1ebeef);
vm.label(rewardsNotifier, "Rewards Notifier");
uniStaker = new UniStaker(rewardToken, govToken, rewardsNotifier);
handler = new UniStakerHandler(uniStaker);
govStaker = new GovernanceStaker(rewardToken, govToken, rewardsNotifier);
handler = new GovernanceStakerHandler(govStaker);

bytes4[] memory selectors = new bytes4[](7);
selectors[0] = UniStakerHandler.stake.selector;
selectors[1] = UniStakerHandler.validStakeMore.selector;
selectors[2] = UniStakerHandler.validWithdraw.selector;
selectors[3] = UniStakerHandler.warpAhead.selector;
selectors[4] = UniStakerHandler.claimReward.selector;
selectors[5] = UniStakerHandler.enableRewardNotifier.selector;
selectors[6] = UniStakerHandler.notifyRewardAmount.selector;
selectors[0] = GovernanceStakerHandler.stake.selector;
selectors[1] = GovernanceStakerHandler.validStakeMore.selector;
selectors[2] = GovernanceStakerHandler.validWithdraw.selector;
selectors[3] = GovernanceStakerHandler.warpAhead.selector;
selectors[4] = GovernanceStakerHandler.claimReward.selector;
selectors[5] = GovernanceStakerHandler.enableRewardNotifier.selector;
selectors[6] = GovernanceStakerHandler.notifyRewardAmount.selector;

targetSelector(FuzzSelector({addr: address(handler), selectors: selectors}));

Expand All @@ -45,19 +45,19 @@ contract UniStakerInvariants is Test {
// Invariants

function invariant_Sum_of_all_depositor_balances_equals_total_stake() public {
assertEq(uniStaker.totalStaked(), handler.reduceDepositors(0, this.accumulateDeposits));
assertEq(govStaker.totalStaked(), handler.reduceDepositors(0, this.accumulateDeposits));
}

function invariant_Sum_of_beneficiary_earning_power_equals_total_stake() public {
assertEq(uniStaker.totalStaked(), handler.reduceBeneficiaries(0, this.accumulateEarningPower));
assertEq(govStaker.totalStaked(), handler.reduceBeneficiaries(0, this.accumulateEarningPower));
}

function invariant_Sum_of_surrogate_balance_equals_total_stake() public {
assertEq(uniStaker.totalStaked(), handler.reduceDelegates(0, this.accumulateSurrogateBalance));
assertEq(govStaker.totalStaked(), handler.reduceDelegates(0, this.accumulateSurrogateBalance));
}

function invariant_Cumulative_staked_minus_withdrawals_equals_total_stake() public view {
assertEq(uniStaker.totalStaked(), handler.ghost_stakeSum() - handler.ghost_stakeWithdrawn());
assertEq(govStaker.totalStaked(), handler.ghost_stakeSum() - handler.ghost_stakeWithdrawn());
}

function invariant_Sum_of_notified_rewards_equals_all_claimed_rewards_plus_rewards_left()
Expand All @@ -66,21 +66,21 @@ contract UniStakerInvariants is Test {
{
assertEq(
handler.ghost_rewardsNotified(),
rewardToken.balanceOf(address(uniStaker)) + handler.ghost_rewardsClaimed()
rewardToken.balanceOf(address(govStaker)) + handler.ghost_rewardsClaimed()
);
}

function invariant_Sum_of_unclaimed_reward_should_be_less_than_or_equal_to_total_rewards() public {
assertLe(
handler.reduceBeneficiaries(0, this.accumulateUnclaimedReward),
rewardToken.balanceOf(address(uniStaker))
rewardToken.balanceOf(address(govStaker))
);
}

function invariant_RewardPerTokenAccumulatedCheckpoint_should_be_greater_or_equal_to_the_last_rewardPerTokenAccumulatedCheckpoint(
) public view {
assertGe(
uniStaker.rewardPerTokenAccumulatedCheckpoint(),
govStaker.rewardPerTokenAccumulatedCheckpoint(),
handler.ghost_prevRewardPerTokenAccumulatedCheckpoint()
);
}
Expand All @@ -93,31 +93,31 @@ contract UniStakerInvariants is Test {
// Helpers

function accumulateDeposits(uint256 balance, address depositor) external view returns (uint256) {
return balance + uniStaker.depositorTotalStaked(depositor);
return balance + govStaker.depositorTotalStaked(depositor);
}

function accumulateEarningPower(uint256 earningPower, address caller)
external
view
returns (uint256)
{
return earningPower + uniStaker.earningPower(caller);
return earningPower + govStaker.earningPower(caller);
}

function accumulateUnclaimedReward(uint256 unclaimedReward, address beneficiary)
external
view
returns (uint256)
{
return unclaimedReward + uniStaker.unclaimedReward(beneficiary);
return unclaimedReward + govStaker.unclaimedReward(beneficiary);
}

function accumulateSurrogateBalance(uint256 balance, address delegate)
external
view
returns (uint256)
{
address surrogateAddr = address(uniStaker.surrogates(delegate));
return balance + IERC20(address(uniStaker.STAKE_TOKEN())).balanceOf(surrogateAddr);
address surrogateAddr = address(govStaker.surrogates(delegate));
return balance + IERC20(address(govStaker.STAKE_TOKEN())).balanceOf(surrogateAddr);
}
}
Loading

0 comments on commit ca7001f

Please sign in to comment.