diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 096ee8e..694e659 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 3c35935..80ea1a3 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -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"; @@ -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), diff --git a/src/GovernanceStaker.sol b/src/GovernanceStaker.sol index 8bc6a6a..9bfb63b 100644 --- a/src/GovernanceStaker.sol +++ b/src/GovernanceStaker.sol @@ -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"; @@ -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; @@ -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 diff --git a/src/extensions/GovernanceStakerDelegateSurrogateVotes.sol b/src/extensions/GovernanceStakerDelegateSurrogateVotes.sol index a6775c5..dfdcb5f 100644 --- a/src/extensions/GovernanceStakerDelegateSurrogateVotes.sol +++ b/src/extensions/GovernanceStakerDelegateSurrogateVotes.sol @@ -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. @@ -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]; @@ -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)); } diff --git a/src/extensions/GovernanceStakerPermitAndStake.sol b/src/extensions/GovernanceStakerPermitAndStake.sol index fa41356..c459a4e 100644 --- a/src/extensions/GovernanceStakerPermitAndStake.sol +++ b/src/extensions/GovernanceStakerPermitAndStake.sol @@ -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) @@ -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. @@ -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); } @@ -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); } } diff --git a/src/interfaces/IDelegates.sol b/src/interfaces/IDelegates.sol new file mode 100644 index 0000000..641e7c5 --- /dev/null +++ b/src/interfaces/IDelegates.sol @@ -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); +} diff --git a/src/interfaces/IERC20Delegates.sol b/src/interfaces/IERC20Delegates.sol index f962359..9678fd6 100644 --- a/src/interfaces/IERC20Delegates.sol +++ b/src/interfaces/IERC20Delegates.sol @@ -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 {} diff --git a/src/interfaces/IERC20Staking.sol b/src/interfaces/IERC20Staking.sol new file mode 100644 index 0000000..da42edf --- /dev/null +++ b/src/interfaces/IERC20Staking.sol @@ -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 {} diff --git a/test/GovernanceStaker.t.sol b/test/GovernanceStaker.t.sol index cc989e4..81cd56c 100644 --- a/test/GovernanceStaker.t.sol +++ b/test/GovernanceStaker.t.sol @@ -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"; @@ -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, diff --git a/test/GovernanceStakerOnBehalf.t.sol b/test/GovernanceStakerOnBehalf.t.sol index 6363dba..f04c427 100644 --- a/test/GovernanceStakerOnBehalf.t.sol +++ b/test/GovernanceStakerOnBehalf.t.sol @@ -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) @@ -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, diff --git a/test/harnesses/GovernanceStakerHarness.sol b/test/harnesses/GovernanceStakerHarness.sol index b340a81..2b5b6f0 100644 --- a/test/harnesses/GovernanceStakerHarness.sol +++ b/test/harnesses/GovernanceStakerHarness.sol @@ -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"; @@ -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; diff --git a/test/mocks/MockERC20Votes.sol b/test/mocks/MockERC20Votes.sol index 1451edf..e1f1eef 100644 --- a/test/mocks/MockERC20Votes.sol +++ b/test/mocks/MockERC20Votes.sol @@ -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; @@ -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); } }