diff --git a/src/GovernanceStaker.sol b/src/GovernanceStaker.sol index e618514..2d68f81 100644 --- a/src/GovernanceStaker.sol +++ b/src/GovernanceStaker.sol @@ -380,32 +380,6 @@ abstract contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP7 _depositId = _stake(msg.sender, _amount, _delegatee, _beneficiary); } - /// @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. - /// @param _amount Quantity of the staking token to stake. - /// @param _delegatee Address to assign the governance voting weight of the staked tokens. - /// @param _beneficiary Address that will accrue rewards for this stake. - /// @param _deadline The timestamp after which the permit signature should expire. - /// @param _v ECDSA signature component: Parity of the `y` coordinate of point `R` - /// @param _r ECDSA signature component: x-coordinate of `R` - /// @param _s ECDSA signature component: `s` value of the signature - /// @return _depositId Unique identifier for this deposit. - /// @dev Neither the delegatee nor the beneficiary may be the zero address. The deposit will be - /// owned by the message sender. - function permitAndStake( - uint256 _amount, - address _delegatee, - address _beneficiary, - uint256 _deadline, - uint8 _v, - bytes32 _r, - bytes32 _s - ) external virtual returns (DepositIdentifier _depositId) { - try STAKE_TOKEN.permit(msg.sender, address(this), _amount, _deadline, _v, _r, _s) {} catch {} - _depositId = _stake(msg.sender, _amount, _delegatee, _beneficiary); - } - /// @notice Stake tokens to a new deposit on behalf of a user, using a signature to validate the /// user's intent. The caller must pre-approve the staking contract to spend at least the /// would-be staked amount of the token. @@ -458,32 +432,6 @@ abstract contract GovernanceStaker is INotifiableRewardReceiver, Multicall, EIP7 _stakeMore(deposit, _depositId, _amount); } - /// @notice Add more staking tokens to an existing deposit. A staker should call this method when - /// they have an existing deposit, and wish to stake more while retaining the same delegatee and - /// beneficiary. 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. - /// @param _depositId Unique identifier of the deposit to which stake will be added. - /// @param _amount Quantity of stake to be added. - /// @param _deadline The timestamp after which the permit signature should expire. - /// @param _v ECDSA signature component: Parity of the `y` coordinate of point `R` - /// @param _r ECDSA signature component: x-coordinate of `R` - /// @param _s ECDSA signature component: `s` value of the signature - /// @dev The message sender must be the owner of the deposit. - function permitAndStakeMore( - DepositIdentifier _depositId, - uint256 _amount, - uint256 _deadline, - uint8 _v, - bytes32 _r, - bytes32 _s - ) external virtual { - Deposit storage deposit = deposits[_depositId]; - _revertIfNotDepositOwner(deposit, msg.sender); - - try STAKE_TOKEN.permit(msg.sender, address(this), _amount, _deadline, _v, _r, _s) {} catch {} - _stakeMore(deposit, _depositId, _amount); - } - /// @notice Add more staking tokens to an existing deposit on behalf of a user, using a signature /// to validate the user's intent. A staker should call this method when they have an existing /// deposit, and wish to stake more while retaining the same delegatee and beneficiary. diff --git a/src/extensions/GovernanceStakerPermitAndStake.sol b/src/extensions/GovernanceStakerPermitAndStake.sol new file mode 100644 index 0000000..e252a30 --- /dev/null +++ b/src/extensions/GovernanceStakerPermitAndStake.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.23; + +import {GovernanceStaker} from "src/GovernanceStaker.sol"; + +/// @title GovernanceStakerPermitAndStake +/// @author [ScopeLift](https://scopelift.co) +/// @notice This contract extension adds permit functionality to the GovernanceStaker base contract, +/// allowing token approvals to happen via signatures rather than requiring a separate transaction. +/// The permit functionality is used in conjunction with staking operations, improving UX by +/// 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 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. + /// @param _amount Quantity of the staking token to stake. + /// @param _delegatee Address to assign the governance voting weight of the staked tokens. + /// @param _beneficiary Address that will accrue rewards for this stake. + /// @param _deadline The timestamp after which the permit signature should expire. + /// @param _v ECDSA signature component: Parity of the `y` coordinate of point `R` + /// @param _r ECDSA signature component: x-coordinate of `R` + /// @param _s ECDSA signature component: `s` value of the signature + /// @return _depositId Unique identifier for this deposit. + /// @dev Neither the delegatee nor the beneficiary may be the zero address. The deposit will be + /// owned by the message sender. + function permitAndStake( + uint256 _amount, + address _delegatee, + address _beneficiary, + uint256 _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s + ) external virtual returns (DepositIdentifier _depositId) { + try STAKE_TOKEN.permit(msg.sender, address(this), _amount, _deadline, _v, _r, _s) {} catch {} + _depositId = _stake(msg.sender, _amount, _delegatee, _beneficiary); + } + + /// @notice Add more staking tokens to an existing deposit. A staker should call this method when + /// they have an existing deposit, and wish to stake more while retaining the same delegatee and + /// beneficiary. 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. + /// @param _depositId Unique identifier of the deposit to which stake will be added. + /// @param _amount Quantity of stake to be added. + /// @param _deadline The timestamp after which the permit signature should expire. + /// @param _v ECDSA signature component: Parity of the `y` coordinate of point `R` + /// @param _r ECDSA signature component: x-coordinate of `R` + /// @param _s ECDSA signature component: `s` value of the signature + /// @dev The message sender must be the owner of the deposit. + function permitAndStakeMore( + DepositIdentifier _depositId, + uint256 _amount, + uint256 _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s + ) external virtual { + Deposit storage deposit = deposits[_depositId]; + _revertIfNotDepositOwner(deposit, msg.sender); + + try STAKE_TOKEN.permit(msg.sender, address(this), _amount, _deadline, _v, _r, _s) {} catch {} + _stakeMore(deposit, _depositId, _amount); + } +} diff --git a/test/harnesses/GovernanceStakerHarness.sol b/test/harnesses/GovernanceStakerHarness.sol index c8a451b..e0a80bc 100644 --- a/test/harnesses/GovernanceStakerHarness.sol +++ b/test/harnesses/GovernanceStakerHarness.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.23; import {DelegationSurrogateVotes} from "src/DelegationSurrogateVotes.sol"; import {GovernanceStaker} from "src/GovernanceStaker.sol"; - +import {GovernanceStakerPermitAndStake} from "src/extensions/GovernanceStakerPermitAndStake.sol"; 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"; @@ -11,7 +11,7 @@ import {IERC20Delegates} from "src/interfaces/IERC20Delegates.sol"; import {IEarningPowerCalculator} from "src/interfaces/IEarningPowerCalculator.sol"; import {DelegationSurrogate} from "src/DelegationSurrogate.sol"; -contract GovernanceStakerHarness is GovernanceStaker { +contract GovernanceStakerHarness is GovernanceStaker, GovernanceStakerPermitAndStake { constructor( IERC20 _rewardsToken, IERC20Delegates _stakeToken,