diff --git a/contracts/governance/GearStakingV3.sol b/contracts/governance/GearStakingV3.sol index c1841465..9d2f2d8c 100644 --- a/contracts/governance/GearStakingV3.sol +++ b/contracts/governance/GearStakingV3.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.17; import {SafeERC20} from "@1inch/solidity-utils/contracts/libraries/SafeERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {AP_GEAR_TOKEN, IAddressProviderV3, NO_VERSION_CONTROL} from "../interfaces/IAddressProviderV3.sol"; @@ -68,10 +69,28 @@ contract GearStakingV3 is ACLNonReentrantTrait, IGearStakingV3 { /// @notice Stakes given amount of GEAR, and, optionally, performs a sequence of votes /// @param amount Amount of GEAR to stake /// @param votes Sequence of votes to perform, see `MultiVote` + /// @dev Requires approval from `msg.sender` for GEAR to this contract function deposit(uint96 amount, MultiVote[] calldata votes) external override nonReentrant { _deposit(amount, msg.sender, votes); // U: [GS-02] } + /// @notice Same as `deposit` but uses signed EIP-2612 permit message + /// @param amount Amount of GEAR to stake + /// @param votes Sequence of votes to perform, see `MultiVote` + /// @param deadline Permit deadline + /// @dev `v`, `r`, `s` must be a valid signature of the permit message from `msg.sender` for GEAR to this contract + function depositWithPermit( + uint96 amount, + MultiVote[] calldata votes, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external override nonReentrant { + try IERC20Permit(gear).permit(msg.sender, address(this), amount, deadline, v, r, s) {} catch {} // U:[GS-02] + _deposit(amount, msg.sender, votes); // U:[GS-02] + } + /// @dev Implementation of `deposit` function _deposit(uint96 amount, address to, MultiVote[] calldata votes) internal { IERC20(gear).safeTransferFrom(msg.sender, address(this), amount); diff --git a/contracts/interfaces/IGearStakingV3.sol b/contracts/interfaces/IGearStakingV3.sol index b87c82a8..1631da22 100644 --- a/contracts/interfaces/IGearStakingV3.sol +++ b/contracts/interfaces/IGearStakingV3.sol @@ -83,6 +83,15 @@ interface IGearStakingV3 is IGearStakingV3Events, IVersion { function deposit(uint96 amount, MultiVote[] calldata votes) external; + function depositWithPermit( + uint96 amount, + MultiVote[] calldata votes, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + function multivote(MultiVote[] calldata votes) external; function withdraw(uint96 amount, address to, MultiVote[] calldata votes) external; diff --git a/contracts/test/mocks/governance/GearStakingMock.sol b/contracts/test/mocks/governance/GearStakingMock.sol index c7f59ca1..4c022454 100644 --- a/contracts/test/mocks/governance/GearStakingMock.sol +++ b/contracts/test/mocks/governance/GearStakingMock.sol @@ -20,6 +20,15 @@ contract GearStakingMock is IGearStakingV3 { function deposit(uint96 amount, MultiVote[] calldata votes) external {} + function depositWithPermit( + uint96 amount, + MultiVote[] calldata votes, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external {} + function multivote(MultiVote[] calldata votes) external {} function withdraw(uint96 amount, address to, MultiVote[] calldata votes) external {} diff --git a/contracts/test/unit/governance/GearStaking.t.sol b/contracts/test/unit/governance/GearStaking.t.sol index 89ee7ab0..a1f88edb 100644 --- a/contracts/test/unit/governance/GearStaking.t.sol +++ b/contracts/test/unit/governance/GearStaking.t.sol @@ -8,6 +8,7 @@ import {IGearStakingV3Events, MultiVote, VotingContractStatus} from "../../../in import {IVotingContractV3} from "../../../interfaces/IVotingContractV3.sol"; import "../../../interfaces/IAddressProviderV3.sol"; +import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; // TEST import "../../lib/constants.sol"; @@ -79,17 +80,40 @@ contract GearStakingTest is Test, IGearStakingV3Events { tokenTestSuite.mint(gearToken, USER, WAD); tokenTestSuite.approve(gearToken, USER, address(gearStaking)); - vm.expectEmit(true, false, false, true); - emit DepositGear(USER, WAD); + uint256 snapshot = vm.snapshot(); + for (uint256 i; i < 2; ++i) { + bool withPermit = i == 1; - vm.expectCall(address(votingContract), abi.encodeCall(IVotingContractV3.vote, (USER, uint96(WAD / 2), ""))); + vm.expectEmit(true, false, false, true); + emit DepositGear(USER, WAD); - vm.prank(USER); - gearStaking.deposit(uint96(WAD), votes); + if (withPermit) { + vm.mockCall( + gearToken, + abi.encodeCall(IERC20Permit.permit, (USER, address(gearStaking), WAD, 0, 0, bytes32(0), bytes32(0))), + bytes("") + ); + vm.expectCall( + gearToken, + abi.encodeCall(IERC20Permit.permit, (USER, address(gearStaking), WAD, 0, 0, bytes32(0), bytes32(0))) + ); + } - assertEq(gearStaking.balanceOf(USER), WAD); + vm.expectCall(address(votingContract), abi.encodeCall(IVotingContractV3.vote, (USER, uint96(WAD / 2), ""))); - assertEq(gearStaking.availableBalance(USER), WAD / 2); + vm.prank(USER); + if (withPermit) { + gearStaking.depositWithPermit(uint96(WAD), votes, 0, 0, bytes32(0), bytes32(0)); + } else { + gearStaking.deposit(uint96(WAD), votes); + } + + assertEq(gearStaking.balanceOf(USER), WAD); + + assertEq(gearStaking.availableBalance(USER), WAD / 2); + + vm.revertTo(snapshot); + } } /// @dev U:[GS-03]: withdraw performs operations in order and emits events