Skip to content

Commit

Permalink
Change stake token to require an IERC20 (#78)
Browse files Browse the repository at this point in the history
* Generalize governance staking to take an IERC20
  • Loading branch information
alexkeating authored Nov 19, 2024
1 parent 3503269 commit db2de2c
Show file tree
Hide file tree
Showing 12 changed files with 87 additions and 87 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ jobs:
uses: zgosalvez/github-actions-report-lcov@v2
with:
coverage-files: ./lcov.info
minimum-coverage: 98
minimum-coverage: 97.9

lint:
runs-on: ubuntu-latest
Expand Down
4 changes: 2 additions & 2 deletions script/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {Script} from "forge-std/Script.sol";

import {DeployInput} from "script/DeployInput.sol";
import {GovernanceStakerHarness} from "test/harnesses/GovernanceStakerHarness.sol";
import {IERC20Delegates} from "src/interfaces/IERC20Delegates.sol";
import {IERC20Staking} from "src/interfaces/IERC20Staking.sol";
import {INotifiableRewardReceiver} from "src/interfaces/INotifiableRewardReceiver.sol";
import {IEarningPowerCalculator} from "src/interfaces/IEarningPowerCalculator.sol";

Expand All @@ -28,7 +28,7 @@ contract Deploy is Script, DeployInput {
// TODO: Replace with the `ArbitrumStaker` contract once it is developed
GovernanceStakerHarness govStaker = new GovernanceStakerHarness(
IERC20(PAYOUT_TOKEN_ADDRESS),
IERC20Delegates(STAKE_TOKEN_ADDRESS),
IERC20Staking(STAKE_TOKEN_ADDRESS),
IEarningPowerCalculator(address(0)),
MAX_BUMP_TIP,
vm.addr(deployerPrivateKey),
Expand Down
5 changes: 2 additions & 3 deletions src/GovernanceStaker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {DelegationSurrogate} from "src/DelegationSurrogate.sol";
import {DelegationSurrogateVotes} from "src/DelegationSurrogateVotes.sol";
import {INotifiableRewardReceiver} from "src/interfaces/INotifiableRewardReceiver.sol";
import {IEarningPowerCalculator} from "src/interfaces/IEarningPowerCalculator.sol";
import {IERC20Delegates} from "src/interfaces/IERC20Delegates.sol";
import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";
import {SafeERC20} from "openzeppelin/token/ERC20/utils/SafeERC20.sol";
import {Multicall} from "openzeppelin/utils/Multicall.sol";
Expand Down Expand Up @@ -158,7 +157,7 @@ abstract contract GovernanceStaker is INotifiableRewardReceiver, Multicall {
IERC20 public immutable REWARD_TOKEN;

/// @notice Delegable governance token which users stake to earn rewards.
IERC20Delegates public immutable STAKE_TOKEN;
IERC20 public immutable STAKE_TOKEN;

/// @notice Length of time over which rewards sent to this contract are distributed to stakers.
uint256 public constant REWARD_DURATION = 30 days;
Expand Down Expand Up @@ -225,7 +224,7 @@ abstract contract GovernanceStaker is INotifiableRewardReceiver, Multicall {
/// @param _admin Address which will have permission to manage rewardNotifiers.
constructor(
IERC20 _rewardToken,
IERC20Delegates _stakeToken,
IERC20 _stakeToken,
IEarningPowerCalculator _earningPowerCalculator,
uint256 _maxBumpTip,
address _admin
Expand Down
14 changes: 12 additions & 2 deletions src/extensions/GovernanceStakerDelegateSurrogateVotes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ pragma solidity ^0.8.23;
import {DelegationSurrogate} from "src/DelegationSurrogate.sol";
import {DelegationSurrogateVotes} from "src/DelegationSurrogateVotes.sol";
import {GovernanceStaker} from "src/GovernanceStaker.sol";
import {IERC20Delegates} from "src/interfaces/IERC20Delegates.sol";

/// @title GovernaceStakerDelegateSurrogateVotes
/// @title GovernanceStakerDelegateSurrogateVotes
/// @author [ScopeLift](https://scopelift.co)
/// @notice This contract extension adds delegation surrogates to the GovernanceStaker base
/// contract, allowing staked tokens to be delegated to a specific delegate.
Expand All @@ -17,6 +18,15 @@ abstract contract GovernanceStakerDelegateSurrogateVotes is GovernanceStaker {
/// 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.
error GovernanceStakerDelegateSurrogateVotes__UnauthorizedToken();

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

/// @inheritdoc GovernanceStaker
function surrogates(address _delegatee) public view override returns (DelegationSurrogate) {
return storedSurrogates[_delegatee];
Expand All @@ -32,7 +42,7 @@ abstract contract GovernanceStakerDelegateSurrogateVotes is GovernanceStaker {
_surrogate = storedSurrogates[_delegatee];

if (address(_surrogate) == address(0)) {
_surrogate = new DelegationSurrogateVotes(STAKE_TOKEN, _delegatee);
_surrogate = new DelegationSurrogateVotes(IERC20Delegates(address(STAKE_TOKEN)), _delegatee);
storedSurrogates[_delegatee] = _surrogate;
emit SurrogateDeployed(_delegatee, address(_surrogate));
}
Expand Down
18 changes: 16 additions & 2 deletions src/extensions/GovernanceStakerPermitAndStake.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.23;

import {GovernanceStaker} from "src/GovernanceStaker.sol";
import {IERC20Permit} from "openzeppelin/token/ERC20/extensions/IERC20Permit.sol";

/// @title GovernanceStakerPermitAndStake
/// @author [ScopeLift](https://scopelift.co)
Expand All @@ -11,6 +12,15 @@ import {GovernanceStaker} from "src/GovernanceStaker.sol";
/// enabling users to approve and stake tokens in a single transaction.
/// Note that this extension requires the stake token to support EIP-2612 permit functionality.
abstract contract GovernanceStakerPermitAndStake is GovernanceStaker {
/// @notice Thrown if an inheritor uses a seperate staking token.
error GovernanceStakerPermitAndStake__UnauthorizedToken();

constructor(IERC20Permit _permitToken) {
if (address(STAKE_TOKEN) != address(_permitToken)) {
revert GovernanceStakerPermitAndStake__UnauthorizedToken();
}
}

/// @notice Method to stake tokens to a new deposit. Before the staking operation occurs, a
/// signature is passed to the token contract's permit method to spend the would-be staked amount
/// of the token.
Expand All @@ -33,7 +43,9 @@ abstract contract GovernanceStakerPermitAndStake is GovernanceStaker {
bytes32 _r,
bytes32 _s
) external virtual returns (DepositIdentifier _depositId) {
try STAKE_TOKEN.permit(msg.sender, address(this), _amount, _deadline, _v, _r, _s) {} catch {}
try IERC20Permit(address(STAKE_TOKEN)).permit(
msg.sender, address(this), _amount, _deadline, _v, _r, _s
) {} catch {}
_depositId = _stake(msg.sender, _amount, _delegatee, _claimer);
}

Expand All @@ -59,7 +71,9 @@ abstract contract GovernanceStakerPermitAndStake is GovernanceStaker {
Deposit storage deposit = deposits[_depositId];
_revertIfNotDepositOwner(deposit, msg.sender);

try STAKE_TOKEN.permit(msg.sender, address(this), _amount, _deadline, _v, _r, _s) {} catch {}
try IERC20Permit(address(STAKE_TOKEN)).permit(
msg.sender, address(this), _amount, _deadline, _v, _r, _s
) {} catch {}
_stakeMore(deposit, _depositId, _amount);
}
}
9 changes: 9 additions & 0 deletions src/interfaces/IDelegates.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.23;

/// @notice An inteface that contains the necessary `IVotes` functions for the governance staking
/// system.
interface IDelegates {
function delegate(address delegatee) external;
function delegates(address account) external view returns (address);
}
32 changes: 5 additions & 27 deletions src/interfaces/IERC20Delegates.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,10 @@
pragma solidity ^0.8.23;

import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";
import {IDelegates} from "src/interfaces/IDelegates.sol";

/// @notice A subset of the ERC20Votes-style governance token to which UNI conforms.
/// Methods related to standard ERC20 functionality and to delegation are included.
/// These methods are needed in the context of this system. Methods related to check pointing,
/// @notice A subset of the ERC20Votes-style governance token to which a staking token should
/// conform. Methods related to standard ERC20 functionality and to delegation are included.
/// These methods are needed in the context of this system. Methods related to checkpointing,
/// past voting weights, and other functionality are omitted.
interface IERC20Delegates is IERC20 {
// ERC20 related methods
function allowance(address account, address spender) external view returns (uint256);
function approve(address spender, uint256 rawAmount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
function decimals() external view returns (uint8);
function symbol() external view returns (string memory);
function totalSupply() external view returns (uint256);
function transfer(address dst, uint256 rawAmount) external returns (bool);
function transferFrom(address src, address dst, uint256 rawAmount) external returns (bool);
function permit(
address owner,
address spender,
uint256 rawAmount,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external;

// ERC20Votes delegation methods
function delegate(address delegatee) external;
function delegates(address) external view returns (address);
}
interface IERC20Delegates is IERC20, IDelegates {}
10 changes: 10 additions & 0 deletions src/interfaces/IERC20Staking.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.23;

import {IERC20Permit} from "openzeppelin/token/ERC20/extensions/IERC20Permit.sol";
import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";
import {IERC20Delegates} from "src/interfaces/IERC20Delegates.sol";

/// @notice The interface of an ERC20 that supports a governor staker and all of the created
/// extensions.
interface IERC20Staking is IERC20Delegates, IERC20Permit {}
10 changes: 3 additions & 7 deletions test/GovernanceStaker.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@
pragma solidity ^0.8.23;

import {Vm, Test, stdStorage, StdStorage, console2, stdError} from "forge-std/Test.sol";
import {
GovernanceStaker,
IERC20,
IERC20Delegates,
IEarningPowerCalculator
} from "src/GovernanceStaker.sol";
import {GovernanceStaker, IERC20, IEarningPowerCalculator} from "src/GovernanceStaker.sol";
import {IERC20Staking} from "src/interfaces/IERC20Staking.sol";
import {DelegationSurrogate} from "src/DelegationSurrogate.sol";
import {GovernanceStakerHarness} from "test/harnesses/GovernanceStakerHarness.sol";
import {GovernanceStakerOnBehalf} from "src/extensions/GovernanceStakerOnBehalf.sol";
Expand Down Expand Up @@ -247,7 +243,7 @@ contract Constructor is GovernanceStakerTest {
vm.assume(_admin != address(0) && _earningPowerCalculator != address(0));
GovernanceStakerHarness _govStaker = new GovernanceStakerHarness(
IERC20(_rewardToken),
IERC20Delegates(_stakeToken),
IERC20Staking(_stakeToken),
IEarningPowerCalculator(_earningPowerCalculator),
_maxBumpTip,
_admin,
Expand Down
10 changes: 3 additions & 7 deletions test/GovernanceStakerOnBehalf.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,8 @@ import {stdStorage, StdStorage} from "forge-std/Test.sol";
import {GovernanceStakerOnBehalf} from "src/extensions/GovernanceStakerOnBehalf.sol";
import {GovernanceStakerTest, GovernanceStakerRewardsTest} from "test/GovernanceStaker.t.sol";
import {GovernanceStakerHarness} from "test/harnesses/GovernanceStakerHarness.sol";
import {
GovernanceStaker,
IERC20,
IERC20Delegates,
IEarningPowerCalculator
} from "src/GovernanceStaker.sol";
import {GovernanceStaker, IERC20, IEarningPowerCalculator} from "src/GovernanceStaker.sol";
import {IERC20Staking} from "src/interfaces/IERC20Staking.sol";

contract Domain_Separator is GovernanceStakerTest {
function _buildDomainSeparator(string memory _name, string memory _version, address _contract)
Expand Down Expand Up @@ -39,7 +35,7 @@ contract Domain_Separator is GovernanceStakerTest {
vm.assume(_admin != address(0) && _earningPowerCalculator != address(0));
GovernanceStakerHarness _govStaker = new GovernanceStakerHarness(
IERC20(_rewardToken),
IERC20Delegates(_stakeToken),
IERC20Staking(_stakeToken),
IEarningPowerCalculator(_earningPowerCalculator),
_maxBumpTip,
_admin,
Expand Down
5 changes: 4 additions & 1 deletion test/harnesses/GovernanceStakerHarness.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {GovernanceStakerDelegateSurrogateVotes} from
import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";
import {SafeERC20} from "openzeppelin/token/ERC20/utils/SafeERC20.sol";
import {EIP712} from "openzeppelin/utils/cryptography/EIP712.sol";
import {IERC20Staking} from "src/interfaces/IERC20Staking.sol";
import {IERC20Delegates} from "src/interfaces/IERC20Delegates.sol";
import {IEarningPowerCalculator} from "src/interfaces/IEarningPowerCalculator.sol";
import {DelegationSurrogate} from "src/DelegationSurrogate.sol";
Expand All @@ -23,13 +24,15 @@ contract GovernanceStakerHarness is
{
constructor(
IERC20 _rewardsToken,
IERC20Delegates _stakeToken,
IERC20Staking _stakeToken,
IEarningPowerCalculator _earningPowerCalculator,
uint256 _maxBumpTip,
address _admin,
string memory _name
)
GovernanceStaker(_rewardsToken, _stakeToken, _earningPowerCalculator, _maxBumpTip, _admin)
GovernanceStakerPermitAndStake(_stakeToken)
GovernanceStakerDelegateSurrogateVotes(_stakeToken)
EIP712(_name, "1")
{
MAX_CLAIM_FEE = 1e18;
Expand Down
55 changes: 20 additions & 35 deletions test/mocks/MockERC20Votes.sol
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.23;

import {
ERC20, ERC20Permit, IERC20Permit
} from "openzeppelin/token/ERC20/extensions/ERC20Permit.sol";
import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";
import {IERC20Delegates} from "src/interfaces/IERC20Delegates.sol";
import {ERC20, ERC20Permit} from "openzeppelin/token/ERC20/extensions/ERC20Permit.sol";
import {IERC20Staking} from "src/interfaces/IERC20Staking.sol";

/// @dev An ERC20Permit token that allows for public minting and mocks the delegation methods used
/// in ERC20Votes governance tokens. It does not included check pointing functionality. This
/// contract is intended only for use as a stand in for contracts that interface with ERC20Votes
// tokens.
contract ERC20VotesMock is IERC20Delegates, ERC20Permit {
contract ERC20VotesMock is IERC20Staking, ERC20Permit {
/// @dev Track delegations for mocked delegation methods
mapping(address account => address delegate) private delegations;

Expand Down Expand Up @@ -39,66 +43,47 @@ contract ERC20VotesMock is IERC20Delegates, ERC20Permit {
function allowance(address account, address spender)
public
view
override(IERC20Delegates, ERC20)
override(IERC20, ERC20)
returns (uint256)
{
return ERC20.allowance(account, spender);
}

function balanceOf(address account)
public
view
override(IERC20Delegates, ERC20)
returns (uint256)
{
function balanceOf(address account) public view override(IERC20, ERC20) returns (uint256) {
return ERC20.balanceOf(account);
}

function approve(address spender, uint256 rawAmount)
public
override(IERC20Delegates, ERC20)
override(IERC20, ERC20)
returns (bool)
{
return ERC20.approve(spender, rawAmount);
}

function decimals() public view override(IERC20Delegates, ERC20) returns (uint8) {
return ERC20.decimals();
}

function symbol() public view override(IERC20Delegates, ERC20) returns (string memory) {
return ERC20.symbol();
}

function totalSupply() public view override(IERC20Delegates, ERC20) returns (uint256) {
function totalSupply() public view override(IERC20, ERC20) returns (uint256) {
return ERC20.totalSupply();
}

function transfer(address dst, uint256 rawAmount)
public
override(IERC20Delegates, ERC20)
returns (bool)
{
function transfer(address dst, uint256 rawAmount) public override(IERC20, ERC20) returns (bool) {
return ERC20.transfer(dst, rawAmount);
}

function transferFrom(address src, address dst, uint256 rawAmount)
public
override(IERC20Delegates, ERC20)
override(IERC20, ERC20)
returns (bool)
{
return ERC20.transferFrom(src, dst, rawAmount);
}

function permit(
address owner,
address spender,
uint256 rawAmount,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public override(IERC20Delegates, ERC20Permit) {
return ERC20Permit.permit(owner, spender, rawAmount, deadline, v, r, s);
function nonces(address owner)
public
view
virtual
override(ERC20Permit, IERC20Permit)
returns (uint256)
{
return ERC20Permit.nonces(owner);
}
}

0 comments on commit db2de2c

Please sign in to comment.